Repository: microsoft/playwright-python Branch: main Commit: 3aecfcfded42 Files: 379 Total size: 3.6 MB Directory structure: gitextract_t_9ditla/ ├── .azure-pipelines/ │ ├── guardian/ │ │ └── SDL/ │ │ └── .gdnsuppress │ └── publish.yml ├── .claude/ │ └── skills/ │ └── playwright-roll/ │ └── SKILL.md ├── .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 │ │ ├── _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/ │ │ │ │ ├── localhost/ │ │ │ │ │ ├── localhost.csr │ │ │ │ │ ├── localhost.ext │ │ │ │ │ ├── localhost.key │ │ │ │ │ └── localhost.pem │ │ │ │ ├── self-signed/ │ │ │ │ │ ├── cert.pem │ │ │ │ │ ├── csr.pem │ │ │ │ │ └── key.pem │ │ │ │ └── trusted/ │ │ │ │ ├── cert-legacy.pfx │ │ │ │ ├── cert.pem │ │ │ │ ├── cert.pfx │ │ │ │ ├── csr.pem │ │ │ │ └── key.pem │ │ │ ├── generate.sh │ │ │ └── server/ │ │ │ ├── server_cert.pem │ │ │ └── server_key.pem │ │ ├── client.py │ │ ├── consolelog.html │ │ ├── csp.html │ │ ├── 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 │ │ ├── extension-mv3-simple/ │ │ │ ├── content-script.js │ │ │ ├── index.js │ │ │ └── manifest.json │ │ ├── extension-mv3-with-logging/ │ │ │ ├── background.js │ │ │ ├── content.js │ │ │ └── manifest.json │ │ ├── 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 │ │ ├── react.html │ │ ├── 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.json │ │ ├── title.html │ │ ├── worker/ │ │ │ ├── worker.html │ │ │ └── worker.js │ │ └── wrappedlink.html │ ├── async/ │ │ ├── __init__.py │ │ ├── conftest.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_extension.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_event_console.py │ │ ├── test_page_event_pageerror.py │ │ ├── test_page_event_request.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 │ ├── server.py │ ├── sync/ │ │ ├── __init__.py │ │ ├── conftest.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_extension.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_event_console.py │ │ ├── test_page_event_pageerror.py │ │ ├── test_page_event_request.py │ │ ├── test_page_network_response.py │ │ ├── test_page_request_fallback.py │ │ ├── test_page_request_gc.py │ │ ├── test_page_request_intercept.py │ │ ├── test_page_request_timeout.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .azure-pipelines/guardian/SDL/.gdnsuppress ================================================ { "hydrated": false, "properties": { "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/suppressions", "hydrationStatus": "This file does not contain identifying data. It is safe to check into your repo. To hydrate this file with identifying data, run `guardian hydrate --help` and follow the guidance." }, "version": "1.0.0", "suppressionSets": { "default": { "name": "default", "createdDate": "2024-02-06 21:00:02Z", "lastUpdatedDate": "2024-02-06 21:00:02Z" } }, "results": { "bffa73d7410f5963f2538f06124ac5524c076da77867a0a19ccf60e508062dff": { "signature": "bffa73d7410f5963f2538f06124ac5524c076da77867a0a19ccf60e508062dff", "alternativeSignatures": [], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "964642e3cd0f022d5b63f5d3c467d034df4b1664e58dd132b6cd54c98bdae6a1": { "signature": "964642e3cd0f022d5b63f5d3c467d034df4b1664e58dd132b6cd54c98bdae6a1", "alternativeSignatures": [ "f2d5560538c833834ca11e62fa6509618ab5454e1e71faf2847cb6fd07f4c4e0" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "5b0f97262e176cd67207fd63a2c74b9984829286e9229d10efc32d6b73130e37": { "signature": "5b0f97262e176cd67207fd63a2c74b9984829286e9229d10efc32d6b73130e37", "alternativeSignatures": [ "29a18985690880b8caeebc339c7d2afd107510838cdc6561c1f5493478712581" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "636fe8a4848f24231e94dc13a238022f90a2894cd47a483e351e467eeb98de52": { "signature": "636fe8a4848f24231e94dc13a238022f90a2894cd47a483e351e467eeb98de52", "alternativeSignatures": [ "e20632aa7941af4239fd857f802e05582c841fb9ae84e17c71ca6c7fc713246b" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "67ae7118600b0793ec3f0a58a753888e13ce4badcc15575614ee6aa622e5009c": { "signature": "67ae7118600b0793ec3f0a58a753888e13ce4badcc15575614ee6aa622e5009c", "alternativeSignatures": [ "d1e68c2c7d9815f47331dd34c31db2634804b45b078a53d00843082747155ac9" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "9b7d0de03b9e0e0b2711e31df7c804528c357bf5aa2d689fb5a5f42750e84077": { "signature": "9b7d0de03b9e0e0b2711e31df7c804528c357bf5aa2d689fb5a5f42750e84077", "alternativeSignatures": [ "e42bf5a49be2b1b815d1fde98ebf9d463fd2e70be1e8ca661f1210ce5b0c4953" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "06ecbceae8bfd10acf8c35f21af3926d172c7930f24a204cc58b61efc6c4c770": { "signature": "06ecbceae8bfd10acf8c35f21af3926d172c7930f24a204cc58b61efc6c4c770", "alternativeSignatures": [ "035d6eb1444a809987923a39793fbb1ab9e4462405f38f94bc425c579705a9f2" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "c103671af429c32de81f2dc2e7a92999de88a517d916a8f75c8e37448bb2efe9": { "signature": "c103671af429c32de81f2dc2e7a92999de88a517d916a8f75c8e37448bb2efe9", "alternativeSignatures": [ "3f904a503c12b62c2922900a2e689632e06272a815448939b1fdd435bcf74388" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" }, "d1196285a4e64cf6f0f7f22a29bf5b33b540137da1a89ed2af0c880d2a8c1d64": { "signature": "d1196285a4e64cf6f0f7f22a29bf5b33b540137da1a89ed2af0c880d2a8c1d64", "alternativeSignatures": [ "1c24094ca9e68a76a81c747853860e46fd139c9f47f0fdbad9133538e7d064b2" ], "memberOf": [ "default" ], "createdDate": "2024-02-06 21:00:02Z" } } } ================================================ FILE: .azure-pipelines/publish.yml ================================================ pr: none trigger: tags: include: - '*' resources: repositories: - repository: 1esPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: pool: name: DevDivPlaywrightAzurePipelinesUbuntu2204 os: linux sdl: sourceAnalysisPool: name: DevDivPlaywrightAzurePipelinesWindows2022 # The image must be windows-based due to restrictions of the SDL tools. See: https://aka.ms/AAo6v8e # In the case of a windows build, this can be the same as the above pool image. os: windows suppression: suppressionFile: $(Build.SourcesDirectory)\.azure-pipelines\guardian\SDL\.gdnsuppress stages: - stage: Stage jobs: - job: Build templateContext: outputs: - output: pipelineArtifact path: $(Build.ArtifactStagingDirectory)/esrp-build artifact: esrp-build steps: - task: UsePythonVersion@0 inputs: versionSpec: '3.9' displayName: 'Use Python' - script: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . for wheel in $(python setup.py --list-wheels); do PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir $(Build.ArtifactStagingDirectory)/esrp-build done displayName: 'Install & Build' - job: Publish dependsOn: Build templateContext: type: releaseJob isProduction: true inputs: - input: pipelineArtifact artifactName: esrp-build targetPath: $(Build.ArtifactStagingDirectory)/esrp-build steps: - checkout: none - task: EsrpRelease@9 inputs: connectedservicename: 'Playwright-ESRP-PME' usemanagedidentity: true keyvaultname: 'playwright-esrp-pme' signcertname: 'ESRP-Release-Sign' clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5' intent: 'PackageDistribution' contenttype: 'PyPi' # Keeping it commented out as a workaround for: # https://portal.microsofticm.com/imp/v3/incidents/incident/499972482/summary # contentsource: 'folder' folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build' waitforreleasecompletion: true owners: 'yurys@microsoft.com' approvers: 'yurys@microsoft.com' serviceendpointurl: 'https://api.esrp.microsoft.com' mainpublisher: 'Playwright' domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346' displayName: 'ESRP Release to PIP' ================================================ FILE: .claude/skills/playwright-roll/SKILL.md ================================================ --- name: playwright-roll description: Roll Playwright Python to a new version --- Help the user roll to a new version of Playwright. ../../../ROLLING.md contains general instructions and scripts. Start with updating the version and generating the API to see the state of things. Afterwards, work through the list of changes that need to be backported. You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes". Work through them one-by-one and check off the items that you have handled. Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch. Rolling includes: - updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client) - adding a couple of new tests to verify new/changed functionality ## Tips & Tricks - Project checkouts are in the parent directory (`../`). - when updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file - use the "gh" cli to interact with GitHub ================================================ FILE: .gitattributes ================================================ # text files must be lf for golden file tests to work * text=auto eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report 🪲 description: Create a bug report to help us improve title: '[Bug]: ' body: - type: markdown attributes: value: | # Please follow these steps first: - type: markdown attributes: value: | ## Troubleshoot If Playwright is not behaving the way you expect, we'd ask you to look at the [documentation](https://playwright.dev/python/docs/intro) and search the issue tracker for evidence supporting your expectation. Please make reasonable efforts to troubleshoot and rule out issues with your code, the configuration, or any 3rd party libraries you might be using. Playwright offers [several debugging tools](https://playwright.dev/python/docs/debug) that you can use to troubleshoot your issues. - type: markdown attributes: value: | ## Ask for help through appropriate channels If you feel unsure about the cause of the problem, consider asking for help on for example [StackOverflow](https://stackoverflow.com/questions/ask) or our [Discord channel](https://aka.ms/playwright/discord) before posting a bug report. The issue tracker is not a help forum. - type: markdown attributes: value: | ## Make a minimal reproduction To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug. The simpler you can make it, the more likely we are to successfully verify and fix the bug. - type: markdown attributes: value: | > [!IMPORTANT] > Bug reports without a minimal reproduction will be rejected. --- - type: input id: version attributes: label: Version description: | The version of Playwright you are using. Is it the [latest](https://github.com/microsoft/playwright-python/releases)? Test and see if the bug has already been fixed. placeholder: ex. 1.41.1 validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: Please link to a repository with a minimal reproduction and describe accurately how we can reproduce/verify the bug. value: | Example steps (replace with your own): 1. Clone my repo at https://github.com//example 2. pip install -r requirements.txt 3. python test.py 4. You should see the error come up validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: A description of what you expect to happen. placeholder: I expect to see X or Y validations: required: true - type: textarea id: what-happened attributes: label: Actual behavior description: | A clear and concise description of the unexpected behavior. Please include any relevant output here, especially any error messages. placeholder: A bug happened! validations: required: true - type: textarea id: context attributes: label: Additional context description: Anything else that might be relevant validations: required: false - type: textarea id: envinfo attributes: label: Environment description: | Please provide information about the environment you are running in. value: | - Operating System: [Ubuntu 22.04] - CPU: [arm64] - Browser: [All, Chromium, Firefox, WebKit] - Python Version: [3.12] - Other info: render: Text validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Join our Discord Server url: https://aka.ms/playwright/discord about: Ask questions and discuss with other community members ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.yml ================================================ name: Documentation 📖 description: Submit a request to add or update documentation title: '[Docs]: ' labels: ['Documentation :book:'] body: - type: markdown attributes: value: | ### Thank you for helping us improve our documentation! 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. - type: textarea id: links attributes: label: Page(s) description: | Links to one or more documentation pages that should be modified. If you are reporting an issue with a specific section of a page, try to link directly to the nearest anchor. If you are suggesting that a new page be created, link to the parent of the proposed page. validations: required: true - type: textarea id: description attributes: label: Description description: | Describe the change you are requesting. If the issue pertains to a single function or matcher, be sure to specify the entire call signature. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature Request 🚀 description: Submit a proposal for a new feature title: '[Feature]: ' body: - type: markdown attributes: value: | ### Thank you for taking the time to suggest a new feature! - type: textarea id: description attributes: label: '🚀 Feature Request' description: A clear and concise description of what the feature is. validations: required: true - type: textarea id: example attributes: label: Example description: Describe how this feature would be used. validations: required: false - type: textarea id: motivation attributes: label: Motivation description: | Outline your motivation for the proposal. How will it make Playwright better? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/question.yml ================================================ name: 'Questions / Help 💬' description: If you have questions, please check StackOverflow or Discord title: '[Please read the message below]' labels: [':speech_balloon: Question'] body: - type: markdown attributes: value: | ## Questions and Help 💬 This issue tracker is reserved for bug reports and feature requests. For anything else, such as questions or getting help, please see: - [The Playwright documentation](https://playwright.dev) - [Our Discord server](https://aka.ms/playwright/discord) - type: checkboxes id: no-post attributes: label: | Please do not submit this issue. description: | > [!IMPORTANT] > This issue will be closed. options: - label: I understand required: true ================================================ FILE: .github/ISSUE_TEMPLATE/regression.yml ================================================ name: Report regression description: Functionality that used to work and does not any more title: "[Regression]: " body: - type: markdown attributes: value: | # Please follow these steps first: - type: markdown attributes: value: | ## Make a minimal reproduction To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the regression. The simpler you can make it, the more likely we are to successfully verify and fix the regression. - type: markdown attributes: value: | > [!IMPORTANT] > Regression reports without a minimal reproduction will be rejected. --- - type: input id: goodVersion attributes: label: Last Good Version description: | Last version of Playwright where the feature was working. placeholder: ex. 1.40.1 validations: required: true - type: input id: badVersion attributes: label: First Bad Version description: | First version of Playwright where the feature was broken. Is it the [latest](https://github.com/microsoft/playwright-python/releases)? Test and see if the regression has already been fixed. placeholder: ex. 1.41.1 validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: Please link to a repository with a minimal reproduction and describe accurately how we can reproduce/verify the bug. value: | Example steps (replace with your own): 1. Clone my repo at https://github.com//example 2. pip -r requirements.txt 3. python test.py 4. You should see the error come up validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: A description of what you expect to happen. placeholder: I expect to see X or Y validations: required: true - type: textarea id: what-happened attributes: label: Actual behavior description: A clear and concise description of the unexpected behavior. placeholder: A bug happened! validations: required: true - type: textarea id: context attributes: label: Additional context description: Anything else that might be relevant validations: required: false - type: textarea id: envinfo attributes: label: Environment description: | Please provide information about the environment you are running in. value: | - Operating System: [Ubuntu 22.04] - CPU: [arm64] - Browser: [All, Chromium, Firefox, WebKit] - Python Version: [3.12] - Other info: render: Text validations: required: true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions: patterns: - "*" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main - release-* pull_request: branches: - main - release-* concurrency: # For pull requests, cancel all currently-running jobs for this workflow # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: infra: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers run: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . python -m build --wheel python -m playwright install --with-deps - name: Lint run: pre-commit run --show-diff-on-failure --color=always --all-files - name: Generate APIs run: bash scripts/update_api.sh - name: Verify generated API is up to date run: git diff --exit-code build: name: Build timeout-minutes: 45 strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.9', '3.10'] browser: [chromium, firefox, webkit] include: - os: windows-latest python-version: '3.11' browser: chromium - os: macos-latest python-version: '3.11' browser: chromium - os: ubuntu-latest python-version: '3.11' browser: chromium - os: windows-latest python-version: '3.12' browser: chromium - os: macos-latest python-version: '3.12' browser: chromium - os: ubuntu-latest python-version: '3.12' browser: chromium - os: windows-latest python-version: '3.13' browser: chromium - os: macos-latest python-version: '3.13' browser: chromium - os: ubuntu-latest python-version: '3.13' browser: chromium runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies & browsers run: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . python -m build --wheel python -m playwright install --with-deps ${{ matrix.browser }} - name: Common Tests run: pytest tests/common --browser=${{ matrix.browser }} --timeout 90 - name: Test Reference count run: pytest tests/test_reference_count_async.py --browser=${{ matrix.browser }} - name: Test Wheel Installation run: pytest tests/test_installation.py --browser=${{ matrix.browser }} - name: Test Sync API if: matrix.os != 'ubuntu-latest' run: pytest tests/sync --browser=${{ matrix.browser }} --timeout 90 - name: Test Sync API if: matrix.os == 'ubuntu-latest' run: xvfb-run pytest tests/sync --browser=${{ matrix.browser }} --timeout 90 - name: Test Async API if: matrix.os != 'ubuntu-latest' run: pytest tests/async --browser=${{ matrix.browser }} --timeout 90 - name: Test Async API if: matrix.os == 'ubuntu-latest' run: xvfb-run pytest tests/async --browser=${{ matrix.browser }} --timeout 90 test-stable: name: Stable timeout-minutes: 45 strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] browser-channel: [chrome] include: - os: windows-latest browser-channel: msedge - os: macos-latest browser-channel: msedge runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers run: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . python -m build --wheel python -m playwright install ${{ matrix.browser-channel }} --with-deps - name: Common Tests run: pytest tests/common --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 - name: Test Sync API if: matrix.os != 'ubuntu-latest' run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 - name: Test Sync API if: matrix.os == 'ubuntu-latest' run: xvfb-run pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 - name: Test Async API if: matrix.os != 'ubuntu-latest' run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 - name: Test Async API if: matrix.os == 'ubuntu-latest' run: xvfb-run pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 build-conda: name: Conda Build strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-13, windows-2022] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda uses: conda-incubator/setup-miniconda@v3 with: python-version: 3.9 channels: conda-forge miniconda-version: latest - name: Prepare run: conda install conda-build conda-verify - name: Build run: conda build . test_examples: name: Examples runs-on: ubuntu-22.04 defaults: run: working-directory: examples/todomvc/ steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install dependencies & browsers run: | pip install -r requirements.txt python -m playwright install --with-deps chromium - name: Common Tests run: pytest ================================================ FILE: .github/workflows/publish.yml ================================================ name: Upload Python Package on: release: types: [published] workflow_dispatch: jobs: deploy-conda: strategy: fail-fast: false matrix: include: - os: ubuntu-latest target-platform: linux-x86_64 - os: ubuntu-latest target-platform: linux-aarch64 - os: windows-latest target-platform: win-64 - os: macos-latest-large target-platform: osx-intel - os: macos-latest-xlarge target-platform: osx-arm64 runs-on: ${{ matrix.os }} defaults: run: # Required for conda-incubator/setup-miniconda@v3 shell: bash -el {0} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda uses: conda-incubator/setup-miniconda@v3 with: python-version: 3.9 channels: conda-forge miniconda-version: latest - name: Prepare run: conda install anaconda-client conda-build conda-verify - name: Build and Upload env: ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }} run: | conda config --set anaconda_upload yes if [ "${{ matrix.target-platform }}" == "osx-arm64" ]; then conda build --user microsoft . -m conda_build_config_osx_arm64.yaml elif [ "${{ matrix.target-platform }}" == "linux-aarch64" ]; then conda build --user microsoft . -m conda_build_config_linux_aarch64.yaml else conda build --user microsoft . fi ================================================ FILE: .github/workflows/publish_docker.yml ================================================ name: "publish release - Docker" on: workflow_dispatch: release: types: [published] jobs: publish-docker-release: name: "publish to DockerHub" runs-on: ubuntu-22.04 if: github.repository == 'microsoft/playwright-python' permissions: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed environment: Docker steps: - uses: actions/checkout@v5 - name: Azure login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }} subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }} - name: Login to ACR via OIDC run: az acr login --name playwright - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Set up Docker QEMU for arm64 docker builds uses: docker/setup-qemu-action@v3 with: platforms: arm64 - name: Install dependencies & browsers run: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . - run: ./utils/docker/publish_docker.sh stable ================================================ FILE: .github/workflows/test_docker.yml ================================================ name: Test Docker on: push: paths: - '.github/workflows/test_docker.yml' - 'setup.py' - '**/Dockerfile.*' branches: - main - release-* pull_request: paths: - '.github/workflows/test_docker.yml' - 'setup.py' - '**/Dockerfile.*' branches: - main - release-* jobs: build: timeout-minutes: 120 runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false matrix: docker-image-variant: - jammy - noble runs-on: - ubuntu-24.04 - ubuntu-24.04-arm steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r local-requirements.txt pip install -r requirements.txt pip install -e . - name: Build Docker image run: | ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}" bash utils/docker/build.sh --$ARCH ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} - name: Test run: | CONTAINER_ID="$(docker run --rm -e CI -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" # Fix permissions for Git inside the container docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt docker exec "${CONTAINER_ID}" pip install -r requirements.txt docker exec "${CONTAINER_ID}" pip install -e . docker exec "${CONTAINER_ID}" python -m build --wheel docker exec "${CONTAINER_ID}" xvfb-run pytest tests/sync/ docker exec "${CONTAINER_ID}" xvfb-run pytest tests/async/ ================================================ FILE: .gitignore ================================================ **/__pycache__/ driver/ playwright/driver/ playwright.egg-info/ build/ dist/ venv/ .idea/ **/*.pyc env/ htmlcov/ .coverage* .DS_Store .vscode/ .eggs _repo_version.py coverage.xml junit/ htmldocs/ utils/docker/dist/ Pipfile Pipfile.lock .venv/ ================================================ FILE: .pre-commit-config.yaml ================================================ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer exclude: tests/assets/har-sha1-main-response.txt - id: check-yaml - id: check-toml - id: requirements-txt-fixer - id: check-ast - id: check-builtin-literals - id: check-executables-have-shebangs - id: check-merge-conflict - repo: https://github.com/psf/black rev: 25.1.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.0 hooks: - id: mypy additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.4.20250611] - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort rev: 6.0.1 hooks: - id: isort - repo: local hooks: - id: pyright name: pyright entry: pyright language: node pass_filenames: false types: [python] additional_dependencies: ["pyright@1.1.403"] - repo: local hooks: - id: check-license-header name: Check License Header entry: ./utils/linting/check_file_header.py language: python types: [python] ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## How to Contribute ### Configuring python environment The project development requires Python version 3.9+. To set it as default in the environment run the following commands: ```sh # You may need to install python 3.9 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.9-venv` python3.9 -m venv env source ./env/bin/activate ``` Install required dependencies: ```sh python -m pip install --upgrade pip pip install -r local-requirements.txt ``` Build and install drivers: ```sh pip install -e . python -m build --wheel ``` Run tests: ```sh pytest --browser chromium ``` Checking for typing errors ```sh mypy playwright ``` Format the code ```sh pre-commit install pre-commit run --all-files ``` For more details look at the [CI configuration](./.github/workflows/ci.yml). Collect coverage ```sh pytest --browser chromium --cov-report html --cov=playwright open htmlcov/index.html ``` ### Regenerating APIs ```bash ./scripts/update_api.sh pre-commit run --all-files ``` ## Contributor License Agreement This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Portions Copyright (c) Microsoft Corporation. Portions Copyright 2017 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # 🎭 [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) 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). | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | | Chromium 145.0.7632.6 | ✅ | ✅ | ✅ | | WebKit 26.0 | ✅ | ✅ | ✅ | | Firefox 146.0.1 | ✅ | ✅ | ✅ | ## Documentation [https://playwright.dev/python/docs/intro](https://playwright.dev/python/docs/intro) ## API Reference [https://playwright.dev/python/docs/api/class-playwright](https://playwright.dev/python/docs/api/class-playwright) ## Example ```py from playwright.sync_api import sync_playwright with sync_playwright() as p: for browser_type in [p.chromium, p.firefox, p.webkit]: browser = browser_type.launch() page = browser.new_page() page.goto('http://playwright.dev') page.screenshot(path=f'example-{browser_type.name}.png') browser.close() ``` ```py import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: for browser_type in [p.chromium, p.firefox, p.webkit]: browser = await browser_type.launch() page = await browser.new_page() await page.goto('http://playwright.dev') await page.screenshot(path=f'example-{browser_type.name}.png') await browser.close() asyncio.run(main()) ``` ## Other languages More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in - [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro), - [.NET](https://playwright.dev/dotnet/docs/intro), - [Java](https://playwright.dev/java/docs/intro). ================================================ FILE: ROLLING.md ================================================ # Rolling Playwright-Python to the latest Playwright driver * checkout repo: `git clone https://github.com/microsoft/playwright-python` * make sure local python is 3.9 * create virtual environment, if don't have one: `python -m venv env` * activate venv: `source env/bin/activate` * install all deps: ``` python -m pip install --upgrade pip pip install -r local-requirements.txt pre-commit install pip install -e . ``` * change driver version in `setup.py` * download new driver: `python -m build --wheel` * generate API: `./scripts/update_api.sh` * commit changes & send PR * wait for bots to pass & merge the PR ## Fix typing issues with Playwright ToT 1. `cd playwright` 1. `API_JSON_MODE=1 node utils/doclint/generateApiJson.js > ../playwright-python/playwright/driver/package/api.json` 1. `./scripts/update_api.sh` ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). ================================================ FILE: SUPPORT.md ================================================ # Support ## How to file issues and get help 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. For help and questions about using this project, please see the [docs site for Playwright for Python][docs]. Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum. ## Microsoft Support Policy Support for Playwright for Python is limited to the resources listed above. [gh-issues]: https://github.com/microsoft/playwright-python/issues/ [docs]: https://playwright.dev/python/ [discord-server]: https://aka.ms/playwright/discord ================================================ FILE: conda_build_config_linux_aarch64.yaml ================================================ target_platform: - linux-aarch64 ================================================ FILE: conda_build_config_osx_arm64.yaml ================================================ target_platform: - osx-arm64 ================================================ FILE: examples/todomvc/mvctests/__init__.py ================================================ ================================================ FILE: examples/todomvc/mvctests/test_clear_completed_button.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import TODO_ITEMS, create_default_todos @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") create_default_todos(page) # run the actual test yield # run any cleanup code def test_should_display_the_correct_text(page: Page) -> None: page.locator(".todo-list li .toggle").first.check() expect(page.locator(".clear-completed")).to_have_text("Clear completed") def test_should_clear_completed_items_when_clicked(page: Page) -> None: todo_items = page.locator(".todo-list li") todo_items.nth(1).locator(".toggle").check() page.locator(".clear-completed").click() expect(todo_items).to_have_count(2) expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[2]]) def test_should_be_hidden_when_there_are_no_items_that_are_completed( page: Page, ) -> None: page.locator(".todo-list li .toggle").first.check() page.locator(".clear-completed").click() expect(page.locator(".clear-completed")).to_be_hidden() ================================================ FILE: examples/todomvc/mvctests/test_counter.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import TODO_ITEMS, assert_number_of_todos_in_local_storage @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") # run the actual test yield # run any cleanup code def test_should_display_the_current_number_of_todo_items(page: Page) -> None: page.locator(".new-todo").fill(TODO_ITEMS[0]) page.locator(".new-todo").press("Enter") expect(page.locator(".todo-count")).to_contain_text("1") page.locator(".new-todo").fill(TODO_ITEMS[1]) page.locator(".new-todo").press("Enter") expect(page.locator(".todo-count")).to_contain_text("2") assert_number_of_todos_in_local_storage(page, 2) ================================================ FILE: examples/todomvc/mvctests/test_editing.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import ( TODO_ITEMS, assert_number_of_todos_in_local_storage, check_todos_in_local_storage, create_default_todos, ) @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") create_default_todos(page) assert_number_of_todos_in_local_storage(page, 3) # run the actual test yield # run any cleanup code def test_should_hide_other_controls_when_editing(page: Page) -> None: todo_item = page.locator(".todo-list li").nth(1) todo_item.dblclick() expect(todo_item.locator(".toggle")).not_to_be_visible() expect(todo_item.locator("label")).not_to_be_visible() assert_number_of_todos_in_local_storage(page, 3) def test_should_save_edits_on_blur(page: Page) -> None: todo_items = page.locator(".todo-list li") todo_items.nth(1).dblclick() todo_items.nth(1).locator(".edit").fill("buy some sausages") todo_items.nth(1).locator(".edit").dispatch_event("blur") expect(todo_items).to_have_text( [ TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2], ] ) check_todos_in_local_storage(page, "buy some sausages") def test_should_trim_entered_text(page: Page) -> None: todo_items = page.locator(".todo-list li") todo_items.nth(1).dblclick() todo_items.nth(1).locator(".edit").fill(" buy some sausages ") todo_items.nth(1).locator(".edit").press("Enter") expect(todo_items).to_have_text( [ TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2], ] ) check_todos_in_local_storage(page, "buy some sausages") def test_should_remove_the_item_if_an_empty_text_string_was_entered(page: Page) -> None: todo_items = page.locator(".todo-list li") todo_items.nth(1).dblclick() todo_items.nth(1).locator(".edit").fill("") todo_items.nth(1).locator(".edit").press("Enter") expect(todo_items).to_have_text( [ TODO_ITEMS[0], TODO_ITEMS[2], ] ) def test_should_cancel_edits_on_escape(page: Page) -> None: todo_items = page.locator(".todo-list li") todo_items.nth(1).dblclick() todo_items.nth(1).locator(".edit").press("Escape") expect(todo_items).to_have_text(TODO_ITEMS) ================================================ FILE: examples/todomvc/mvctests/test_item.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import ( TODO_ITEMS, check_number_of_completed_todos_in_local_storage, check_todos_in_local_storage, create_default_todos, ) @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") # run the actual test yield # run any cleanup code def test_should_allow_me_to_mark_items_as_completed(page: Page) -> None: # Create two items. for item in TODO_ITEMS[:2]: page.locator(".new-todo").fill(item) page.locator(".new-todo").press("Enter") # Check first item. firstTodo = page.locator(".todo-list li").nth(0) firstTodo.locator(".toggle").check() expect(firstTodo).to_have_class("completed") # Check second item. secondTodo = page.locator(".todo-list li").nth(1) expect(secondTodo).not_to_have_class("completed") secondTodo.locator(".toggle").check() # Assert completed class. expect(firstTodo).to_have_class("completed") expect(secondTodo).to_have_class("completed") def test_should_allow_me_to_un_mark_items_as_completed(page: Page) -> None: # Create two items. for item in TODO_ITEMS[:2]: page.locator(".new-todo").fill(item) page.locator(".new-todo").press("Enter") firstTodo = page.locator(".todo-list li").nth(0) secondTodo = page.locator(".todo-list li").nth(1) firstTodo.locator(".toggle").check() expect(firstTodo).to_have_class("completed") expect(secondTodo).not_to_have_class("completed") check_number_of_completed_todos_in_local_storage(page, 1) firstTodo.locator(".toggle").uncheck() expect(firstTodo).not_to_have_class("completed") expect(secondTodo).not_to_have_class("completed") check_number_of_completed_todos_in_local_storage(page, 0) def test_should_allow_me_to_edit_an_item(page: Page) -> None: create_default_todos(page) todo_items = page.locator(".todo-list li") secondTodo = todo_items.nth(1) secondTodo.dblclick() expect(secondTodo.locator(".edit")).to_have_value(TODO_ITEMS[1]) secondTodo.locator(".edit").fill("buy some sausages") secondTodo.locator(".edit").press("Enter") # Explicitly assert the new text value. expect(todo_items).to_have_text([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]) check_todos_in_local_storage(page, "buy some sausages") ================================================ FILE: examples/todomvc/mvctests/test_mark_all_as_completed.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import ( assert_number_of_todos_in_local_storage, check_number_of_completed_todos_in_local_storage, create_default_todos, ) @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") # run the actual test yield # run any cleanup code def test_should_allow_me_to_mark_all_items_as_completed(page: Page) -> None: create_default_todos(page) assert_number_of_todos_in_local_storage(page, 3) # Complete all todos. page.locator(".toggle-all").check() # Ensure all todos have 'completed' class. expect(page.locator(".todo-list li")).to_have_class( ["completed", "completed", "completed"] ) check_number_of_completed_todos_in_local_storage(page, 3) assert_number_of_todos_in_local_storage(page, 3) def test_should_allow_me_to_clear_the_complete_state_of_all_items(page: Page) -> None: create_default_todos(page) assert_number_of_todos_in_local_storage(page, 3) # Check and then immediately uncheck. page.locator(".toggle-all").check() page.locator(".toggle-all").uncheck() # Should be no completed classes. expect(page.locator(".todo-list li")).to_have_class(["", "", ""]) assert_number_of_todos_in_local_storage(page, 3) def test_complete_all_checkbox_should_update_state_when_items_are_completed_or_cleared( page: Page, ) -> None: create_default_todos(page) assert_number_of_todos_in_local_storage(page, 3) toggleAll = page.locator(".toggle-all") toggleAll.check() expect(toggleAll).to_be_checked() check_number_of_completed_todos_in_local_storage(page, 3) # Uncheck first todo. firstTodo = page.locator(".todo-list li").nth(0) firstTodo.locator(".toggle").uncheck() # Reuse toggleAll locator and make sure its not checked. expect(toggleAll).not_to_be_checked() firstTodo.locator(".toggle").check() check_number_of_completed_todos_in_local_storage(page, 3) # Assert the toggle all is checked again. expect(toggleAll).to_be_checked() assert_number_of_todos_in_local_storage(page, 3) ================================================ FILE: examples/todomvc/mvctests/test_new_todo.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import ( TODO_ITEMS, assert_number_of_todos_in_local_storage, create_default_todos, ) @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") # run the actual test yield # run any cleanup code def test_new_todo_test_should_allow_me_to_add_todo_items(page: Page) -> None: # Create 1st todo. page.locator(".new-todo").fill(TODO_ITEMS[0]) page.locator(".new-todo").press("Enter") # Make sure the list only has one todo item. expect(page.locator(".view label")).to_have_text([TODO_ITEMS[0]]) # Create 2nd todo. page.locator(".new-todo").fill(TODO_ITEMS[1]) page.locator(".new-todo").press("Enter") # Make sure the list now has two todo items. expect(page.locator(".view label")).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) assert_number_of_todos_in_local_storage(page, 2) def test_new_todo_test_should_clear_text_input_field_when_an_item_is_added( page: Page, ) -> None: # Create one todo item. page.locator(".new-todo").fill(TODO_ITEMS[0]) page.locator(".new-todo").press("Enter") # Check that input is empty. expect(page.locator(".new-todo")).to_be_empty() assert_number_of_todos_in_local_storage(page, 1) def test_new_todo_test_should_append_new_items_to_the_bottom_of_the_list( page: Page, ) -> None: # Create 3 items. create_default_todos(page) # Check test using different methods. expect(page.locator(".todo-count")).to_have_text("3 items left") expect(page.locator(".todo-count")).to_contain_text("3") expect(page.locator(".todo-count")).to_have_text(re.compile("3")) # Check all items in one call. expect(page.locator(".view label")).to_have_text(TODO_ITEMS) assert_number_of_todos_in_local_storage(page, 3) def test_new_todo_should_show_main_and_foter_when_items_added(page: Page) -> None: page.locator(".new-todo").fill(TODO_ITEMS[0]) page.locator(".new-todo").press("Enter") expect(page.locator(".main")).to_be_visible() expect(page.locator(".footer")).to_be_visible() assert_number_of_todos_in_local_storage(page, 1) ================================================ FILE: examples/todomvc/mvctests/test_persistence.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import TODO_ITEMS, check_number_of_completed_todos_in_local_storage @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") # run the actual test yield # run any cleanup code def test_should_persist_its_data(page: Page) -> None: for item in TODO_ITEMS[:2]: page.locator(".new-todo").fill(item) page.locator(".new-todo").press("Enter") todo_items = page.locator(".todo-list li") todo_items.nth(0).locator(".toggle").check() expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) expect(todo_items).to_have_class(["completed", ""]) # Ensure there is 1 completed item. check_number_of_completed_todos_in_local_storage(page, 1) # Now reload. page.reload() expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) expect(todo_items).to_have_class(["completed", ""]) ================================================ FILE: examples/todomvc/mvctests/test_routing.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generator import pytest from playwright.sync_api import Page, expect from .utils import ( TODO_ITEMS, check_number_of_completed_todos_in_local_storage, check_todos_in_local_storage, create_default_todos, ) @pytest.fixture(autouse=True) def run_around_tests(page: Page) -> Generator[None, None, None]: # setup before a test page.goto("https://demo.playwright.dev/todomvc") create_default_todos(page) # make sure the app had a chance to save updated todos in storage # before navigating to a new view, otherwise the items can get lost :( # in some frameworks like Durandal check_todos_in_local_storage(page, TODO_ITEMS[0]) # run the actual test yield # run any cleanup code def test_should_allow_me_to_display_active_item(page: Page) -> None: page.locator(".todo-list li .toggle").nth(1).check() check_number_of_completed_todos_in_local_storage(page, 1) page.locator(".filters >> text=Active").click() expect(page.locator(".todo-list li")).to_have_count(2) expect(page.locator(".todo-list li")).to_have_text([TODO_ITEMS[0], TODO_ITEMS[2]]) def test_should_respect_the_back_button(page: Page) -> None: page.locator(".todo-list li .toggle").nth(1).check() check_number_of_completed_todos_in_local_storage(page, 1) # Showing all items page.locator(".filters >> text=All").click() expect(page.locator(".todo-list li")).to_have_count(3) # Showing active items page.locator(".filters >> text=Active").click() # Showing completed items page.locator(".filters >> text=Completed").click() expect(page.locator(".todo-list li")).to_have_count(1) page.go_back() expect(page.locator(".todo-list li")).to_have_count(2) page.go_back() expect(page.locator(".todo-list li")).to_have_count(3) def test_should_allow_me_to_display_completed_items(page: Page) -> None: page.locator(".todo-list li .toggle").nth(1).check() check_number_of_completed_todos_in_local_storage(page, 1) page.locator(".filters >> text=Completed").click() expect(page.locator(".todo-list li")).to_have_count(1) def test_should_allow_me_to_display_all_items(page: Page) -> None: page.locator(".todo-list li .toggle").nth(1).check() check_number_of_completed_todos_in_local_storage(page, 1) page.locator(".filters >> text=Active").click() page.locator(".filters >> text=Completed").click() page.locator(".filters >> text=All").click() expect(page.locator(".todo-list li")).to_have_count(3) def test_should_highlight_the_current_applied_filter(page: Page) -> None: expect(page.locator(".filters >> text=All")).to_have_class("selected") page.locator(".filters >> text=Active").click() # Page change - active items. expect(page.locator(".filters >> text=Active")).to_have_class("selected") page.locator(".filters >> text=Completed").click() # Page change - completed items. expect(page.locator(".filters >> text=Completed")).to_have_class("selected") ================================================ FILE: examples/todomvc/mvctests/utils.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from playwright.sync_api import Page TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] def create_default_todos(page: Page) -> None: for item in TODO_ITEMS: page.locator(".new-todo").fill(item) page.locator(".new-todo").press("Enter") def check_number_of_completed_todos_in_local_storage(page: Page, expected: int) -> None: assert ( page.evaluate( "JSON.parse(localStorage['react-todos']).filter(i => i.completed).length" ) == expected ) def assert_number_of_todos_in_local_storage(page: Page, expected: int) -> None: assert len(page.evaluate("JSON.parse(localStorage['react-todos'])")) == expected def check_todos_in_local_storage(page: Page, title: str) -> None: assert title in page.evaluate( "JSON.parse(localStorage['react-todos']).map(i => i.title)" ) ================================================ FILE: examples/todomvc/requirements.txt ================================================ pytest-playwright ================================================ FILE: local-requirements.txt ================================================ autobahn==23.1.2 black==25.1.0 build==1.3.0 flake8==7.2.0 mypy==1.17.1 objgraph==3.6.2 Pillow==11.3.0 pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 pytest==8.4.1 pytest-asyncio==1.1.0 pytest-cov==6.3.0 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 requests==2.32.5 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 types-requests==2.32.4.20250809 ================================================ FILE: meta.yaml ================================================ package: name: playwright version: "{{ environ.get('GIT_DESCRIBE_TAG') | replace('v', '') }}" source: path: . build: number: 0 script: "{{ PYTHON }} -m pip install . --no-deps -vv" binary_relocation: False missing_dso_whitelist: "*" entry_points: - playwright = playwright.__main__:main requirements: build: - python >=3.9 # [build_platform != target_platform] - pip # [build_platform != target_platform] - cross-python_{{ target_platform }} # [build_platform != target_platform] host: - python >=3.9 - wheel - pip - curl - setuptools_scm run: - python >=3.9 # This should be the same as the dependencies in pyproject.toml - greenlet>=3.1.1,<4.0.0 - pyee>=13,<14 test: # [build_platform == target_platform] files: - scripts/example_sync.py - scripts/example_async.py requires: - pip imports: - playwright - playwright.sync_api - playwright.async_api commands: - playwright --help - playwright install --with-deps - python scripts/example_sync.py - python scripts/example_async.py about: home: https://github.com/microsoft/playwright-python license: Apache-2.0 license_family: Apache license_file: LICENSE summary: Python version of the Playwright testing and automation library. description: | Playwright is a Python library to automate Chromium, Firefox and WebKit browsers with a single API. Playwright delivers automation that is ever-green, capable, reliable and fast. doc_url: https://playwright.dev/python/docs/intro/ dev_url: https://github.com/microsoft/playwright-python ================================================ FILE: playwright/__init__.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Python package `playwright` is a Python library to automate Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast. """ ================================================ FILE: playwright/__main__.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import subprocess import sys from playwright._impl._driver import compute_driver_executable, get_driver_env def main() -> None: try: driver_executable, driver_cli = compute_driver_executable() completed_process = subprocess.run( [driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env() ) sys.exit(completed_process.returncode) except KeyboardInterrupt: sys.exit(130) if __name__ == "__main__": main() ================================================ FILE: playwright/_impl/__init__.py ================================================ ================================================ FILE: playwright/_impl/__pyinstaller/__init__.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from typing import List def get_hook_dirs() -> List[str]: return [os.path.dirname(__file__)] ================================================ FILE: playwright/_impl/__pyinstaller/hook-playwright.async_api.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from PyInstaller.utils.hooks import collect_data_files # type: ignore datas = collect_data_files("playwright") ================================================ FILE: playwright/_impl/__pyinstaller/hook-playwright.sync_api.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from PyInstaller.utils.hooks import collect_data_files # type: ignore datas = collect_data_files("playwright") ================================================ FILE: playwright/_impl/_api_structures.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union # These are the structures that we like keeping in a JSON form for their potential # reuse between SDKs / services. They are public and are a part of the # stable API. # Explicitly mark optional params as such for the documentation # If there is at least one optional param, set total=False for better mypy handling. class Cookie(TypedDict, total=False): name: str value: str domain: str path: str expires: float httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] partitionKey: Optional[str] class StorageStateCookie(TypedDict, total=False): name: str value: str domain: str path: str expires: float httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] # TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie. class SetCookieParam(TypedDict, total=False): name: str value: str url: Optional[str] domain: Optional[str] path: Optional[str] expires: Optional[float] httpOnly: Optional[bool] secure: Optional[bool] sameSite: Optional[Literal["Lax", "None", "Strict"]] partitionKey: Optional[str] class FloatRect(TypedDict): x: float y: float width: float height: float class Geolocation(TypedDict, total=False): latitude: float longitude: float accuracy: Optional[float] class HttpCredentials(TypedDict, total=False): username: str password: str origin: Optional[str] send: Optional[Literal["always", "unauthorized"]] class LocalStorageEntry(TypedDict): name: str value: str class OriginState(TypedDict): origin: str localStorage: List[LocalStorageEntry] class PdfMargins(TypedDict, total=False): top: Optional[Union[str, float]] right: Optional[Union[str, float]] bottom: Optional[Union[str, float]] left: Optional[Union[str, float]] class Position(TypedDict): x: float y: float class ProxySettings(TypedDict, total=False): server: str bypass: Optional[str] username: Optional[str] password: Optional[str] class StorageState(TypedDict, total=False): cookies: List[StorageStateCookie] origins: List[OriginState] class ClientCertificate(TypedDict, total=False): origin: str certPath: Optional[Union[str, Path]] cert: Optional[bytes] keyPath: Optional[Union[str, Path]] key: Optional[bytes] pfxPath: Optional[Union[str, Path]] pfx: Optional[bytes] passphrase: Optional[str] class ResourceTiming(TypedDict): startTime: float domainLookupStart: float domainLookupEnd: float connectStart: float secureConnectionStart: float connectEnd: float requestStart: float responseStart: float responseEnd: float class RequestSizes(TypedDict): requestBodySize: int requestHeadersSize: int responseBodySize: int responseHeadersSize: int class ViewportSize(TypedDict): width: int height: int class SourceLocation(TypedDict): url: str lineNumber: int columnNumber: int class FilePayload(TypedDict): name: str mimeType: str buffer: bytes class RemoteAddr(TypedDict): ipAddress: str port: int class SecurityDetails(TypedDict): issuer: Optional[str] protocol: Optional[str] subjectName: Optional[str] validFrom: Optional[float] validTo: Optional[float] class NameValue(TypedDict): name: str value: str HeadersArray = List[NameValue] Headers = Dict[str, str] class ServerFilePayload(TypedDict): name: str mimeType: str buffer: str class FormField(TypedDict, total=False): name: str value: Optional[str] file: Optional[ServerFilePayload] class ExpectedTextValue(TypedDict, total=False): string: str regexSource: str regexFlags: str matchSubstring: bool normalizeWhiteSpace: bool ignoreCase: Optional[bool] class FrameExpectOptions(TypedDict, total=False): expressionArg: Any expectedText: Optional[Sequence[ExpectedTextValue]] expectedNumber: Optional[float] expectedValue: Optional[Any] useInnerText: Optional[bool] isNot: bool timeout: Optional[float] class FrameExpectResult(TypedDict): matches: bool received: Any log: List[str] errorMessage: Optional[str] AriaRole = Literal[ "alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "directory", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "meter", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem", ] class TracingGroupLocation(TypedDict): file: str line: Optional[int] column: Optional[int] ================================================ FILE: playwright/_impl/_artifact.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib from pathlib import Path from typing import Dict, Optional, Union, cast from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message from playwright._impl._stream import Stream class Artifact(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self.absolute_path = initializer["absolutePath"] async def path_after_finished(self) -> pathlib.Path: if self._connection.is_remote: raise Error( "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." ) path = await self._channel.send( "pathAfterFinished", None, ) return pathlib.Path(path) async def save_as(self, path: Union[str, Path]) -> None: stream = cast( Stream, from_channel( await self._channel.send( "saveAsStream", None, ) ), ) make_dirs_for_file(path) await stream.save_as(path) async def failure(self) -> Optional[str]: reason = await self._channel.send( "failure", None, ) if reason is None: return None return patch_error_message(reason) async def delete(self) -> None: await self._channel.send( "delete", None, ) async def read_info_buffer(self) -> bytes: stream = cast( Stream, from_channel( await self._channel.send( "stream", None, ) ), ) buffer = await stream.read_all() return buffer async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride] await self._channel.send( "cancel", None, ) ================================================ FILE: playwright/_impl/_assertions.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections.abc from typing import Any, List, Optional, Pattern, Sequence, Union from urllib.parse import urljoin from playwright._impl._api_structures import ( AriaRole, ExpectedTextValue, FrameExpectOptions, FrameExpectResult, ) from playwright._impl._connection import format_call_log from playwright._impl._errors import Error from playwright._impl._fetch import APIResponse from playwright._impl._helper import is_textual_mime_type from playwright._impl._locator import Locator from playwright._impl._page import Page from playwright._impl._str_utils import escape_regex_flags class AssertionsBase: def __init__( self, locator: Locator, timeout: float = None, is_not: bool = False, message: Optional[str] = None, ) -> None: self._actual_locator = locator self._loop = locator._loop self._dispatcher_fiber = locator._dispatcher_fiber self._timeout = timeout self._is_not = is_not self._custom_message = message async def _call_expect( self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] ) -> FrameExpectResult: raise NotImplementedError( "_call_expect must be implemented in a derived class." ) async def _expect_impl( self, expression: str, expect_options: FrameExpectOptions, expected: Any, message: str, title: str = None, ) -> None: __tracebackhide__ = True expect_options["isNot"] = self._is_not if expect_options.get("timeout") is None: expect_options["timeout"] = self._timeout or 5_000 if expect_options["isNot"]: message = message.replace("expected to", "expected not to") if "useInnerText" in expect_options and expect_options["useInnerText"] is None: del expect_options["useInnerText"] result = await self._call_expect(expression, expect_options, title) if result["matches"] == self._is_not: actual = result.get("received") if self._custom_message: out_message = self._custom_message if expected is not None: out_message += f"\nExpected value: '{expected or ''}'" else: out_message = ( f"{message} '{expected}'" if expected is not None else f"{message}" ) error_message = result.get("errorMessage") error_message = f"\n{error_message}" if error_message else "" raise AssertionError( f"{out_message}\nActual value: {actual}{error_message} {format_call_log(result.get('log'))}" ) class PageAssertions(AssertionsBase): def __init__( self, page: Page, timeout: float = None, is_not: bool = False, message: Optional[str] = None, ) -> None: super().__init__(page.locator(":root"), timeout, is_not, message) self._actual_page = page async def _call_expect( self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] ) -> FrameExpectResult: __tracebackhide__ = True return await self._actual_page.main_frame._expect( None, expression, expect_options, title ) @property def _not(self) -> "PageAssertions": return PageAssertions( self._actual_page, self._timeout, not self._is_not, self._custom_message ) async def to_have_title( self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None ) -> None: __tracebackhide__ = True expected_values = to_expected_text_values( [titleOrRegExp], normalize_white_space=True ) await self._expect_impl( "to.have.title", FrameExpectOptions(expectedText=expected_values, timeout=timeout), titleOrRegExp, "Page title expected to be", 'Expect "to_have_title"', ) async def not_to_have_title( self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None ) -> None: __tracebackhide__ = True await self._not.to_have_title(titleOrRegExp, timeout) async def to_have_url( self, urlOrRegExp: Union[str, Pattern[str]], timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True base_url = self._actual_page.context._base_url if isinstance(urlOrRegExp, str) and base_url: urlOrRegExp = urljoin(base_url, urlOrRegExp) expected_text = to_expected_text_values([urlOrRegExp], ignoreCase=ignoreCase) await self._expect_impl( "to.have.url", FrameExpectOptions(expectedText=expected_text, timeout=timeout), urlOrRegExp, "Page URL expected to be", 'Expect "to_have_url"', ) async def not_to_have_url( self, urlOrRegExp: Union[Pattern[str], str], timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True await self._not.to_have_url(urlOrRegExp, timeout, ignoreCase) class LocatorAssertions(AssertionsBase): def __init__( self, locator: Locator, timeout: float = None, is_not: bool = False, message: Optional[str] = None, ) -> None: super().__init__(locator, timeout, is_not, message) self._actual_locator = locator async def _call_expect( self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] ) -> FrameExpectResult: __tracebackhide__ = True return await self._actual_locator._expect(expression, expect_options, title) @property def _not(self) -> "LocatorAssertions": return LocatorAssertions( self._actual_locator, self._timeout, not self._is_not, self._custom_message ) async def to_contain_text( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], useInnerText: bool = None, timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True if isinstance(expected, collections.abc.Sequence) and not isinstance( expected, str ): expected_text = to_expected_text_values( expected, match_substring=True, normalize_white_space=True, ignoreCase=ignoreCase, ) await self._expect_impl( "to.contain.text.array", FrameExpectOptions( expectedText=expected_text, useInnerText=useInnerText, timeout=timeout, ), expected, "Locator expected to contain text", 'Expect "to_contain_text"', ) else: expected_text = to_expected_text_values( [expected], match_substring=True, normalize_white_space=True, ignoreCase=ignoreCase, ) await self._expect_impl( "to.have.text", FrameExpectOptions( expectedText=expected_text, useInnerText=useInnerText, timeout=timeout, ), expected, "Locator expected to contain text", 'Expect "to_contain_text"', ) async def not_to_contain_text( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], useInnerText: bool = None, timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True await self._not.to_contain_text(expected, useInnerText, timeout, ignoreCase) async def to_have_attribute( self, name: str, value: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True expected_text = to_expected_text_values([value], ignoreCase=ignoreCase) await self._expect_impl( "to.have.attribute.value", FrameExpectOptions( expressionArg=name, expectedText=expected_text, timeout=timeout ), value, "Locator expected to have attribute", 'Expect "to_have_attribute"', ) async def not_to_have_attribute( self, name: str, value: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_attribute( name, value, ignoreCase=ignoreCase, timeout=timeout ) async def to_have_class( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], timeout: float = None, ) -> None: __tracebackhide__ = True if isinstance(expected, collections.abc.Sequence) and not isinstance( expected, str ): expected_text = to_expected_text_values(expected) await self._expect_impl( "to.have.class.array", FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", 'Expect "to_have_class"', ) else: expected_text = to_expected_text_values([expected]) await self._expect_impl( "to.have.class", FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", 'Expect "to_have_class"', ) async def not_to_have_class( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_class(expected, timeout) async def to_contain_class( self, expected: Union[ Sequence[str], str, ], timeout: float = None, ) -> None: __tracebackhide__ = True if isinstance(expected, collections.abc.Sequence) and not isinstance( expected, str ): expected_text = to_expected_text_values(expected) await self._expect_impl( "to.contain.class.array", FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to contain class names", 'Expect "to_contain_class"', ) else: expected_text = to_expected_text_values([expected]) await self._expect_impl( "to.contain.class", FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to contain class", 'Expect "to_contain_class"', ) async def not_to_contain_class( self, expected: Union[ Sequence[str], str, ], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_contain_class(expected, timeout) async def to_have_count( self, count: int, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.have.count", FrameExpectOptions(expectedNumber=count, timeout=timeout), count, "Locator expected to have count", 'Expect "to_have_count"', ) async def not_to_have_count( self, count: int, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_count(count, timeout) async def to_have_css( self, name: str, value: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True expected_text = to_expected_text_values([value]) await self._expect_impl( "to.have.css", FrameExpectOptions( expressionArg=name, expectedText=expected_text, timeout=timeout ), value, "Locator expected to have CSS", 'Expect "to_have_css"', ) async def not_to_have_css( self, name: str, value: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_css(name, value, timeout) async def to_have_id( self, id: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True expected_text = to_expected_text_values([id]) await self._expect_impl( "to.have.id", FrameExpectOptions(expectedText=expected_text, timeout=timeout), id, "Locator expected to have ID", 'Expect "to_have_id"', ) async def not_to_have_id( self, id: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_id(id, timeout) async def to_have_js_property( self, name: str, value: Any, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.have.property", FrameExpectOptions( expressionArg=name, expectedValue=value, timeout=timeout ), value, "Locator expected to have JS Property", 'Expect "to_have_property"', ) async def not_to_have_js_property( self, name: str, value: Any, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_js_property(name, value, timeout) async def to_have_value( self, value: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True expected_text = to_expected_text_values([value]) await self._expect_impl( "to.have.value", FrameExpectOptions(expectedText=expected_text, timeout=timeout), value, "Locator expected to have Value", 'Expect "to_have_value"', ) async def not_to_have_value( self, value: Union[str, Pattern[str]], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_value(value, timeout) async def to_have_values( self, values: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]] ], timeout: float = None, ) -> None: __tracebackhide__ = True expected_text = to_expected_text_values(values) await self._expect_impl( "to.have.values", FrameExpectOptions(expectedText=expected_text, timeout=timeout), values, "Locator expected to have Values", 'Expect "to_have_values"', ) async def not_to_have_values( self, values: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]] ], timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_values(values, timeout) async def to_have_text( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], useInnerText: bool = None, timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True if isinstance(expected, collections.abc.Sequence) and not isinstance( expected, str ): expected_text = to_expected_text_values( expected, normalize_white_space=True, ignoreCase=ignoreCase, ) await self._expect_impl( "to.have.text.array", FrameExpectOptions( expectedText=expected_text, useInnerText=useInnerText, timeout=timeout, ), expected, "Locator expected to have text", 'Expect "to_have_text"', ) else: expected_text = to_expected_text_values( [expected], normalize_white_space=True, ignoreCase=ignoreCase ) await self._expect_impl( "to.have.text", FrameExpectOptions( expectedText=expected_text, useInnerText=useInnerText, timeout=timeout, ), expected, "Locator expected to have text", 'Expect "to_have_text"', ) async def not_to_have_text( self, expected: Union[ Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]], Pattern[str], str, ], useInnerText: bool = None, timeout: float = None, ignoreCase: bool = None, ) -> None: __tracebackhide__ = True await self._not.to_have_text(expected, useInnerText, timeout, ignoreCase) async def to_be_attached( self, attached: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True if attached is None: attached = True attached_string = "attached" if attached else "detached" await self._expect_impl( ("to.be.attached" if attached else "to.be.detached"), FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {attached_string}", 'Expect "to_be_attached"', ) async def to_be_checked( self, timeout: float = None, checked: bool = None, indeterminate: bool = None, ) -> None: __tracebackhide__ = True expected_value = {} if indeterminate is not None: expected_value["indeterminate"] = indeterminate if checked is not None: expected_value["checked"] = checked checked_string: str if indeterminate: checked_string = "indeterminate" else: checked_string = "unchecked" if checked is False else "checked" await self._expect_impl( "to.be.checked", FrameExpectOptions(timeout=timeout, expectedValue=expected_value), None, f"Locator expected to be {checked_string}", 'Expect "to_be_checked"', ) async def not_to_be_attached( self, attached: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_attached(attached=attached, timeout=timeout) async def not_to_be_checked( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_checked(timeout) async def to_be_disabled( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.be.disabled", FrameExpectOptions(timeout=timeout), None, "Locator expected to be disabled", 'Expect "to_be_disabled"', ) async def not_to_be_disabled( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_disabled(timeout) async def to_be_editable( self, editable: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True if editable is None: editable = True editable_string = "editable" if editable else "readonly" await self._expect_impl( "to.be.editable" if editable else "to.be.readonly", FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {editable_string}", 'Expect "to_be_editable"', ) async def not_to_be_editable( self, editable: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_editable(editable, timeout) async def to_be_empty( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.be.empty", FrameExpectOptions(timeout=timeout), None, "Locator expected to be empty", 'Expect "to_be_empty"', ) async def not_to_be_empty( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_empty(timeout) async def to_be_enabled( self, enabled: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True if enabled is None: enabled = True enabled_string = "enabled" if enabled else "disabled" await self._expect_impl( "to.be.enabled" if enabled else "to.be.disabled", FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {enabled_string}", 'Expect "to_be_enabled"', ) async def not_to_be_enabled( self, enabled: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_enabled(enabled, timeout) async def to_be_hidden( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.be.hidden", FrameExpectOptions(timeout=timeout), None, "Locator expected to be hidden", 'Expect "to_be_hidden"', ) async def not_to_be_hidden( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_hidden(timeout) async def to_be_visible( self, visible: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True if visible is None: visible = True visible_string = "visible" if visible else "hidden" await self._expect_impl( "to.be.visible" if visible else "to.be.hidden", FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {visible_string}", 'Expect "to_be_visible"', ) async def not_to_be_visible( self, visible: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_visible(visible, timeout) async def to_be_focused( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.be.focused", FrameExpectOptions(timeout=timeout), None, "Locator expected to be focused", 'Expect "to_be_focused"', ) async def not_to_be_focused( self, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_be_focused(timeout) async def to_be_in_viewport( self, ratio: float = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._expect_impl( "to.be.in.viewport", FrameExpectOptions(timeout=timeout, expectedNumber=ratio), None, "Locator expected to be in viewport", 'Expect "to_be_in_viewport"', ) async def not_to_be_in_viewport( self, ratio: float = None, timeout: float = None ) -> None: __tracebackhide__ = True await self._not.to_be_in_viewport(ratio=ratio, timeout=timeout) async def to_have_accessible_description( self, description: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True expected_values = to_expected_text_values( [description], ignoreCase=ignoreCase, normalize_white_space=True ) await self._expect_impl( "to.have.accessible.description", FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible description", 'Expect "to_have_accessible_description"', ) async def not_to_have_accessible_description( self, name: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_accessible_description(name, ignoreCase, timeout) async def to_have_accessible_name( self, name: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True expected_values = to_expected_text_values( [name], ignoreCase=ignoreCase, normalize_white_space=True ) await self._expect_impl( "to.have.accessible.name", FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible name", 'Expect "to_have_accessible_name"', ) async def not_to_have_accessible_name( self, name: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_accessible_name(name, ignoreCase, timeout) async def to_have_role(self, role: AriaRole, timeout: float = None) -> None: __tracebackhide__ = True if isinstance(role, Pattern): raise Error('"role" argument in to_have_role must be a string') expected_values = to_expected_text_values([role]) await self._expect_impl( "to.have.role", FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible role", 'Expect "to_have_role"', ) async def to_have_accessible_error_message( self, errorMessage: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True expected_values = to_expected_text_values( [errorMessage], ignoreCase=ignoreCase, normalize_white_space=True ) await self._expect_impl( "to.have.accessible.error.message", FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible error message", 'Expect "to_have_accessible_error_message"', ) async def not_to_have_accessible_error_message( self, errorMessage: Union[str, Pattern[str]], ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True await self._not.to_have_accessible_error_message( errorMessage=errorMessage, ignoreCase=ignoreCase, timeout=timeout ) async def not_to_have_role(self, role: AriaRole, timeout: float = None) -> None: __tracebackhide__ = True await self._not.to_have_role(role, timeout) async def to_match_aria_snapshot( self, expected: str, timeout: float = None ) -> None: __tracebackhide__ = True await self._expect_impl( "to.match.aria", FrameExpectOptions(expectedValue=expected, timeout=timeout), expected, "Locator expected to match Aria snapshot", 'Expect "to_match_aria_snapshot"', ) async def not_to_match_aria_snapshot( self, expected: str, timeout: float = None ) -> None: __tracebackhide__ = True await self._not.to_match_aria_snapshot(expected, timeout) class APIResponseAssertions: def __init__( self, response: APIResponse, timeout: float = None, is_not: bool = False, message: Optional[str] = None, ) -> None: self._loop = response._loop self._dispatcher_fiber = response._dispatcher_fiber self._timeout = timeout self._is_not = is_not self._actual = response self._custom_message = message @property def _not(self) -> "APIResponseAssertions": return APIResponseAssertions( self._actual, self._timeout, not self._is_not, self._custom_message ) async def to_be_ok( self, ) -> None: __tracebackhide__ = True if self._is_not is not self._actual.ok: return message = f"Response status expected to be within [200..299] range, was '{self._actual.status}'" if self._is_not: message = message.replace("expected to", "expected not to") out_message = self._custom_message or message out_message += format_call_log(await self._actual._fetch_log()) content_type = self._actual.headers.get("content-type") is_text_encoding = content_type and is_textual_mime_type(content_type) text = await self._actual.text() if is_text_encoding else None if text is not None: out_message += f"\n Response Text:\n{text[:1000]}" raise AssertionError(out_message) async def not_to_be_ok(self) -> None: __tracebackhide__ = True await self._not.to_be_ok() def expected_regex( pattern: Pattern[str], match_substring: bool, normalize_white_space: bool, ignoreCase: Optional[bool] = None, ) -> ExpectedTextValue: expected = ExpectedTextValue( regexSource=pattern.pattern, regexFlags=escape_regex_flags(pattern), matchSubstring=match_substring, normalizeWhiteSpace=normalize_white_space, ignoreCase=ignoreCase, ) if expected["ignoreCase"] is None: del expected["ignoreCase"] return expected def to_expected_text_values( items: Union[ Sequence[Pattern[str]], Sequence[str], Sequence[Union[str, Pattern[str]]] ], match_substring: bool = False, normalize_white_space: bool = False, ignoreCase: Optional[bool] = None, ) -> Sequence[ExpectedTextValue]: out: List[ExpectedTextValue] = [] assert isinstance(items, (list, tuple)) for item in items: if isinstance(item, str): o = ExpectedTextValue( string=item, matchSubstring=match_substring, normalizeWhiteSpace=normalize_white_space, ignoreCase=ignoreCase, ) if o["ignoreCase"] is None: del o["ignoreCase"] out.append(o) elif isinstance(item, Pattern): out.append( expected_regex(item, match_substring, normalize_white_space, ignoreCase) ) else: raise Error("value must be a string or regular expression") return out ================================================ FILE: playwright/_impl/_async_base.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from contextlib import AbstractAsyncContextManager from types import TracebackType from typing import Any, Callable, Generic, Optional, Type, TypeVar, Union from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper mapping = ImplToApiMapping() T = TypeVar("T") Self = TypeVar("Self", bound="AsyncContextManager") class AsyncEventInfo(Generic[T]): def __init__(self, future: "asyncio.Future[T]") -> None: self._future = future @property async def value(self) -> T: return mapping.from_maybe_impl(await self._future) def _cancel(self) -> None: self._future.cancel() def is_done(self) -> bool: return self._future.done() class AsyncEventContextManager(Generic[T], AbstractAsyncContextManager): def __init__(self, future: "asyncio.Future[T]") -> None: self._event = AsyncEventInfo[T](future) async def __aenter__(self) -> AsyncEventInfo[T]: return self._event async def __aexit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: if exc_val: self._event._cancel() else: await self._event.value class AsyncBase(ImplWrapper): def __init__(self, impl_obj: Any) -> None: super().__init__(impl_obj) self._loop = impl_obj._loop def __str__(self) -> str: return self._impl_obj.__str__() def _wrap_handler( self, handler: Union[Callable[..., Any], Any] ) -> Callable[..., None]: if callable(handler): return mapping.wrap_handler(handler) return handler def on(self, event: Any, f: Any) -> None: """Registers the function ``f`` to the event name ``event``.""" self._impl_obj.on(event, self._wrap_handler(f)) def once(self, event: Any, f: Any) -> None: """The same as ``self.on``, except that the listener is automatically removed after being called. """ self._impl_obj.once(event, self._wrap_handler(f)) def remove_listener(self, event: Any, f: Any) -> None: """Removes the function ``f`` from ``event``.""" self._impl_obj.remove_listener(event, self._wrap_handler(f)) class AsyncContextManager(AsyncBase): async def __aenter__(self: Self) -> Self: return self async def __aexit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: await self.close() async def close(self) -> None: ... ================================================ FILE: playwright/_impl/_browser.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path from types import SimpleNamespace from typing import ( TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Set, Union, cast, ) from playwright._impl._api_structures import ( ClientCertificate, Geolocation, HttpCredentials, ProxySettings, StorageState, ViewportSize, ) from playwright._impl._artifact import Artifact from playwright._impl._browser_context import BrowserContext from playwright._impl._cdp_session import CDPSession from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._errors import is_target_closed_error from playwright._impl._helper import ( ColorScheme, Contrast, ForcedColors, HarContentPolicy, HarMode, ReducedMotion, ServiceWorkersPolicy, locals_to_params, make_dirs_for_file, ) from playwright._impl._page import Page if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_type import BrowserType class Browser(ChannelOwner): Events = SimpleNamespace( Disconnected="disconnected", ) def __init__( self, parent: "BrowserType", type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._browser_type: Optional["BrowserType"] = None self._is_connected = True self._should_close_connection_on_close = False self._cr_tracing_path: Optional[str] = None self._contexts: Set[BrowserContext] = set() self._traces_dir: Optional[str] = None self._channel.on( "context", lambda params: self._did_create_context( cast(BrowserContext, from_channel(params["context"])) ), ) self._channel.on("close", lambda _: self._on_close()) self._close_reason: Optional[str] = None def __repr__(self) -> str: return f"" def _connect_to_browser_type( self, browser_type: "BrowserType", traces_dir: Optional[str] = None, ) -> None: # Note: when using connect(), `browserType` is different from `this.parent`. # This is why browser type is not wired up in the constructor, and instead this separate method is called later on. self._browser_type = browser_type self._traces_dir = traces_dir for context in self._contexts: self._setup_browser_context(context) def _did_create_context(self, context: BrowserContext) -> None: context._browser = self self._contexts.add(context) # Note: when connecting to a browser, initial contexts arrive before `_browserType` is set, # and will be configured later in `ConnectToBrowserType`. if self._browser_type: self._setup_browser_context(context) def _setup_browser_context(self, context: BrowserContext) -> None: context._tracing._traces_dir = self._traces_dir assert self._browser_type is not None self._browser_type._playwright.selectors._contexts_for_selectors.add(context) def _on_close(self) -> None: self._is_connected = False self.emit(Browser.Events.Disconnected, self) @property def contexts(self) -> List[BrowserContext]: return list(self._contexts) @property def browser_type(self) -> "BrowserType": assert self._browser_type is not None return self._browser_type def is_connected(self) -> bool: return self._is_connected async def new_context( self, viewport: ViewportSize = None, screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, bypassCSP: bool = None, userAgent: str = None, locale: str = None, timezoneId: str = None, geolocation: Geolocation = None, permissions: Sequence[str] = None, extraHTTPHeaders: Dict[str, str] = None, offline: bool = None, httpCredentials: HttpCredentials = None, deviceScaleFactor: float = None, isMobile: bool = None, hasTouch: bool = None, colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, contrast: Contrast = None, acceptDownloads: bool = None, defaultBrowserType: str = None, proxy: ProxySettings = None, recordHarPath: Union[Path, str] = None, recordHarOmitContent: bool = None, recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, storageState: Union[StorageState, str, Path] = None, baseURL: str = None, strictSelectors: bool = None, serviceWorkers: ServiceWorkersPolicy = None, recordHarUrlFilter: Union[Pattern[str], str] = None, recordHarMode: HarMode = None, recordHarContent: HarContentPolicy = None, clientCertificates: List[ClientCertificate] = None, ) -> BrowserContext: params = locals_to_params(locals()) assert self._browser_type is not None await self._browser_type._prepare_browser_context_params(params) channel = await self._channel.send("newContext", None, params) context = cast(BrowserContext, from_channel(channel)) await context._initialize_har_from_options( record_har_content=recordHarContent, record_har_mode=recordHarMode, record_har_omit_content=recordHarOmitContent, record_har_path=recordHarPath, record_har_url_filter=recordHarUrlFilter, ) return context async def new_page( self, viewport: ViewportSize = None, screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, bypassCSP: bool = None, userAgent: str = None, locale: str = None, timezoneId: str = None, geolocation: Geolocation = None, permissions: Sequence[str] = None, extraHTTPHeaders: Dict[str, str] = None, offline: bool = None, httpCredentials: HttpCredentials = None, deviceScaleFactor: float = None, isMobile: bool = None, hasTouch: bool = None, colorScheme: ColorScheme = None, forcedColors: ForcedColors = None, contrast: Contrast = None, reducedMotion: ReducedMotion = None, acceptDownloads: bool = None, defaultBrowserType: str = None, proxy: ProxySettings = None, recordHarPath: Union[Path, str] = None, recordHarOmitContent: bool = None, recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, storageState: Union[StorageState, str, Path] = None, baseURL: str = None, strictSelectors: bool = None, serviceWorkers: ServiceWorkersPolicy = None, recordHarUrlFilter: Union[Pattern[str], str] = None, recordHarMode: HarMode = None, recordHarContent: HarContentPolicy = None, clientCertificates: List[ClientCertificate] = None, ) -> Page: params = locals_to_params(locals()) async def inner() -> Page: context = await self.new_context(**params) page = await context.new_page() page._owned_context = context context._owner_page = page return page return await self._connection.wrap_api_call(inner, title="Create page") async def close(self, reason: str = None) -> None: self._close_reason = reason try: if self._should_close_connection_on_close: await self._connection.stop_async() else: await self._channel.send("close", None, {"reason": reason}) except Exception as e: if not is_target_closed_error(e): raise e @property def version(self) -> str: return self._initializer["version"] async def new_browser_cdp_session(self) -> CDPSession: return from_channel(await self._channel.send("newBrowserCDPSession", None)) async def start_tracing( self, page: Page = None, path: Union[str, Path] = None, screenshots: bool = None, categories: Sequence[str] = None, ) -> None: params = locals_to_params(locals()) if page: params["page"] = page._channel if path: self._cr_tracing_path = str(path) params["path"] = str(path) await self._channel.send("startTracing", None, params) async def stop_tracing(self) -> bytes: artifact = cast( Artifact, from_channel(await self._channel.send("stopTracing", None)) ) buffer = await artifact.read_info_buffer() await artifact.delete() if self._cr_tracing_path: make_dirs_for_file(self._cr_tracing_path) with open(self._cr_tracing_path, "wb") as f: f.write(buffer) self._cr_tracing_path = None return buffer ================================================ FILE: playwright/_impl/_browser_context.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import json from pathlib import Path from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Pattern, Sequence, Set, Union, cast, ) from playwright._impl._api_structures import ( Cookie, Geolocation, SetCookieParam, StorageState, ) from playwright._impl._artifact import Artifact from playwright._impl._cdp_session import CDPSession from playwright._impl._clock import Clock from playwright._impl._connection import ( ChannelOwner, from_channel, from_nullable_channel, ) from playwright._impl._console_message import ConsoleMessage from playwright._impl._dialog import Dialog from playwright._impl._errors import Error, TargetClosedError from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( HarContentPolicy, HarMode, HarRecordingMetadata, RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, TimeoutSettings, URLMatch, WebSocketRouteHandlerCallback, async_readfile, async_writefile, locals_to_params, parse_error, to_impl, ) from playwright._impl._network import ( Request, Response, Route, WebSocketRoute, WebSocketRouteHandler, serialize_headers, ) from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._str_utils import escape_regex_flags from playwright._impl._tracing import Tracing from playwright._impl._waiter import Waiter from playwright._impl._web_error import WebError if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser import Browser class BrowserContext(ChannelOwner): Events = SimpleNamespace( # Deprecated in v1.56, never emitted anymore. BackgroundPage="backgroundpage", Close="close", Console="console", Dialog="dialog", Page="page", WebError="weberror", ServiceWorker="serviceworker", Request="request", Response="response", RequestFailed="requestfailed", RequestFinished="requestfinished", ) def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) # Browser is null for browser contexts created outside of normal browser, e.g. android or electron. # circular import workaround: self._browser: Optional["Browser"] = None if parent.__class__.__name__ == "Browser": self._browser = cast("Browser", parent) self._pages: List[Page] = [] self._routes: List[RouteHandler] = [] self._web_socket_routes: List[WebSocketRouteHandler] = [] self._bindings: Dict[str, Any] = {} self._timeout_settings = TimeoutSettings(None) self._owner_page: Optional[Page] = None self._options: Dict[str, Any] = initializer["options"] self._service_workers: Set[Worker] = set() self._base_url: Optional[str] = self._options.get("baseURL") self._videos_dir: Optional[str] = self._options.get("recordVideo") self._tracing = cast(Tracing, from_channel(initializer["tracing"])) self._har_recorders: Dict[str, HarRecordingMetadata] = {} self._request: APIRequestContext = from_channel(initializer["requestContext"]) self._request._timeout_settings = self._timeout_settings self._clock = Clock(self) self._channel.on( "bindingCall", lambda params: self._on_binding(from_channel(params["binding"])), ) self._channel.on("close", lambda _: self._on_close()) self._channel.on( "page", lambda params: self._on_page(from_channel(params["page"])) ) self._channel.on( "route", lambda params: self._loop.create_task( self._on_route( from_channel(params.get("route")), ) ), ) self._channel.on( "webSocketRoute", lambda params: self._loop.create_task( self._on_web_socket_route( from_channel(params["webSocketRoute"]), ) ), ) self._channel.on( "serviceWorker", lambda params: self._on_service_worker(from_channel(params["worker"])), ) self._channel.on( "console", lambda event: self._on_console_message(event), ) self._channel.on( "dialog", lambda params: self._on_dialog(from_channel(params["dialog"])) ) self._channel.on( "pageError", lambda params: self._on_page_error( parse_error(params["error"]["error"]), from_nullable_channel(params["page"]), ), ) self._channel.on( "request", lambda params: self._on_request( from_channel(params["request"]), from_nullable_channel(params.get("page")), ), ) self._channel.on( "response", lambda params: self._on_response( from_channel(params["response"]), from_nullable_channel(params.get("page")), ), ) self._channel.on( "requestFailed", lambda params: self._on_request_failed( from_channel(params["request"]), params["responseEndTiming"], params.get("failureText"), from_nullable_channel(params.get("page")), ), ) self._channel.on( "requestFinished", lambda params: self._on_request_finished( from_channel(params["request"]), from_nullable_channel(params.get("response")), params["responseEndTiming"], from_nullable_channel(params.get("page")), ), ) self._closed_future: asyncio.Future = asyncio.Future() self.once( self.Events.Close, lambda context: self._closed_future.set_result(True) ) self._close_reason: Optional[str] = None self._har_routers: List[HarRouter] = [] self._set_event_to_subscription_mapping( { BrowserContext.Events.Console: "console", BrowserContext.Events.Dialog: "dialog", BrowserContext.Events.Request: "request", BrowserContext.Events.Response: "response", BrowserContext.Events.RequestFinished: "requestFinished", BrowserContext.Events.RequestFailed: "requestFailed", } ) self._closing_or_closed = False def __repr__(self) -> str: return f"" def _on_page(self, page: Page) -> None: self._pages.append(page) self.emit(BrowserContext.Events.Page, page) if page._opener and not page._opener.is_closed(): page._opener.emit(Page.Events.Popup, page) async def _on_route(self, route: Route) -> None: route._context = self page = route.request._safe_page() route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page or the context was closed we stall all requests right away. if (page and page._close_was_called) or self._closing_or_closed: return if not route_handler.matches(route.request.url): continue if route_handler not in self._routes: continue if route_handler.will_expire: self._routes.remove(route_handler) try: handled = await route_handler.handle(route) finally: if len(self._routes) == 0: asyncio.create_task( self._connection.wrap_api_call( lambda: self._update_interception_patterns(), True ) ) if handled: return try: # If the page is closed or unrouteAll() was called without waiting and interception disabled, # the method will throw an error - silence it. await route._inner_continue(True) except Exception: pass async def _on_web_socket_route(self, web_socket_route: WebSocketRoute) -> None: route_handler = next( ( route_handler for route_handler in self._web_socket_routes if route_handler.matches(web_socket_route.url) ), None, ) if route_handler: await route_handler.handle(web_socket_route) else: web_socket_route.connect_to_server() def _on_binding(self, binding_call: BindingCall) -> None: func = self._bindings.get(binding_call._initializer["name"]) if func is None: return asyncio.create_task(binding_call.call(func)) def set_default_navigation_timeout(self, timeout: float) -> None: return self._set_default_navigation_timeout_impl(timeout) def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) def set_default_timeout(self, timeout: float) -> None: return self._set_default_timeout_impl(timeout) def _set_default_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_timeout(timeout) @property def pages(self) -> List[Page]: return self._pages.copy() @property def browser(self) -> Optional["Browser"]: return self._browser async def _initialize_har_from_options( self, record_har_path: Optional[Union[Path, str]], record_har_content: Optional[HarContentPolicy], record_har_omit_content: Optional[bool], record_har_url_filter: Optional[Union[Pattern[str], str]], record_har_mode: Optional[HarMode], ) -> None: if not record_har_path: return record_har_path = str(record_har_path) default_policy: HarContentPolicy = ( "attach" if record_har_path.endswith(".zip") else "embed" ) content_policy: HarContentPolicy = record_har_content or ( "omit" if record_har_omit_content is True else default_policy ) await self._record_into_har( har=record_har_path, page=None, url=record_har_url_filter, update_content=content_policy, update_mode=(record_har_mode or "full"), ) async def new_page(self) -> Page: if self._owner_page: raise Error("Please use browser.new_context()") return from_channel(await self._channel.send("newPage", None)) async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: if urls is None: urls = [] if isinstance(urls, str): urls = [urls] return await self._channel.send("cookies", None, dict(urls=urls)) async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: await self._channel.send("addCookies", None, dict(cookies=cookies)) async def clear_cookies( self, name: Union[str, Pattern[str]] = None, domain: Union[str, Pattern[str]] = None, path: Union[str, Pattern[str]] = None, ) -> None: await self._channel.send( "clearCookies", None, { "name": name if isinstance(name, str) else None, "nameRegexSource": name.pattern if isinstance(name, Pattern) else None, "nameRegexFlags": ( escape_regex_flags(name) if isinstance(name, Pattern) else None ), "domain": domain if isinstance(domain, str) else None, "domainRegexSource": ( domain.pattern if isinstance(domain, Pattern) else None ), "domainRegexFlags": ( escape_regex_flags(domain) if isinstance(domain, Pattern) else None ), "path": path if isinstance(path, str) else None, "pathRegexSource": path.pattern if isinstance(path, Pattern) else None, "pathRegexFlags": ( escape_regex_flags(path) if isinstance(path, Pattern) else None ), }, ) async def grant_permissions( self, permissions: Sequence[str], origin: str = None ) -> None: await self._channel.send("grantPermissions", None, locals_to_params(locals())) async def clear_permissions(self) -> None: await self._channel.send("clearPermissions", None) async def set_geolocation(self, geolocation: Geolocation = None) -> None: await self._channel.send("setGeolocation", None, locals_to_params(locals())) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( "setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers)) ) async def set_offline(self, offline: bool) -> None: await self._channel.send("setOffline", None, dict(offline=offline)) async def add_init_script( self, script: str = None, path: Union[str, Path] = None ) -> None: if path: script = (await async_readfile(path)).decode() if not isinstance(script, str): raise Error("Either path or script parameter must be specified") await self._channel.send("addInitScript", None, dict(source=script)) async def expose_binding( self, name: str, callback: Callable, handle: bool = None ) -> None: for page in self._pages: if name in page._bindings: raise Error( f'Function "{name}" has been already registered in one of the pages' ) if name in self._bindings: raise Error(f'Function "{name}" has been already registered') self._bindings[name] = callback await self._channel.send( "exposeBinding", None, dict(name=name, needsHandle=handle or False) ) async def expose_function(self, name: str, callback: Callable) -> None: await self.expose_binding(name, lambda source, *args: callback(*args)) async def route( self, url: URLMatch, handler: RouteHandlerCallback, times: int = None ) -> None: self._routes.insert( 0, RouteHandler( self._base_url, url, handler, True if self._dispatcher_fiber else False, times, ), ) await self._update_interception_patterns() async def unroute( self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None ) -> None: removed = [] remaining = [] for route in self._routes: if route.url != url or (handler and route.handler != handler): remaining.append(route) else: removed.append(route) await self._unroute_internal(removed, remaining, "default") async def _unroute_internal( self, removed: List[RouteHandler], remaining: List[RouteHandler], behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining if behavior is not None and behavior != "default": await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore await self._update_interception_patterns() async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, WebSocketRouteHandler(self._base_url, url, handler), ) await self._update_web_socket_interception_patterns() def _dispose_har_routers(self) -> None: for router in self._har_routers: router.dispose() self._har_routers = [] async def unroute_all( self, behavior: Literal["default", "ignoreErrors", "wait"] = None ) -> None: await self._unroute_internal(self._routes, [], behavior) self._dispose_har_routers() async def _record_into_har( self, har: Union[Path, str], page: Optional[Page] = None, url: Union[Pattern[str], str] = None, update_content: HarContentPolicy = None, update_mode: HarMode = None, ) -> None: update_content = update_content or "attach" params: Dict[str, Any] = { "options": { "zip": str(har).endswith(".zip"), "content": update_content, "urlGlob": url if isinstance(url, str) else None, "urlRegexSource": url.pattern if isinstance(url, Pattern) else None, "urlRegexFlags": ( escape_regex_flags(url) if isinstance(url, Pattern) else None ), "mode": update_mode or "minimal", } } if page: params["page"] = page._channel har_id = await self._channel.send("harStart", None, params) self._har_recorders[har_id] = { "path": str(har), "content": update_content, } async def route_from_har( self, har: Union[Path, str], url: Union[Pattern[str], str] = None, notFound: RouteFromHarNotFoundPolicy = None, update: bool = None, updateContent: Literal["attach", "embed"] = None, updateMode: HarMode = None, ) -> None: if update: await self._record_into_har( har=har, page=None, url=url, update_content=updateContent, update_mode=updateMode, ) return router = await HarRouter.create( local_utils=self._connection.local_utils, file=str(har), not_found_action=notFound or "abort", url_matcher=url, ) self._har_routers.append(router) await router.add_context_route(self) async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( "setNetworkInterceptionPatterns", None, {"patterns": patterns} ) async def _update_web_socket_interception_patterns(self) -> None: patterns = WebSocketRouteHandler.prepare_interception_patterns( self._web_socket_routes ) await self._channel.send( "setWebSocketInterceptionPatterns", None, {"patterns": patterns} ) def expect_event( self, event: str, predicate: Callable = None, timeout: float = None, ) -> EventContextManagerImpl: if timeout is None: timeout = self._timeout_settings.timeout() waiter = Waiter(self, f"browser_context.expect_event({event})") waiter.reject_on_timeout( timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' ) if event != BrowserContext.Events.Close: waiter.reject_on_event( self, BrowserContext.Events.Close, lambda: TargetClosedError() ) waiter.wait_for_event(self, event, predicate) return EventContextManagerImpl(waiter.result()) def _on_close(self) -> None: self._closing_or_closed = True if self._browser: if self in self._browser._contexts: self._browser._contexts.remove(self) assert self._browser._browser_type is not None if ( self in self._browser._browser_type._playwright.selectors._contexts_for_selectors ): self._browser._browser_type._playwright.selectors._contexts_for_selectors.remove( self ) self._dispose_har_routers() self._tracing._reset_stack_counter() self.emit(BrowserContext.Events.Close, self) async def close(self, reason: str = None) -> None: if self._closing_or_closed: return self._close_reason = reason self._closing_or_closed = True await self.request.dispose(reason=reason) async def _inner_close() -> None: for har_id, params in self._har_recorders.items(): har = cast( Artifact, from_channel( await self._channel.send("harExport", None, {"harId": har_id}) ), ) # Server side will compress artifact if content is attach or if file is .zip. is_compressed = params.get("content") == "attach" or params[ "path" ].endswith(".zip") need_compressed = params["path"].endswith(".zip") if is_compressed and not need_compressed: tmp_path = params["path"] + ".tmp" await har.save_as(tmp_path) await self._connection.local_utils.har_unzip( zipFile=tmp_path, harFile=params["path"] ) else: await har.save_as(params["path"]) await har.delete() await self._channel._connection.wrap_api_call(_inner_close, True) await self._channel.send("close", None, {"reason": reason}) await self._closed_future async def storage_state( self, path: Union[str, Path] = None, indexedDB: bool = None ) -> StorageState: result = await self._channel.send_return_as_dict( "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) return result def _effective_close_reason(self) -> Optional[str]: if self._close_reason: return self._close_reason if self._browser: return self._browser._close_reason return None async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None ) -> Any: async with self.expect_event(event, predicate, timeout) as event_info: pass return await event_info def expect_console_message( self, predicate: Callable[[ConsoleMessage], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[ConsoleMessage]: return self.expect_event(Page.Events.Console, predicate, timeout) def expect_page( self, predicate: Callable[[Page], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[Page]: return self.expect_event(BrowserContext.Events.Page, predicate, timeout) def _on_service_worker(self, worker: Worker) -> None: worker._context = self self._service_workers.add(worker) self.emit(BrowserContext.Events.ServiceWorker, worker) def _on_request_failed( self, request: Request, response_end_timing: float, failure_text: Optional[str], page: Optional[Page], ) -> None: request._failure_text = failure_text request._set_response_end_timing(response_end_timing) self.emit(BrowserContext.Events.RequestFailed, request) if page: page.emit(Page.Events.RequestFailed, request) def _on_request_finished( self, request: Request, response: Optional[Response], response_end_timing: float, page: Optional[Page], ) -> None: request._set_response_end_timing(response_end_timing) self.emit(BrowserContext.Events.RequestFinished, request) if page: page.emit(Page.Events.RequestFinished, request) if response: response._finished_future.set_result(True) def _on_console_message(self, event: Dict) -> None: message = ConsoleMessage(event, self._loop, self._dispatcher_fiber) worker = message.worker if worker: worker.emit(Worker.Events.Console, message) page = message.page if page: page.emit(Page.Events.Console, message) self.emit(BrowserContext.Events.Console, message) def _on_dialog(self, dialog: Dialog) -> None: has_listeners = self.emit(BrowserContext.Events.Dialog, dialog) page = dialog.page if page: has_listeners = page.emit(Page.Events.Dialog, dialog) or has_listeners if not has_listeners: # Although we do similar handling on the server side, we still need this logic # on the client side due to a possible race condition between two async calls: # a) removing "dialog" listener subscription (client->server) # b) actual "dialog" event (server->client) if dialog.type == "beforeunload": asyncio.create_task(dialog.accept()) else: asyncio.create_task(dialog.dismiss()) def _on_page_error(self, error: Error, page: Optional[Page]) -> None: self.emit( BrowserContext.Events.WebError, WebError(self._loop, self._dispatcher_fiber, page, error), ) if page: page.emit(Page.Events.PageError, error) def _on_request(self, request: Request, page: Optional[Page]) -> None: self.emit(BrowserContext.Events.Request, request) if page: page.emit(Page.Events.Request, request) def _on_response(self, response: Response, page: Optional[Page]) -> None: self.emit(BrowserContext.Events.Response, response) if page: page.emit(Page.Events.Response, response) @property def background_pages(self) -> List[Page]: return [] @property def service_workers(self) -> List[Worker]: return list(self._service_workers) async def new_cdp_session(self, page: Union[Page, Frame]) -> CDPSession: page = to_impl(page) params = {} if isinstance(page, Page): params["page"] = page._channel elif isinstance(page, Frame): params["frame"] = page._channel else: raise Error("page: expected Page or Frame") return from_channel(await self._channel.send("newCDPSession", None, params)) @property def tracing(self) -> Tracing: return self._tracing @property def request(self) -> "APIRequestContext": return self._request @property def clock(self) -> Clock: return self._clock ================================================ FILE: playwright/_impl/_browser_type.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import json import pathlib import sys from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast from playwright._impl._api_structures import ( ClientCertificate, Geolocation, HttpCredentials, ProxySettings, ViewportSize, ) from playwright._impl._browser import Browser from playwright._impl._browser_context import BrowserContext from playwright._impl._connection import ChannelOwner, Connection, from_channel from playwright._impl._errors import Error from playwright._impl._helper import ( PLAYWRIGHT_MAX_DEADLINE, ColorScheme, Contrast, Env, ForcedColors, HarContentPolicy, HarMode, ReducedMotion, ServiceWorkersPolicy, TimeoutSettings, async_readfile, locals_to_params, ) from playwright._impl._json_pipe import JsonPipeTransport from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._waiter import throw_on_timeout if TYPE_CHECKING: from playwright._impl._playwright import Playwright class BrowserType(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._playwright: "Playwright" def __repr__(self) -> str: return f"" @property def name(self) -> str: return self._initializer["name"] @property def executable_path(self) -> str: return self._initializer["executablePath"] async def launch( self, executablePath: Union[str, Path] = None, channel: str = None, args: Sequence[str] = None, ignoreDefaultArgs: Union[bool, Sequence[str]] = None, handleSIGINT: bool = None, handleSIGTERM: bool = None, handleSIGHUP: bool = None, timeout: float = None, env: Env = None, headless: bool = None, proxy: ProxySettings = None, downloadsPath: Union[str, Path] = None, slowMo: float = None, tracesDir: Union[pathlib.Path, str] = None, chromiumSandbox: bool = None, firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None, ) -> Browser: params = locals_to_params(locals()) normalize_launch_params(params) browser = cast( Browser, from_channel( await self._channel.send( "launch", TimeoutSettings.launch_timeout, params ) ), ) browser._connect_to_browser_type( self, str(tracesDir) if tracesDir is not None else None ) return browser async def launch_persistent_context( self, userDataDir: Union[str, Path], channel: str = None, executablePath: Union[str, Path] = None, args: Sequence[str] = None, ignoreDefaultArgs: Union[bool, Sequence[str]] = None, handleSIGINT: bool = None, handleSIGTERM: bool = None, handleSIGHUP: bool = None, timeout: float = None, env: Env = None, headless: bool = None, proxy: ProxySettings = None, downloadsPath: Union[str, Path] = None, slowMo: float = None, viewport: ViewportSize = None, screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, bypassCSP: bool = None, userAgent: str = None, locale: str = None, timezoneId: str = None, geolocation: Geolocation = None, permissions: Sequence[str] = None, extraHTTPHeaders: Dict[str, str] = None, offline: bool = None, httpCredentials: HttpCredentials = None, deviceScaleFactor: float = None, isMobile: bool = None, hasTouch: bool = None, colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, contrast: Contrast = None, acceptDownloads: bool = None, tracesDir: Union[pathlib.Path, str] = None, chromiumSandbox: bool = None, firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None, recordHarPath: Union[Path, str] = None, recordHarOmitContent: bool = None, recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, baseURL: str = None, strictSelectors: bool = None, serviceWorkers: ServiceWorkersPolicy = None, recordHarUrlFilter: Union[Pattern[str], str] = None, recordHarMode: HarMode = None, recordHarContent: HarContentPolicy = None, clientCertificates: List[ClientCertificate] = None, ) -> BrowserContext: userDataDir = self._user_data_dir(userDataDir) params = locals_to_params(locals()) await self._prepare_browser_context_params(params) normalize_launch_params(params) result = await self._channel.send_return_as_dict( "launchPersistentContext", TimeoutSettings.launch_timeout, params ) browser = cast( Browser, from_channel(result["browser"]), ) browser._connect_to_browser_type( self, str(tracesDir) if tracesDir is not None else None ) context = cast(BrowserContext, from_channel(result["context"])) await context._initialize_har_from_options( record_har_content=recordHarContent, record_har_mode=recordHarMode, record_har_omit_content=recordHarOmitContent, record_har_path=recordHarPath, record_har_url_filter=recordHarUrlFilter, ) return context def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: if not userDataDir: return "" if not Path(userDataDir).is_absolute(): # Can be dropped once we drop Python 3.9 support (10/2025): # https://github.com/python/cpython/issues/82852 if sys.platform == "win32" and sys.version_info[:2] < (3, 10): return str(pathlib.Path.cwd() / userDataDir) return str(Path(userDataDir).resolve()) return str(Path(userDataDir)) async def connect_over_cdp( self, endpointURL: str, timeout: float = None, slowMo: float = None, headers: Dict[str, str] = None, isLocal: bool = None, ) -> Browser: params = locals_to_params(locals()) if params.get("headers"): params["headers"] = serialize_headers(params["headers"]) response = await self._channel.send_return_as_dict( "connectOverCDP", TimeoutSettings.launch_timeout, params ) browser = cast(Browser, from_channel(response["browser"])) browser._connect_to_browser_type(self, None) return browser async def connect( self, wsEndpoint: str, timeout: float = None, slowMo: float = None, headers: Dict[str, str] = None, exposeNetwork: str = None, ) -> Browser: if slowMo is None: slowMo = 0 headers = {**(headers if headers else {}), "x-playwright-browser": self.name} local_utils = self._connection.local_utils pipe_channel = ( await local_utils._channel.send_return_as_dict( "connect", None, { "wsEndpoint": wsEndpoint, "headers": headers, "slowMo": slowMo, "timeout": timeout if timeout is not None else 0, "exposeNetwork": exposeNetwork, }, ) )["pipe"] transport = JsonPipeTransport(self._connection._loop, pipe_channel) connection = Connection( self._connection._dispatcher_fiber, self._connection._object_factory, transport, self._connection._loop, local_utils=self._connection.local_utils, ) connection.mark_as_remote() browser = None def handle_transport_close(reason: Optional[str]) -> None: if browser: for context in browser.contexts: for page in context.pages: page._on_close() context._on_close() browser._on_close() connection.cleanup(reason) # TODO: Backport https://github.com/microsoft/playwright/commit/d8d5289e8692c9b1265d23ee66988d1ac5122f33 # Give a chance to any API call promises to reject upon page/context closure. # This happens naturally when we receive page.onClose and browser.onClose from the server # in separate tasks. However, upon pipe closure we used to dispatch them all synchronously # here and promises did not have a chance to reject. # The order of rejects vs closure is a part of the API contract and our test runner # relies on it to attribute rejections to the right test. transport.once("close", handle_transport_close) connection._is_sync = self._connection._is_sync connection._loop.create_task(connection.run()) playwright_future = connection.playwright_future timeout_future = throw_on_timeout( timeout if timeout is not None else PLAYWRIGHT_MAX_DEADLINE, Error("Connection timed out"), ) done, pending = await asyncio.wait( {transport.on_error_future, playwright_future, timeout_future}, return_when=asyncio.FIRST_COMPLETED, ) if not playwright_future.done(): playwright_future.cancel() if not timeout_future.done(): timeout_future.cancel() playwright: "Playwright" = next(iter(done)).result() playwright._set_selectors(self._playwright.selectors) self._connection._child_ws_connections.append(connection) pre_launched_browser = playwright._initializer.get("preLaunchedBrowser") assert pre_launched_browser browser = cast(Browser, from_channel(pre_launched_browser)) browser._should_close_connection_on_close = True browser._connect_to_browser_type(self, None) return browser async def _prepare_browser_context_params(self, params: Dict) -> None: if params.get("noViewport"): del params["noViewport"] params["noDefaultViewport"] = True if "defaultBrowserType" in params: del params["defaultBrowserType"] if "extraHTTPHeaders" in params: params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) if "recordVideoDir" in params: params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} if "recordVideoSize" in params: params["recordVideo"]["size"] = params["recordVideoSize"] del params["recordVideoSize"] del params["recordVideoDir"] if "storageState" in params: storageState = params["storageState"] if not isinstance(storageState, dict): params["storageState"] = json.loads( (await async_readfile(storageState)).decode() ) if params.get("colorScheme", None) == "null": params["colorScheme"] = "no-override" if params.get("reducedMotion", None) == "null": params["reducedMotion"] = "no-override" if params.get("forcedColors", None) == "null": params["forcedColors"] = "no-override" if params.get("contrast", None) == "null": params["contrast"] = "no-override" if "acceptDownloads" in params: params["acceptDownloads"] = ( "accept" if params["acceptDownloads"] else "deny" ) if "clientCertificates" in params: params["clientCertificates"] = await to_client_certificates_protocol( params["clientCertificates"] ) params["selectorEngines"] = self._playwright.selectors._selector_engines params["testIdAttributeName"] = ( self._playwright.selectors._test_id_attribute_name ) # Remove HAR options params.pop("recordHarPath", None) params.pop("recordHarOmitContent", None) params.pop("recordHarUrlFilter", None) params.pop("recordHarMode", None) params.pop("recordHarContent", None) def normalize_launch_params(params: Dict) -> None: if "env" in params: params["env"] = [ {"name": name, "value": str(value)} for [name, value] in params["env"].items() ] if "ignoreDefaultArgs" in params: if params["ignoreDefaultArgs"] is True: params["ignoreAllDefaultArgs"] = True del params["ignoreDefaultArgs"] if "executablePath" in params: params["executablePath"] = str(Path(params["executablePath"])) if "downloadsPath" in params: params["downloadsPath"] = str(Path(params["downloadsPath"])) if "tracesDir" in params: params["tracesDir"] = str(Path(params["tracesDir"])) ================================================ FILE: playwright/_impl/_cdp_session.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Any, Dict from playwright._impl._connection import ChannelOwner from playwright._impl._helper import locals_to_params class CDPSession(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._channel.on("event", lambda params: self._on_event(params)) def _on_event(self, params: Any) -> None: self.emit(params["method"], params.get("params")) async def send(self, method: str, params: Dict = None) -> Dict: return await self._channel.send("send", None, locals_to_params(locals())) async def detach(self) -> None: await self._channel.send( "detach", None, ) ================================================ FILE: playwright/_impl/_clock.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime from typing import TYPE_CHECKING, Dict, Union if TYPE_CHECKING: from playwright._impl._browser_context import BrowserContext class Clock: def __init__(self, browser_context: "BrowserContext") -> None: self._browser_context = browser_context self._loop = browser_context._loop self._dispatcher_fiber = browser_context._dispatcher_fiber async def install(self, time: Union[float, str, datetime.datetime] = None) -> None: await self._browser_context._channel.send( "clockInstall", None, parse_time(time) if time is not None else {}, ) async def fast_forward( self, ticks: Union[int, str], ) -> None: await self._browser_context._channel.send( "clockFastForward", None, parse_ticks(ticks), ) async def pause_at( self, time: Union[float, str, datetime.datetime], ) -> None: await self._browser_context._channel.send( "clockPauseAt", None, parse_time(time), ) async def resume( self, ) -> None: await self._browser_context._channel.send("clockResume", None) async def run_for( self, ticks: Union[int, str], ) -> None: await self._browser_context._channel.send( "clockRunFor", None, parse_ticks(ticks), ) async def set_fixed_time( self, time: Union[float, str, datetime.datetime], ) -> None: await self._browser_context._channel.send( "clockSetFixedTime", None, parse_time(time), ) async def set_system_time( self, time: Union[float, str, datetime.datetime], ) -> None: await self._browser_context._channel.send( "clockSetSystemTime", None, parse_time(time), ) def parse_time( time: Union[float, str, datetime.datetime], ) -> Dict[str, Union[int, str]]: if isinstance(time, (float, int)): return {"timeNumber": int(time * 1_000)} if isinstance(time, str): return {"timeString": time} return {"timeNumber": int(time.timestamp() * 1_000)} def parse_ticks(ticks: Union[int, str]) -> Dict[str, Union[int, str]]: if isinstance(ticks, int): return {"ticksNumber": ticks} return {"ticksString": ticks} ================================================ FILE: playwright/_impl/_connection.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import collections.abc import contextvars import datetime import inspect import sys import traceback from pathlib import Path from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, TypedDict, Union, cast, ) from pyee import EventEmitter from pyee.asyncio import AsyncIOEventEmitter import playwright import playwright._impl._impl_to_api_mapping from playwright._impl._errors import TargetClosedError, rewrite_error from playwright._impl._greenlets import EventGreenlet from playwright._impl._helper import Error, ParsedMessagePayload, parse_error from playwright._impl._transport import Transport if TYPE_CHECKING: from playwright._impl._local_utils import LocalUtils from playwright._impl._playwright import Playwright TimeoutCalculator = Optional[Callable[[Optional[float]], float]] class Channel(AsyncIOEventEmitter): def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: super().__init__() self._connection = connection self._guid = object._guid self._object = object self.on("error", lambda exc: self._connection._on_event_listener_error(exc)) async def send( self, method: str, timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, ) -> Any: return await self._connection.wrap_api_call( lambda: self._inner_send(method, timeout_calculator, params, False), is_internal, title, ) async def send_return_as_dict( self, method: str, timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, ) -> Any: return await self._connection.wrap_api_call( lambda: self._inner_send(method, timeout_calculator, params, True), is_internal, title, ) def send_no_reply( self, method: str, timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, ) -> None: # No reply messages are used to e.g. waitForEventInfo(after). self._connection.wrap_api_call_sync( lambda: self._connection._send_message_to_server( self._object, method, _augment_params(params, timeout_calculator), True, ), is_internal, title, ) async def _inner_send( self, method: str, timeout_calculator: TimeoutCalculator, params: Optional[Dict], return_as_dict: bool, ) -> Any: if self._connection._error: error = self._connection._error self._connection._error = None raise error callback = self._connection._send_message_to_server( self._object, method, _augment_params(params, timeout_calculator) ) done, _ = await asyncio.wait( { self._connection._transport.on_error_future, callback.future, }, return_when=asyncio.FIRST_COMPLETED, ) if not callback.future.done(): callback.future.cancel() result = next(iter(done)).result() # Protocol now has named return values, assume result is one level deeper unless # there is explicit ambiguity. if not result: return None assert isinstance(result, dict) if return_as_dict: return result if len(result) == 0: return None assert len(result) == 1 key = next(iter(result)) return result[key] class ChannelOwner(AsyncIOEventEmitter): def __init__( self, parent: Union["ChannelOwner", "Connection"], type: str, guid: str, initializer: Dict, ) -> None: super().__init__(loop=parent._loop) self._loop: asyncio.AbstractEventLoop = parent._loop self._dispatcher_fiber: Any = parent._dispatcher_fiber self._type = type self._guid: str = guid self._connection: Connection = ( parent._connection if isinstance(parent, ChannelOwner) else parent ) self._parent: Optional[ChannelOwner] = ( parent if isinstance(parent, ChannelOwner) else None ) self._objects: Dict[str, "ChannelOwner"] = {} self._channel: Channel = Channel(self._connection, self) self._initializer = initializer self._was_collected = False self._connection._objects[guid] = self if self._parent: self._parent._objects[guid] = self self._event_to_subscription_mapping: Dict[str, str] = {} def _dispose(self, reason: Optional[str]) -> None: # Clean up from parent and connection. if self._parent: del self._parent._objects[self._guid] del self._connection._objects[self._guid] self._was_collected = reason == "gc" # Dispose all children. for object in list(self._objects.values()): object._dispose(reason) self._objects.clear() def _adopt(self, child: "ChannelOwner") -> None: del cast("ChannelOwner", child._parent)._objects[child._guid] self._objects[child._guid] = child child._parent = self def _set_event_to_subscription_mapping(self, mapping: Dict[str, str]) -> None: self._event_to_subscription_mapping = mapping def _update_subscription(self, event: str, enabled: bool) -> None: protocol_event = self._event_to_subscription_mapping.get(event) if protocol_event: self._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( "updateSubscription", None, {"event": protocol_event, "enabled": enabled}, ), True, ) def _add_event_handler(self, event: str, k: Any, v: Any) -> None: if not self.listeners(event): self._update_subscription(event, True) super()._add_event_handler(event, k, v) def remove_listener(self, event: str, f: Any) -> None: super().remove_listener(event, f) if not self.listeners(event): self._update_subscription(event, False) class ProtocolCallback: def __init__(self, loop: asyncio.AbstractEventLoop, no_reply: bool = False) -> None: self.stack_trace: traceback.StackSummary self.no_reply = no_reply self.future = loop.create_future() if no_reply: self.future.set_result(None) # The outer task can get cancelled by the user, this forwards the cancellation to the inner task. current_task = asyncio.current_task() def cb(task: asyncio.Task) -> None: if current_task: current_task.remove_done_callback(cb) if task.cancelled(): self.future.cancel() if current_task: current_task.add_done_callback(cb) self.future.add_done_callback( lambda _: ( current_task.remove_done_callback(cb) if current_task else None ) ) class RootChannelOwner(ChannelOwner): def __init__(self, connection: "Connection") -> None: super().__init__(connection, "Root", "", {}) async def initialize(self) -> "Playwright": return from_channel( await self._channel.send( "initialize", None, { "sdkLanguage": "python", }, ) ) class Connection(EventEmitter): def __init__( self, dispatcher_fiber: Any, object_factory: Callable[[ChannelOwner, str, str, Dict], ChannelOwner], transport: Transport, loop: asyncio.AbstractEventLoop, local_utils: Optional["LocalUtils"] = None, ) -> None: super().__init__() self._dispatcher_fiber = dispatcher_fiber self._transport = transport self._transport.on_message = lambda msg: self.dispatch(msg) self._waiting_for_object: Dict[str, Callable[[ChannelOwner], None]] = {} self._last_id = 0 self._objects: Dict[str, ChannelOwner] = {} self._callbacks: Dict[int, ProtocolCallback] = {} self._object_factory = object_factory self._is_sync = False self._child_ws_connections: List["Connection"] = [] self._loop = loop self.playwright_future: asyncio.Future["Playwright"] = loop.create_future() self._error: Optional[BaseException] = None self.is_remote = False self._init_task: Optional[asyncio.Task] = None self._api_zone: contextvars.ContextVar[Optional[ParsedStackTrace]] = ( contextvars.ContextVar("ApiZone", default=None) ) self._local_utils: Optional["LocalUtils"] = local_utils self._tracing_count = 0 self._closed_error: Optional[Exception] = None @property def local_utils(self) -> "LocalUtils": assert self._local_utils return self._local_utils def mark_as_remote(self) -> None: self.is_remote = True async def run_as_sync(self) -> None: self._is_sync = True await self.run() async def run(self) -> None: self._loop = asyncio.get_running_loop() self._root_object = RootChannelOwner(self) async def init() -> None: self.playwright_future.set_result(await self._root_object.initialize()) await self._transport.connect() self._init_task = self._loop.create_task(init()) await self._transport.run() def stop_sync(self) -> None: self._transport.request_stop() self._dispatcher_fiber.switch() self._loop.run_until_complete(self._transport.wait_until_stopped()) self.cleanup() async def stop_async(self) -> None: self._transport.request_stop() await self._transport.wait_until_stopped() self.cleanup() def cleanup(self, cause: str = None) -> None: self._closed_error = TargetClosedError(cause) if cause else TargetClosedError() if self._init_task and not self._init_task.done(): self._init_task.cancel() for ws_connection in self._child_ws_connections: ws_connection._transport.dispose() for callback in self._callbacks.values(): # To prevent 'Future exception was never retrieved' we ignore all callbacks that are no_reply. if callback.no_reply: continue if callback.future.cancelled(): continue callback.future.set_exception(self._closed_error) self._callbacks.clear() self.emit("close") def call_on_object_with_known_name( self, guid: str, callback: Callable[[ChannelOwner], None] ) -> None: self._waiting_for_object[guid] = callback def set_is_tracing(self, is_tracing: bool) -> None: if is_tracing: self._tracing_count += 1 else: self._tracing_count -= 1 def _send_message_to_server( self, object: ChannelOwner, method: str, params: Dict, no_reply: bool = False ) -> ProtocolCallback: if self._closed_error: raise self._closed_error if object._was_collected: raise Error( "The object has been collected to prevent unbounded heap growth." ) self._last_id += 1 id = self._last_id callback = ProtocolCallback(self._loop, no_reply=no_reply) task = asyncio.current_task(self._loop) callback.stack_trace = cast( traceback.StackSummary, getattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)), ) callback.no_reply = no_reply stack_trace_information = cast(ParsedStackTrace, self._api_zone.get()) frames = stack_trace_information.get("frames", []) location = ( { "file": frames[0]["file"], "line": frames[0]["line"], "column": frames[0]["column"], } if frames else None ) metadata = { "wallTime": int(datetime.datetime.now().timestamp() * 1000), "apiName": stack_trace_information["apiName"], "internal": not stack_trace_information["apiName"], } if location: metadata["location"] = location # type: ignore title = stack_trace_information["title"] if title: metadata["title"] = title message = { "id": id, "guid": object._guid, "method": method, "params": self._replace_channels_with_guids(params), "metadata": metadata, } if self._tracing_count > 0 and frames and object._guid != "localUtils": self.local_utils.add_stack_to_tracing_no_reply(id, frames) self._callbacks[id] = callback self._transport.send(message) return callback def dispatch(self, msg: ParsedMessagePayload) -> None: if self._closed_error: return id = msg.get("id") if id: callback = self._callbacks.pop(id) if callback.future.cancelled(): return # No reply messages are used to e.g. waitForEventInfo(after) which returns exceptions on page close. # To prevent 'Future exception was never retrieved' we just ignore such messages. if callback.no_reply: return error = msg.get("error") if error and not msg.get("result"): parsed_error = parse_error( error["error"], format_call_log(msg.get("log")) # type: ignore ) parsed_error._stack = "".join(callback.stack_trace.format()) callback.future.set_exception(parsed_error) else: result = self._replace_guids_with_channels(msg.get("result")) callback.future.set_result(result) return guid = msg["guid"] method = msg["method"] params = msg.get("params") if method == "__create__": assert params parent = self._objects[guid] self._create_remote_object( parent, params["type"], params["guid"], params["initializer"] ) return object = self._objects.get(guid) if not object: raise Exception(f'Cannot find object to "{method}": {guid}') if method == "__adopt__": child_guid = cast(Dict[str, str], params)["guid"] child = self._objects.get(child_guid) if not child: raise Exception(f"Unknown new child: {child_guid}") object._adopt(child) return if method == "__dispose__": assert isinstance(params, dict) self._objects[guid]._dispose(cast(Optional[str], params.get("reason"))) return object = self._objects[guid] should_replace_guids_with_channels = "jsonPipe@" not in guid try: if self._is_sync: for listener in object._channel.listeners(method): # Event handlers like route/locatorHandlerTriggered require us to perform async work. # In order to report their potential errors to the user, we need to catch it and store it in the connection def _done_callback(future: asyncio.Future) -> None: exc = future.exception() if exc: self._on_event_listener_error(exc) def _listener_with_error_handler_attached(params: Any) -> None: potential_future = listener(params) if asyncio.isfuture(potential_future): potential_future.add_done_callback(_done_callback) # Each event handler is a potentilly blocking context, create a fiber for each # and switch to them in order, until they block inside and pass control to each # other and then eventually back to dispatcher as listener functions return. g = EventGreenlet(_listener_with_error_handler_attached) if should_replace_guids_with_channels: g.switch(self._replace_guids_with_channels(params)) else: g.switch(params) else: if should_replace_guids_with_channels: object._channel.emit( method, self._replace_guids_with_channels(params) ) else: object._channel.emit(method, params) except BaseException as exc: self._on_event_listener_error(exc) def _on_event_listener_error(self, exc: BaseException) -> None: print("Error occurred in event listener", file=sys.stderr) traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr) # Save the error to throw at the next API call. This "replicates" unhandled rejection in Node.js. self._error = exc def _create_remote_object( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> ChannelOwner: initializer = self._replace_guids_with_channels(initializer) result = self._object_factory(parent, type, guid, initializer) if guid in self._waiting_for_object: self._waiting_for_object.pop(guid)(result) return result def _replace_channels_with_guids( self, payload: Any, ) -> Any: if payload is None: return payload if isinstance(payload, Path): return str(payload) if isinstance(payload, collections.abc.Sequence) and not isinstance( payload, str ): return list(map(self._replace_channels_with_guids, payload)) if isinstance(payload, Channel): return dict(guid=payload._guid) if isinstance(payload, dict): result = {} for key, value in payload.items(): result[key] = self._replace_channels_with_guids(value) return result return payload def _replace_guids_with_channels(self, payload: Any) -> Any: if payload is None: return payload if isinstance(payload, list): return list(map(self._replace_guids_with_channels, payload)) if isinstance(payload, dict): if payload.get("guid") in self._objects: return self._objects[payload["guid"]]._channel result = {} for key, value in payload.items(): result[key] = self._replace_guids_with_channels(value) return result return payload async def wrap_api_call( self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return await cb() task = asyncio.current_task(self._loop) st: List[inspect.FrameInfo] = getattr( task, "__pw_stack__", None ) or inspect.stack(0) parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return await cb() except Exception as error: raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None finally: self._api_zone.set(None) def wrap_api_call_sync( self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return cb() task = asyncio.current_task(self._loop) st: List[inspect.FrameInfo] = getattr( task, "__pw_stack__", None ) or inspect.stack(0) parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return cb() except Exception as error: raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None finally: self._api_zone.set(None) def from_channel(channel: Channel) -> Any: return channel._object def from_nullable_channel(channel: Optional[Channel]) -> Optional[Any]: return channel._object if channel else None class StackFrame(TypedDict): file: str line: int column: int function: Optional[str] class ParsedStackTrace(TypedDict): frames: List[StackFrame] apiName: Optional[str] title: Optional[str] def _extract_stack_trace_information_from_stack( st: List[inspect.FrameInfo], is_internal: bool, title: str = None ) -> ParsedStackTrace: playwright_module_path = str(Path(playwright.__file__).parents[0]) last_internal_api_name = "" api_name = "" parsed_frames: List[StackFrame] = [] for frame in st: # Sync and Async implementations can have event handlers. When these are sync, they # get evaluated in the context of the event loop, so they contain the stack trace of when # the message was received. _impl_to_api_mapping is glue between the user-code and internal # code to translate impl classes to api classes. We want to ignore these frames. if playwright._impl._impl_to_api_mapping.__file__ == frame.filename: continue is_playwright_internal = frame.filename.startswith(playwright_module_path) method_name = "" if "self" in frame[0].f_locals: method_name = frame[0].f_locals["self"].__class__.__name__ + "." method_name += frame[0].f_code.co_name if not is_playwright_internal: parsed_frames.append( { "file": frame.filename, "line": frame.lineno, "column": 0, "function": method_name, } ) if is_playwright_internal: last_internal_api_name = method_name elif last_internal_api_name: api_name = last_internal_api_name last_internal_api_name = "" if not api_name: api_name = last_internal_api_name return { "frames": parsed_frames, "apiName": "" if is_internal else api_name, "title": title, } def _augment_params( params: Optional[Dict], timeout_calculator: Optional[Callable[[Optional[float]], float]], ) -> Dict: if params is None: params = {} if timeout_calculator: params["timeout"] = timeout_calculator(params.get("timeout")) return _filter_none(params) def _filter_none(d: Mapping) -> Dict: result = {} for k, v in d.items(): if v is None: continue result[k] = _filter_none(v) if isinstance(v, dict) else v return result def format_call_log(log: Optional[List[str]]) -> str: if not log: return "" if len(list(filter(lambda x: x.strip(), log))) == 0: return "" return "\nCall log:\n" + "\n".join(log) + "\n" ================================================ FILE: playwright/_impl/_console_message.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from asyncio import AbstractEventLoop from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union from playwright._impl._api_structures import SourceLocation from playwright._impl._connection import from_channel, from_nullable_channel from playwright._impl._js_handle import JSHandle if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page from playwright._impl._worker import Worker class ConsoleMessage: def __init__( self, event: Dict, loop: AbstractEventLoop, dispatcher_fiber: Any ) -> None: self._event = event self._loop = loop self._dispatcher_fiber = dispatcher_fiber self._page: Optional["Page"] = from_nullable_channel(event.get("page")) self._worker: Optional["Worker"] = from_nullable_channel(event.get("worker")) def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.text @property def type(self) -> Union[ Literal["assert"], Literal["clear"], Literal["count"], Literal["debug"], Literal["dir"], Literal["dirxml"], Literal["endGroup"], Literal["error"], Literal["info"], Literal["log"], Literal["profile"], Literal["profileEnd"], Literal["startGroup"], Literal["startGroupCollapsed"], Literal["table"], Literal["time"], Literal["timeEnd"], Literal["trace"], Literal["warning"], ]: return self._event["type"] @property def text(self) -> str: return self._event["text"] @property def args(self) -> List[JSHandle]: return list(map(from_channel, self._event["args"])) @property def location(self) -> SourceLocation: return self._event["location"] @property def page(self) -> Optional["Page"]: return self._page @property def worker(self) -> Optional["Worker"]: return self._worker ================================================ FILE: playwright/_impl/_dialog.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import TYPE_CHECKING, Dict, Optional from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._helper import locals_to_params if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page class Dialog(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) def __repr__(self) -> str: return f"" @property def type(self) -> str: return self._initializer["type"] @property def message(self) -> str: return self._initializer["message"] @property def default_value(self) -> str: return self._initializer["defaultValue"] @property def page(self) -> Optional["Page"]: return self._page async def accept(self, promptText: str = None) -> None: await self._channel.send("accept", None, locals_to_params(locals())) async def dismiss(self) -> None: await self._channel.send( "dismiss", None, ) ================================================ FILE: playwright/_impl/_download.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib from pathlib import Path from typing import TYPE_CHECKING, Optional, Union from playwright._impl._artifact import Artifact if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page class Download: def __init__( self, page: "Page", url: str, suggested_filename: str, artifact: Artifact ) -> None: self._page = page self._loop = page._loop self._dispatcher_fiber = page._dispatcher_fiber self._url = url self._suggested_filename = suggested_filename self._artifact = artifact def __repr__(self) -> str: return f"" @property def page(self) -> "Page": return self._page @property def url(self) -> str: return self._url @property def suggested_filename(self) -> str: return self._suggested_filename async def delete(self) -> None: await self._artifact.delete() async def failure(self) -> Optional[str]: return await self._artifact.failure() async def path(self) -> pathlib.Path: return await self._artifact.path_after_finished() async def save_as(self, path: Union[str, Path]) -> None: await self._artifact.save_as(path) async def cancel(self) -> None: return await self._artifact.cancel() ================================================ FILE: playwright/_impl/_driver.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import os import sys from pathlib import Path from typing import Tuple import playwright from playwright._repo_version import version def compute_driver_executable() -> Tuple[str, str]: driver_path = Path(inspect.getfile(playwright)).parent / "driver" cli_path = str(driver_path / "package" / "cli.js") if sys.platform == "win32": return ( os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node.exe")), cli_path, ) return (os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node")), cli_path) def get_driver_env() -> dict: env = os.environ.copy() env["PW_LANG_NAME"] = "python" env["PW_LANG_NAME_VERSION"] = f"{sys.version_info.major}.{sys.version_info.minor}" env["PW_CLI_DISPLAY_VERSION"] = version return env ================================================ FILE: playwright/_impl/_element_handle.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import mimetypes from pathlib import Path from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Sequence, Union, cast, ) from playwright._impl._api_structures import FilePayload, FloatRect, Position from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._helper import ( Error, KeyboardModifier, MouseButton, async_writefile, locals_to_params, make_dirs_for_file, ) from playwright._impl._js_handle import ( JSHandle, Serializable, parse_result, serialize_argument, ) from playwright._impl._set_input_files_helpers import convert_input_files if TYPE_CHECKING: # pragma: no cover from playwright._impl._frame import Frame from playwright._impl._locator import Locator class ElementHandle(JSHandle): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._frame = cast("Frame", parent) async def _createSelectorForTest(self, name: str) -> Optional[str]: return await self._channel.send( "createSelectorForTest", self._frame._timeout, dict(name=name) ) def as_element(self) -> Optional["ElementHandle"]: return self async def owner_frame(self) -> Optional["Frame"]: return from_nullable_channel(await self._channel.send("ownerFrame", None)) async def content_frame(self) -> Optional["Frame"]: return from_nullable_channel(await self._channel.send("contentFrame", None)) async def get_attribute(self, name: str) -> Optional[str]: return await self._channel.send("getAttribute", None, dict(name=name)) async def text_content(self) -> Optional[str]: return await self._channel.send("textContent", None) async def inner_text(self) -> str: return await self._channel.send("innerText", None) async def inner_html(self) -> str: return await self._channel.send("innerHTML", None) async def is_checked(self) -> bool: return await self._channel.send("isChecked", None) async def is_disabled(self) -> bool: return await self._channel.send("isDisabled", None) async def is_editable(self) -> bool: return await self._channel.send("isEditable", None) async def is_enabled(self) -> bool: return await self._channel.send("isEnabled", None) async def is_hidden(self) -> bool: return await self._channel.send("isHidden", None) async def is_visible(self) -> bool: return await self._channel.send("isVisible", None) async def dispatch_event(self, type: str, eventInit: Dict = None) -> None: await self._channel.send( "dispatchEvent", None, dict(type=type, eventInit=serialize_argument(eventInit)), ) async def scroll_into_view_if_needed(self, timeout: float = None) -> None: await self._channel.send( "scrollIntoViewIfNeeded", self._frame._timeout, locals_to_params(locals()) ) async def hover( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, trial: bool = None, ) -> None: await self._channel.send( "hover", self._frame._timeout, locals_to_params(locals()) ) async def click( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, clickCount: int = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, steps: int = None, ) -> None: await self._channel.send( "click", self._frame._timeout, locals_to_params(locals()) ) async def dblclick( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, steps: int = None, ) -> None: await self._channel.send( "dblclick", self._frame._timeout, locals_to_params(locals()) ) async def select_option( self, value: Union[str, Sequence[str]] = None, index: Union[int, Sequence[int]] = None, label: Union[str, Sequence[str]] = None, element: Union["ElementHandle", Sequence["ElementHandle"]] = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, ) -> List[str]: params = locals_to_params( dict( timeout=timeout, force=force, **convert_select_option_values(value, index, label, element), ) ) return await self._channel.send("selectOption", self._frame._timeout, params) async def tap( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: await self._channel.send( "tap", self._frame._timeout, locals_to_params(locals()) ) async def fill( self, value: str, timeout: float = None, noWaitAfter: bool = None, force: bool = None, ) -> None: await self._channel.send( "fill", self._frame._timeout, locals_to_params(locals()) ) async def select_text(self, force: bool = None, timeout: float = None) -> None: await self._channel.send( "selectText", self._frame._timeout, locals_to_params(locals()) ) async def input_value(self, timeout: float = None) -> str: return await self._channel.send( "inputValue", self._frame._timeout, locals_to_params(locals()) ) async def set_input_files( self, files: Union[ str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] ], timeout: float = None, noWaitAfter: bool = None, ) -> None: frame = await self.owner_frame() if not frame: raise Error("Cannot set input files to detached element") converted = await convert_input_files(files, frame.page.context) await self._channel.send( "setInputFiles", self._frame._timeout, { "timeout": timeout, **converted, }, ) async def focus(self) -> None: await self._channel.send("focus", None) async def type( self, text: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: await self._channel.send( "type", self._frame._timeout, locals_to_params(locals()) ) async def press( self, key: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: await self._channel.send( "press", self._frame._timeout, locals_to_params(locals()) ) async def set_checked( self, checked: bool, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: if checked: await self.check( position=position, timeout=timeout, force=force, trial=trial, ) else: await self.uncheck( position=position, timeout=timeout, force=force, trial=trial, ) async def check( self, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: await self._channel.send( "check", self._frame._timeout, locals_to_params(locals()) ) async def uncheck( self, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: await self._channel.send( "uncheck", self._frame._timeout, locals_to_params(locals()) ) async def bounding_box(self) -> Optional[FloatRect]: return await self._channel.send("boundingBox", None) async def screenshot( self, timeout: float = None, type: Literal["jpeg", "png"] = None, path: Union[str, Path] = None, quality: int = None, omitBackground: bool = None, animations: Literal["allow", "disabled"] = None, caret: Literal["hide", "initial"] = None, scale: Literal["css", "device"] = None, mask: Sequence["Locator"] = None, maskColor: str = None, style: str = None, ) -> bytes: params = locals_to_params(locals()) if "path" in params: if "type" not in params: params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( map( lambda locator: ( { "frame": locator._frame._channel, "selector": locator._selector, } ), params["mask"], ) ) encoded_binary = await self._channel.send( "screenshot", self._frame._timeout, params ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) await async_writefile(path, decoded_binary) return decoded_binary async def query_selector(self, selector: str) -> Optional["ElementHandle"]: return from_nullable_channel( await self._channel.send("querySelector", None, dict(selector=selector)) ) async def query_selector_all(self, selector: str) -> List["ElementHandle"]: return list( map( cast(Callable[[Any], Any], from_nullable_channel), await self._channel.send( "querySelectorAll", None, dict(selector=selector) ), ) ) async def eval_on_selector( self, selector: str, expression: str, arg: Serializable = None, ) -> Any: return parse_result( await self._channel.send( "evalOnSelector", None, dict( selector=selector, expression=expression, arg=serialize_argument(arg), ), ) ) async def eval_on_selector_all( self, selector: str, expression: str, arg: Serializable = None, ) -> Any: return parse_result( await self._channel.send( "evalOnSelectorAll", None, dict( selector=selector, expression=expression, arg=serialize_argument(arg), ), ) ) async def wait_for_element_state( self, state: Literal[ "disabled", "editable", "enabled", "hidden", "stable", "visible" ], timeout: float = None, ) -> None: await self._channel.send( "waitForElementState", self._frame._timeout, locals_to_params(locals()) ) async def wait_for_selector( self, selector: str, state: Literal["attached", "detached", "hidden", "visible"] = None, timeout: float = None, strict: bool = None, ) -> Optional["ElementHandle"]: return from_nullable_channel( await self._channel.send( "waitForSelector", self._frame._timeout, locals_to_params(locals()) ) ) def convert_select_option_values( value: Union[str, Sequence[str]] = None, index: Union[int, Sequence[int]] = None, label: Union[str, Sequence[str]] = None, element: Union["ElementHandle", Sequence["ElementHandle"]] = None, ) -> Any: if value is None and index is None and label is None and element is None: return {} options: Any = None elements: Any = None if value is not None: if isinstance(value, str): value = [value] options = (options or []) + list(map(lambda e: dict(valueOrLabel=e), value)) if index is not None: if isinstance(index, int): index = [index] options = (options or []) + list(map(lambda e: dict(index=e), index)) if label is not None: if isinstance(label, str): label = [label] options = (options or []) + list(map(lambda e: dict(label=e), label)) if element: if isinstance(element, ElementHandle): element = [element] elements = list(map(lambda e: e._channel, element)) return dict(options=options, elements=elements) def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png"]: mime_type, _ = mimetypes.guess_type(path) if mime_type == "image/png": return "png" if mime_type == "image/jpeg": return "jpeg" raise Error(f'Unsupported screenshot mime type for path "{path}": {mime_type}') ================================================ FILE: playwright/_impl/_errors.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # These are types that we use in the API. They are public and are a part of the # stable API. from typing import Optional def is_target_closed_error(error: Exception) -> bool: return isinstance(error, TargetClosedError) class Error(Exception): def __init__(self, message: str) -> None: self._message = message self._name: Optional[str] = None self._stack: Optional[str] = None super().__init__(message) @property def message(self) -> str: return self._message @property def name(self) -> Optional[str]: return self._name @property def stack(self) -> Optional[str]: return self._stack class TimeoutError(Error): pass class TargetClosedError(Error): def __init__(self, message: str = None) -> None: super().__init__(message or "Target page, context or browser has been closed") def rewrite_error(error: Exception, message: str) -> Exception: rewritten_exc = type(error)(message) if isinstance(rewritten_exc, Error) and isinstance(error, Error): rewritten_exc._name = error.name rewritten_exc._stack = error.stack return rewritten_exc ================================================ FILE: playwright/_impl/_event_context_manager.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from typing import Any, Generic, TypeVar T = TypeVar("T") class EventContextManagerImpl(Generic[T]): def __init__(self, future: asyncio.Future) -> None: self._future: asyncio.Future = future @property def future(self) -> asyncio.Future: return self._future async def __aenter__(self) -> asyncio.Future: return self._future async def __aexit__(self, *args: Any) -> None: await self._future ================================================ FILE: playwright/_impl/_fetch.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import json import pathlib import typing from pathlib import Path from typing import Any, Dict, List, Optional, Union, cast import playwright._impl._network as network from playwright._impl._api_structures import ( ClientCertificate, FilePayload, FormField, Headers, HttpCredentials, ProxySettings, ServerFilePayload, StorageState, ) from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._errors import is_target_closed_error from playwright._impl._helper import ( Error, NameValue, TargetClosedError, TimeoutSettings, async_readfile, async_writefile, is_file_payload, locals_to_params, object_to_array, to_impl, ) from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._tracing import Tracing if typing.TYPE_CHECKING: from playwright._impl._playwright import Playwright FormType = Dict[str, Union[bool, float, str]] DataType = Union[Any, bytes, str] MultipartType = Dict[str, Union[bytes, bool, float, str, FilePayload]] ParamsType = Union[Dict[str, Union[bool, float, str]], str] class APIRequest: def __init__(self, playwright: "Playwright") -> None: self.playwright = playwright self._loop = playwright._loop self._dispatcher_fiber = playwright._connection._dispatcher_fiber async def new_context( self, baseURL: str = None, extraHTTPHeaders: Dict[str, str] = None, httpCredentials: HttpCredentials = None, ignoreHTTPSErrors: bool = None, proxy: ProxySettings = None, userAgent: str = None, timeout: float = None, storageState: Union[StorageState, str, Path] = None, clientCertificates: List[ClientCertificate] = None, failOnStatusCode: bool = None, maxRedirects: int = None, ) -> "APIRequestContext": params = locals_to_params(locals()) if "storageState" in params: storage_state = params["storageState"] if not isinstance(storage_state, dict) and storage_state: params["storageState"] = json.loads( (await async_readfile(storage_state)).decode() ) if "extraHTTPHeaders" in params: params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) params["clientCertificates"] = await to_client_certificates_protocol( params.get("clientCertificates") ) context = cast( APIRequestContext, from_channel( await self.playwright._channel.send("newRequest", None, params) ), ) context._timeout_settings.set_default_timeout(timeout) return context class APIRequestContext(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._tracing: Tracing = from_channel(initializer["tracing"]) self._close_reason: Optional[str] = None self._timeout_settings = TimeoutSettings(None) async def dispose(self, reason: str = None) -> None: self._close_reason = reason try: await self._channel.send("dispose", None, {"reason": reason}) except Error as e: if is_target_closed_error(e): return raise e self._tracing._reset_stack_counter() async def delete( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: MultipartType = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="DELETE", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def head( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: MultipartType = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="HEAD", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def get( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: MultipartType = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="GET", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def patch( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="PATCH", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def put( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="PUT", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def post( self, url: str, params: ParamsType = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": return await self.fetch( url, method="POST", params=params, headers=headers, data=data, form=form, multipart=multipart, timeout=timeout, failOnStatusCode=failOnStatusCode, ignoreHTTPSErrors=ignoreHTTPSErrors, maxRedirects=maxRedirects, maxRetries=maxRetries, ) async def fetch( self, urlOrRequest: Union[str, network.Request], params: ParamsType = None, method: str = None, headers: Headers = None, data: DataType = None, form: FormType = None, multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": url = urlOrRequest if isinstance(urlOrRequest, str) else None request = ( cast(network.Request, to_impl(urlOrRequest)) if isinstance(to_impl(urlOrRequest), network.Request) else None ) assert request or isinstance( urlOrRequest, str ), "First argument must be either URL string or Request" return await self._inner_fetch( request, url, method, headers, data, params, form, multipart, timeout, failOnStatusCode, ignoreHTTPSErrors, maxRedirects, maxRetries, ) async def _inner_fetch( self, request: Optional[network.Request], url: Optional[str], method: str = None, headers: Headers = None, data: DataType = None, params: ParamsType = None, form: FormType = None, multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None, timeout: float = None, failOnStatusCode: bool = None, ignoreHTTPSErrors: bool = None, maxRedirects: int = None, maxRetries: int = None, ) -> "APIResponse": if self._close_reason: raise TargetClosedError(self._close_reason) assert ( (1 if data else 0) + (1 if form else 0) + (1 if multipart else 0) ) <= 1, "Only one of 'data', 'form' or 'multipart' can be specified" assert ( maxRedirects is None or maxRedirects >= 0 ), "'max_redirects' must be greater than or equal to '0'" assert ( maxRetries is None or maxRetries >= 0 ), "'max_retries' must be greater than or equal to '0'" url = url or (request.url if request else url) method = method or (request.method if request else "GET") # Cannot call allHeaders() here as the request may be paused inside route handler. headers_obj = headers or (request.headers if request else None) serialized_headers = serialize_headers(headers_obj) if headers_obj else None json_data: Any = None form_data: Optional[List[NameValue]] = None multipart_data: Optional[List[FormField]] = None post_data_buffer: Optional[bytes] = None if data is not None: if isinstance(data, str): if is_json_content_type(serialized_headers): json_data = data if is_json_parsable(data) else json.dumps(data) else: post_data_buffer = data.encode() elif isinstance(data, bytes): post_data_buffer = data elif isinstance(data, (dict, list, int, bool)): json_data = json.dumps(data) else: raise Error(f"Unsupported 'data' type: {type(data)}") elif form: form_data = object_to_array(form) elif multipart: multipart_data = [] # Convert file-like values to ServerFilePayload structs. for name, value in multipart.items(): if is_file_payload(value): payload = cast(FilePayload, value) assert isinstance( payload["buffer"], bytes ), f"Unexpected buffer type of 'data.{name}'" multipart_data.append( FormField(name=name, file=file_payload_to_json(payload)) ) elif isinstance(value, str): multipart_data.append(FormField(name=name, value=value)) if ( post_data_buffer is None and json_data is None and form_data is None and multipart_data is None ): post_data_buffer = request.post_data_buffer if request else None post_data = ( base64.b64encode(post_data_buffer).decode() if post_data_buffer else None ) response = await self._channel.send( "fetch", self._timeout_settings.timeout, { "url": url, "timeout": timeout, "params": object_to_array(params) if isinstance(params, dict) else None, "encodedParams": params if isinstance(params, str) else None, "method": method, "headers": serialized_headers, "postData": post_data, "jsonData": json_data, "formData": form_data, "multipartData": multipart_data, "failOnStatusCode": failOnStatusCode, "ignoreHTTPSErrors": ignoreHTTPSErrors, "maxRedirects": maxRedirects, "maxRetries": maxRetries, }, ) return APIResponse(self, response) async def storage_state( self, path: Union[pathlib.Path, str] = None, indexedDB: bool = None, ) -> StorageState: result = await self._channel.send_return_as_dict( "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) return result def file_payload_to_json(payload: FilePayload) -> ServerFilePayload: return ServerFilePayload( name=payload["name"], mimeType=payload["mimeType"], buffer=base64.b64encode(payload["buffer"]).decode(), ) class APIResponse: def __init__(self, context: APIRequestContext, initializer: Dict) -> None: self._loop = context._loop self._dispatcher_fiber = context._connection._dispatcher_fiber self._request = context self._initializer = initializer self._headers = network.RawHeaders(initializer["headers"]) def __repr__(self) -> str: return f"" @property def ok(self) -> bool: return self.status >= 200 and self.status <= 299 @property def url(self) -> str: return self._initializer["url"] @property def status(self) -> int: return self._initializer["status"] @property def status_text(self) -> str: return self._initializer["statusText"] @property def headers(self) -> Headers: return self._headers.headers() @property def headers_array(self) -> network.HeadersArray: return self._headers.headers_array() async def body(self) -> bytes: try: result = await self._request._connection.wrap_api_call( lambda: self._request._channel.send_return_as_dict( "fetchResponseBody", None, { "fetchUid": self._fetch_uid, }, ), True, ) if result is None: raise Error("Response has been disposed") return base64.b64decode(result["binary"]) except Error as exc: if is_target_closed_error(exc): raise Error("Response has been disposed") raise exc async def text(self) -> str: content = await self.body() return content.decode() async def json(self) -> Any: content = await self.text() return json.loads(content) async def dispose(self) -> None: await self._request._channel.send( "disposeAPIResponse", None, { "fetchUid": self._fetch_uid, }, ) @property def _fetch_uid(self) -> str: return self._initializer["fetchUid"] async def _fetch_log(self) -> List[str]: return await self._request._channel.send( "fetchLog", None, { "fetchUid": self._fetch_uid, }, ) def is_json_content_type(headers: network.HeadersArray = None) -> bool: if not headers: return False for header in headers: if header["name"] == "Content-Type": return header["value"].startswith("application/json") return False def is_json_parsable(value: Any) -> bool: if not isinstance(value, str): return False try: json.loads(value) return True except json.JSONDecodeError: return False ================================================ FILE: playwright/_impl/_file_chooser.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path from typing import TYPE_CHECKING, Sequence, Union from playwright._impl._api_structures import FilePayload if TYPE_CHECKING: # pragma: no cover from playwright._impl._element_handle import ElementHandle from playwright._impl._page import Page class FileChooser: def __init__( self, page: "Page", element_handle: "ElementHandle", is_multiple: bool ) -> None: self._page = page self._loop = page._loop self._dispatcher_fiber = page._dispatcher_fiber self._element_handle = element_handle self._is_multiple = is_multiple def __repr__(self) -> str: return f"" @property def page(self) -> "Page": return self._page @property def element(self) -> "ElementHandle": return self._element_handle def is_multiple(self) -> bool: return self._is_multiple async def set_files( self, files: Union[ str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] ], timeout: float = None, noWaitAfter: bool = None, ) -> None: await self._element_handle.set_input_files(files, timeout, noWaitAfter) ================================================ FILE: playwright/_impl/_frame.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from pathlib import Path from typing import ( TYPE_CHECKING, Any, Dict, List, Literal, Optional, Pattern, Sequence, Set, Union, cast, ) from pyee import EventEmitter from playwright._impl._api_structures import ( AriaRole, FilePayload, FrameExpectOptions, FrameExpectResult, Position, ) from playwright._impl._connection import ( ChannelOwner, from_channel, from_nullable_channel, ) from playwright._impl._element_handle import ElementHandle, convert_select_option_values from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._helper import ( DocumentLoadState, FrameNavigatedEvent, KeyboardModifier, MouseButton, TimeoutSettings, URLMatch, async_readfile, locals_to_params, monotonic_time, url_matches, ) from playwright._impl._js_handle import ( JSHandle, Serializable, add_source_url_to_script, parse_result, parse_value, serialize_argument, ) from playwright._impl._locator import ( FrameLocator, Locator, get_by_alt_text_selector, get_by_label_selector, get_by_placeholder_selector, get_by_role_selector, get_by_test_id_selector, get_by_text_selector, get_by_title_selector, test_id_attribute_name, ) from playwright._impl._network import Response from playwright._impl._set_input_files_helpers import convert_input_files from playwright._impl._waiter import Waiter if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page class Frame(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._parent_frame = from_nullable_channel(initializer.get("parentFrame")) if self._parent_frame: self._parent_frame._child_frames.append(self) self._name = initializer["name"] self._url = initializer["url"] self._detached = False self._child_frames: List[Frame] = [] self._page: Optional[Page] = None self._load_states: Set[str] = set(initializer["loadStates"]) self._event_emitter = EventEmitter() self._channel.on( "loadstate", lambda params: self._on_load_state(params.get("add"), params.get("remove")), ) self._channel.on( "navigated", lambda params: self._on_frame_navigated(params), ) def __repr__(self) -> str: return f"" def _on_load_state( self, add: DocumentLoadState = None, remove: DocumentLoadState = None ) -> None: if add: self._load_states.add(add) self._event_emitter.emit("loadstate", add) elif remove and remove in self._load_states: self._load_states.remove(remove) if not self._parent_frame and add == "load" and self._page: self._page.emit("load", self._page) if not self._parent_frame and add == "domcontentloaded" and self._page: self._page.emit("domcontentloaded", self._page) def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: self._url = event["url"] self._name = event["name"] self._event_emitter.emit("navigated", event) if "error" not in event and self._page: self._page.emit("framenavigated", self) async def _query_count(self, selector: str) -> int: return await self._channel.send("queryCount", None, {"selector": selector}) @property def page(self) -> "Page": assert self._page return self._page async def goto( self, url: str, timeout: float = None, waitUntil: DocumentLoadState = None, referer: str = None, ) -> Optional[Response]: return cast( Optional[Response], from_nullable_channel( await self._channel.send( "goto", self._navigation_timeout, locals_to_params(locals()) ) ), ) def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Waiter: assert self._page waiter = Waiter(self._page, f"frame.{wait_name}") waiter.reject_on_event( self._page, "close", lambda: cast("Page", self._page)._close_error_with_reason(), ) waiter.reject_on_event( self._page, "crash", Error("Navigation failed because page crashed!") ) waiter.reject_on_event( self._page, "framedetached", Error("Navigating frame was detached!"), lambda frame: frame == self, ) timeout = self._page._timeout_settings.navigation_timeout(timeout) waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") return waiter async def _expect( self, selector: Optional[str], expression: str, options: FrameExpectOptions, title: str = None, ) -> FrameExpectResult: if "expectedValue" in options: options["expectedValue"] = serialize_argument(options["expectedValue"]) result = await self._channel.send_return_as_dict( "expect", self._timeout, { "selector": selector, "expression": expression, **options, }, title=title, ) if result.get("received"): result["received"] = parse_value(result["received"]) return result def expect_navigation( self, url: URLMatch = None, waitUntil: DocumentLoadState = None, timeout: float = None, ) -> EventContextManagerImpl[Response]: assert self._page if not waitUntil: waitUntil = "load" if timeout is None: timeout = self._page._timeout_settings.navigation_timeout() deadline = monotonic_time() + timeout waiter = self._setup_navigation_waiter("expect_navigation", timeout) to_url = f' to "{url}"' if url else "" waiter.log(f"waiting for navigation{to_url} until '{waitUntil}'") def predicate(event: Any) -> bool: # Any failed navigation results in a rejection. if event.get("error"): return True waiter.log(f' navigated to "{event["url"]}"') return url_matches( cast("Page", self._page)._browser_context._base_url, event["url"], url, ) waiter.wait_for_event( self._event_emitter, "navigated", predicate=predicate, ) async def continuation() -> Optional[Response]: event = await waiter.result() if "error" in event: raise Error(event["error"]) if waitUntil not in self._load_states: t = deadline - monotonic_time() if t > 0: await self._wait_for_load_state_impl(state=waitUntil, timeout=t) if "newDocument" in event and "request" in event["newDocument"]: request = from_channel(event["newDocument"]["request"]) return await request.response() return None return EventContextManagerImpl(asyncio.create_task(continuation())) async def wait_for_url( self, url: URLMatch, waitUntil: DocumentLoadState = None, timeout: float = None, ) -> None: assert self._page if url_matches(self._page._browser_context._base_url, self.url, url): await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) return async with self.expect_navigation( url=url, waitUntil=waitUntil, timeout=timeout ): pass async def wait_for_load_state( self, state: Literal["domcontentloaded", "load", "networkidle"] = None, timeout: float = None, ) -> None: return await self._wait_for_load_state_impl(state, timeout) async def _wait_for_load_state_impl( self, state: DocumentLoadState = None, timeout: float = None ) -> None: if not state: state = "load" if state not in ("load", "domcontentloaded", "networkidle", "commit"): raise Error( "state: expected one of (load|domcontentloaded|networkidle|commit)" ) waiter = self._setup_navigation_waiter("wait_for_load_state", timeout) if state in self._load_states: waiter.log(f' not waiting, "{state}" event already fired') # TODO: align with upstream waiter._fulfill(None) else: def handle_load_state_event(actual_state: str) -> bool: waiter.log(f'"{actual_state}" event fired') return actual_state == state waiter.wait_for_event( self._event_emitter, "loadstate", handle_load_state_event, ) await waiter.result() def _timeout(self, timeout: Optional[float]) -> float: timeout_settings = ( self._page._timeout_settings if self._page else TimeoutSettings(None) ) return timeout_settings.timeout(timeout) def _navigation_timeout(self, timeout: Optional[float]) -> float: timeout_settings = ( self._page._timeout_settings if self._page else TimeoutSettings(None) ) return timeout_settings.navigation_timeout(timeout) async def frame_element(self) -> ElementHandle: return from_channel(await self._channel.send("frameElement", None)) async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) async def evaluate_handle( self, expression: str, arg: Serializable = None ) -> JSHandle: return from_channel( await self._channel.send( "evaluateExpressionHandle", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) async def query_selector( self, selector: str, strict: bool = None ) -> Optional[ElementHandle]: return from_nullable_channel( await self._channel.send("querySelector", None, locals_to_params(locals())) ) async def query_selector_all(self, selector: str) -> List[ElementHandle]: return list( map( from_channel, await self._channel.send( "querySelectorAll", None, dict(selector=selector) ), ) ) async def wait_for_selector( self, selector: str, strict: bool = None, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None, ) -> Optional[ElementHandle]: return from_nullable_channel( await self._channel.send( "waitForSelector", self._timeout, locals_to_params(locals()) ) ) async def is_checked( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._channel.send( "isChecked", self._timeout, locals_to_params(locals()) ) async def is_disabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._channel.send( "isDisabled", self._timeout, locals_to_params(locals()) ) async def is_editable( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._channel.send( "isEditable", self._timeout, locals_to_params(locals()) ) async def is_enabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._channel.send( "isEnabled", self._timeout, locals_to_params(locals()) ) async def is_hidden(self, selector: str, strict: bool = None) -> bool: return await self._channel.send( "isHidden", self._timeout, locals_to_params(locals()) ) async def is_visible(self, selector: str, strict: bool = None) -> bool: return await self._channel.send( "isVisible", self._timeout, locals_to_params(locals()) ) async def dispatch_event( self, selector: str, type: str, eventInit: Dict = None, strict: bool = None, timeout: float = None, ) -> None: await self._channel.send( "dispatchEvent", self._timeout, locals_to_params( dict( selector=selector, type=type, eventInit=serialize_argument(eventInit), strict=strict, timeout=timeout, ), ), ) async def eval_on_selector( self, selector: str, expression: str, arg: Serializable = None, strict: bool = None, ) -> Any: return parse_result( await self._channel.send( "evalOnSelector", None, locals_to_params( dict( selector=selector, expression=expression, arg=serialize_argument(arg), strict=strict, ) ), ) ) async def eval_on_selector_all( self, selector: str, expression: str, arg: Serializable = None, ) -> Any: return parse_result( await self._channel.send( "evalOnSelectorAll", None, dict( selector=selector, expression=expression, arg=serialize_argument(arg), ), ) ) async def content(self) -> str: return await self._channel.send("content", None) async def set_content( self, html: str, timeout: float = None, waitUntil: DocumentLoadState = None, ) -> None: await self._channel.send( "setContent", self._navigation_timeout, locals_to_params(locals()) ) @property def name(self) -> str: return self._name or "" @property def url(self) -> str: return self._url or "" @property def parent_frame(self) -> Optional["Frame"]: return self._parent_frame @property def child_frames(self) -> List["Frame"]: return self._child_frames.copy() def is_detached(self) -> bool: return self._detached async def add_script_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None, type: str = None, ) -> ElementHandle: params = locals_to_params(locals()) if path: params["content"] = add_source_url_to_script( (await async_readfile(path)).decode(), path ) del params["path"] return from_channel(await self._channel.send("addScriptTag", None, params)) async def add_style_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None ) -> ElementHandle: params = locals_to_params(locals()) if path: params["content"] = ( (await async_readfile(path)).decode() + "\n/*# sourceURL=" + str(Path(path)) + "*/" ) del params["path"] return from_channel(await self._channel.send("addStyleTag", None, params)) async def click( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, clickCount: int = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._click(**locals_to_params(locals())) async def _click( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, clickCount: int = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, steps: int = None, ) -> None: await self._channel.send("click", self._timeout, locals_to_params(locals())) async def dblclick( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._channel.send( "dblclick", self._timeout, locals_to_params(locals()), title="Double click" ) async def tap( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("tap", self._timeout, locals_to_params(locals())) async def fill( self, selector: str, value: str, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, force: bool = None, ) -> None: await self._fill(**locals_to_params(locals())) async def _fill( self, selector: str, value: str, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, force: bool = None, title: str = None, ) -> None: await self._channel.send("fill", self._timeout, locals_to_params(locals())) def locator( self, selector: str, hasText: Union[str, Pattern[str]] = None, hasNotText: Union[str, Pattern[str]] = None, has: Locator = None, hasNot: Locator = None, ) -> Locator: return Locator( self, selector, has_text=hasText, has_not_text=hasNotText, has=has, has_not=hasNot, ) def get_by_alt_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_alt_text_selector(text, exact=exact)) def get_by_label( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_label_selector(text, exact=exact)) def get_by_placeholder( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_placeholder_selector(text, exact=exact)) def get_by_role( self, role: AriaRole, checked: bool = None, disabled: bool = None, expanded: bool = None, includeHidden: bool = None, level: int = None, name: Union[str, Pattern[str]] = None, pressed: bool = None, selected: bool = None, exact: bool = None, ) -> "Locator": return self.locator( get_by_role_selector( role, checked=checked, disabled=disabled, expanded=expanded, includeHidden=includeHidden, level=level, name=name, pressed=pressed, selected=selected, exact=exact, ) ) def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) def get_by_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_text_selector(text, exact=exact)) def get_by_title( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_title_selector(text, exact=exact)) def frame_locator(self, selector: str) -> FrameLocator: return FrameLocator(self, selector) async def focus( self, selector: str, strict: bool = None, timeout: float = None ) -> None: await self._channel.send("focus", self._timeout, locals_to_params(locals())) async def text_content( self, selector: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._channel.send( "textContent", self._timeout, locals_to_params(locals()) ) async def inner_text( self, selector: str, strict: bool = None, timeout: float = None ) -> str: return await self._channel.send( "innerText", self._timeout, locals_to_params(locals()) ) async def inner_html( self, selector: str, strict: bool = None, timeout: float = None ) -> str: return await self._channel.send( "innerHTML", self._timeout, locals_to_params(locals()) ) async def get_attribute( self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._channel.send( "getAttribute", self._timeout, locals_to_params(locals()) ) async def hover( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("hover", self._timeout, locals_to_params(locals())) async def drag_and_drop( self, source: str, target: str, sourcePosition: Position = None, targetPosition: Position = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, timeout: float = None, trial: bool = None, steps: int = None, ) -> None: await self._channel.send( "dragAndDrop", self._timeout, locals_to_params(locals()) ) async def select_option( self, selector: str, value: Union[str, Sequence[str]] = None, index: Union[int, Sequence[int]] = None, label: Union[str, Sequence[str]] = None, element: Union["ElementHandle", Sequence["ElementHandle"]] = None, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, force: bool = None, ) -> List[str]: params = locals_to_params( dict( selector=selector, timeout=timeout, strict=strict, force=force, **convert_select_option_values(value, index, label, element), ) ) return await self._channel.send("selectOption", self._timeout, params) async def input_value( self, selector: str, strict: bool = None, timeout: float = None, ) -> str: return await self._channel.send( "inputValue", self._timeout, locals_to_params(locals()) ) async def set_input_files( self, selector: str, files: Union[ str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] ], strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: converted = await convert_input_files(files, self.page.context) await self._channel.send( "setInputFiles", self._timeout, { "selector": selector, "strict": strict, "timeout": self._timeout(timeout), **converted, }, ) async def type( self, selector: str, text: str, delay: float = None, strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: await self._channel.send("type", self._timeout, locals_to_params(locals())) async def press( self, selector: str, key: str, delay: float = None, strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: await self._channel.send("press", self._timeout, locals_to_params(locals())) async def check( self, selector: str, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("check", self._timeout, locals_to_params(locals())) async def uncheck( self, selector: str, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("uncheck", self._timeout, locals_to_params(locals())) async def wait_for_timeout(self, timeout: float) -> None: await self._channel.send("waitForTimeout", None, {"waitTimeout": timeout}) async def wait_for_function( self, expression: str, arg: Serializable = None, timeout: float = None, polling: Union[float, Literal["raf"]] = None, ) -> JSHandle: if isinstance(polling, str) and polling != "raf": raise Error(f"Unknown polling option: {polling}") params = locals_to_params(locals()) params["arg"] = serialize_argument(arg) if polling is not None and polling != "raf": params["pollingInterval"] = polling return from_channel( await self._channel.send("waitForFunction", self._timeout, params) ) async def title(self) -> str: return await self._channel.send("title", None) async def set_checked( self, selector: str, checked: bool, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: if checked: await self.check( selector=selector, position=position, timeout=timeout, force=force, strict=strict, trial=trial, ) else: await self.uncheck( selector=selector, position=position, timeout=timeout, force=force, strict=strict, trial=trial, ) async def _highlight(self, selector: str) -> None: await self._channel.send("highlight", None, {"selector": selector}) ================================================ FILE: playwright/_impl/_glob.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} def glob_to_regex_pattern(glob: str) -> str: tokens = ["^"] in_group = False i = 0 while i < len(glob): c = glob[i] if c == "\\" and i + 1 < len(glob): char = glob[i + 1] tokens.append("\\" + char if char in escaped_chars else char) i += 1 elif c == "*": char_before = glob[i - 1] if i > 0 else None star_count = 1 while i + 1 < len(glob) and glob[i + 1] == "*": star_count += 1 i += 1 if star_count > 1: char_after = glob[i + 1] if i + 1 < len(glob) else None if char_after == "/": if char_before == "/": tokens.append("((.+/)|)") else: tokens.append("(.*/)") i += 1 else: tokens.append("(.*)") else: tokens.append("([^/]*)") else: if c == "{": in_group = True tokens.append("(") elif c == "}": in_group = False tokens.append(")") elif c == ",": if in_group: tokens.append("|") else: tokens.append("\\" + c) else: tokens.append("\\" + c if c in escaped_chars else c) i += 1 tokens.append("$") return "".join(tokens) ================================================ FILE: playwright/_impl/_greenlets.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from typing import Tuple import greenlet def _greenlet_trace_callback( event: str, args: Tuple[greenlet.greenlet, greenlet.greenlet] ) -> None: if event in ("switch", "throw"): origin, target = args print(f"Transfer from {origin} to {target} with {event}") if os.environ.get("INTERNAL_PW_GREENLET_DEBUG"): greenlet.settrace(_greenlet_trace_callback) class MainGreenlet(greenlet.greenlet): def __str__(self) -> str: return "" class RouteGreenlet(greenlet.greenlet): def __str__(self) -> str: return "" class LocatorHandlerGreenlet(greenlet.greenlet): def __str__(self) -> str: return "" class EventGreenlet(greenlet.greenlet): def __str__(self) -> str: return "" ================================================ FILE: playwright/_impl/_har_router.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import base64 from typing import TYPE_CHECKING, Optional, cast from playwright._impl._api_structures import HeadersArray from playwright._impl._helper import ( HarLookupResult, RouteFromHarNotFoundPolicy, URLMatch, ) from playwright._impl._local_utils import LocalUtils if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext from playwright._impl._network import Route from playwright._impl._page import Page class HarRouter: def __init__( self, local_utils: LocalUtils, har_id: str, not_found_action: RouteFromHarNotFoundPolicy, url_matcher: Optional[URLMatch] = None, ) -> None: self._local_utils: LocalUtils = local_utils self._har_id: str = har_id self._not_found_action: RouteFromHarNotFoundPolicy = not_found_action self._options_url_match: Optional[URLMatch] = url_matcher @staticmethod async def create( local_utils: LocalUtils, file: str, not_found_action: RouteFromHarNotFoundPolicy, url_matcher: Optional[URLMatch] = None, ) -> "HarRouter": har_id = await local_utils._channel.send("harOpen", None, {"file": file}) return HarRouter( local_utils=local_utils, har_id=har_id, not_found_action=not_found_action, url_matcher=url_matcher, ) async def _handle(self, route: "Route") -> None: request = route.request response: HarLookupResult = await self._local_utils.har_lookup( harId=self._har_id, url=request.url, method=request.method, headers=await request.headers_array(), postData=request.post_data_buffer, isNavigationRequest=request.is_navigation_request(), ) action = response["action"] if action == "redirect": redirect_url = response["redirectURL"] assert redirect_url await route._redirected_navigation_request(redirect_url) return if action == "fulfill": # If the response status is -1, the request was canceled or stalled, so we just stall it here. # See https://github.com/microsoft/playwright/issues/29311. # TODO: it'd be better to abort such requests, but then we likely need to respect the timing, # because the request might have been stalled for a long time until the very end of the # test when HAR was recorded but we'd abort it immediately. if response.get("status") == -1: return body = response["body"] assert body is not None await route.fulfill( status=response.get("status"), headers={ v["name"]: v["value"] for v in cast(HeadersArray, response.get("headers", [])) }, body=base64.b64decode(body), ) return if action == "error": pass # Report the error, but fall through to the default handler. if self._not_found_action == "abort": await route.abort() return await route.fallback() async def add_context_route(self, context: "BrowserContext") -> None: await context.route( url=self._options_url_match or "**/*", handler=lambda route, _: asyncio.create_task(self._handle(route)), ) async def add_page_route(self, page: "Page") -> None: await page.route( url=self._options_url_match or "**/*", handler=lambda route, _: asyncio.create_task(self._handle(route)), ) def dispose(self) -> None: asyncio.create_task( self._local_utils._channel.send("harClose", None, {"harId": self._har_id}) ) ================================================ FILE: playwright/_impl/_helper.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import math import os import re import time import traceback from pathlib import Path from types import TracebackType from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Pattern, Set, Tuple, TypedDict, TypeVar, Union, cast, ) from urllib.parse import ParseResult, urljoin, urlparse, urlunparse from playwright._impl._api_structures import NameValue from playwright._impl._errors import ( Error, TargetClosedError, TimeoutError, is_target_closed_error, rewrite_error, ) from playwright._impl._glob import glob_to_regex_pattern from playwright._impl._greenlets import RouteGreenlet from playwright._impl._str_utils import escape_regex_flags if TYPE_CHECKING: # pragma: no cover from playwright._impl._api_structures import HeadersArray from playwright._impl._network import Request, Response, Route, WebSocketRoute URLMatch = Union[str, Pattern[str], Callable[[str], bool]] URLMatchRequest = Union[str, Pattern[str], Callable[["Request"], bool]] URLMatchResponse = Union[str, Pattern[str], Callable[["Response"], bool]] RouteHandlerCallback = Union[ Callable[["Route"], Any], Callable[["Route", "Request"], Any] ] WebSocketRouteHandlerCallback = Callable[["WebSocketRoute"], Any] ColorScheme = Literal["dark", "light", "no-preference", "null"] ForcedColors = Literal["active", "none", "null"] Contrast = Literal["more", "no-preference", "null"] ReducedMotion = Literal["no-preference", "null", "reduce"] DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"] KeyboardModifier = Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"] MouseButton = Literal["left", "middle", "right"] ServiceWorkersPolicy = Literal["allow", "block"] HarMode = Literal["full", "minimal"] HarContentPolicy = Literal["attach", "embed", "omit"] RouteFromHarNotFoundPolicy = Literal["abort", "fallback"] class ErrorPayload(TypedDict, total=False): message: str name: str stack: str value: Optional[Any] class HarRecordingMetadata(TypedDict, total=False): path: str content: Optional[HarContentPolicy] def prepare_record_har_options(params: Dict) -> Dict[str, Any]: out_params: Dict[str, Any] = {"path": str(params["recordHarPath"])} if "recordHarUrlFilter" in params: opt = params["recordHarUrlFilter"] if isinstance(opt, str): out_params["urlGlob"] = opt if isinstance(opt, Pattern): out_params["urlRegexSource"] = opt.pattern out_params["urlRegexFlags"] = escape_regex_flags(opt) del params["recordHarUrlFilter"] if "recordHarMode" in params: out_params["mode"] = params["recordHarMode"] del params["recordHarMode"] new_content_api = None old_content_api = None if "recordHarContent" in params: new_content_api = params["recordHarContent"] del params["recordHarContent"] if "recordHarOmitContent" in params: old_content_api = params["recordHarOmitContent"] del params["recordHarOmitContent"] content = new_content_api or ("omit" if old_content_api else None) if content: out_params["content"] = content return out_params class ParsedMessageParams(TypedDict): type: str guid: str initializer: Dict class ParsedMessagePayload(TypedDict, total=False): id: int guid: str method: str params: ParsedMessageParams result: Any error: ErrorPayload class Document(TypedDict): request: Optional[Any] class FrameNavigatedEvent(TypedDict): url: str name: str newDocument: Optional[Document] error: Optional[str] Env = Dict[str, Union[str, float, bool]] def url_matches( base_url: Optional[str], url_string: str, match: Optional[URLMatch], websocket_url: bool = None, ) -> bool: if not match: return True if isinstance(match, str): match = re.compile( resolve_glob_to_regex_pattern(base_url, match, websocket_url) ) if isinstance(match, Pattern): return bool(match.search(url_string)) return match(url_string) def resolve_glob_to_regex_pattern( base_url: Optional[str], glob: str, websocket_url: bool = None ) -> str: if websocket_url: base_url = to_websocket_base_url(base_url) glob = resolve_glob_base(base_url, glob) return glob_to_regex_pattern(glob) def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]: if base_url is not None and re.match(r"^https?://", base_url): base_url = re.sub(r"^http", "ws", base_url) return base_url def resolve_glob_base(base_url: Optional[str], match: str) -> str: if match[0] == "*": return match token_map: Dict[str, str] = {} def map_token(original: str, replacement: str) -> str: if len(original) == 0: return "" token_map[replacement] = original return replacement # Escaped `\\?` behaves the same as `?` in our glob patterns. match = match.replace(r"\\?", "?") # Special case about: URLs as they are not relative to base_url if ( match.startswith("about:") or match.startswith("data:") or match.startswith("chrome:") or match.startswith("edge:") or match.startswith("file:") ): # about: and data: URLs are not relative to base_url, so we return them as is. return match # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, # so we replace them with safe components first. processed_parts = [] for index, token in enumerate(match.split("/")): if token in (".", "..", ""): processed_parts.append(token) continue # Handle special case of http*://, note that the new schema has to be # a web schema so that slashes are properly inserted after domain. if index == 0 and token.endswith(":"): # Replace any pattern with http: if "*" in token or "{" in token: processed_parts.append(map_token(token, "http:")) else: # Preserve explicit schema as is as it may affect trailing slashes after domain. processed_parts.append(token) continue question_index = token.find("?") if question_index == -1: processed_parts.append(map_token(token, f"$_{index}_$")) else: new_prefix = map_token(token[:question_index], f"$_{index}_$") new_suffix = map_token(token[question_index:], f"?$_{index}_$") processed_parts.append(new_prefix + new_suffix) relative_path = "/".join(processed_parts) resolved, case_insensitive_part = resolve_base_url(base_url, relative_path) for token, original in token_map.items(): normalize = case_insensitive_part and token in case_insensitive_part resolved = resolved.replace( token, original.lower() if normalize else original, 1 ) return resolved def resolve_base_url( base_url: Optional[str], given_url: str ) -> Tuple[str, Optional[str]]: try: url = nodelike_urlparse( urljoin(base_url if base_url is not None else "", given_url) ) resolved = urlunparse(url) # Schema and domain are case-insensitive. hostname_port = ( url.hostname or "" ) # can't use parsed.netloc because it includes userinfo (username:password) if url.port: hostname_port += f":{url.port}" case_insensitive_prefix = f"{url.scheme}://{hostname_port}" return resolved, case_insensitive_prefix except Exception: return given_url, None def nodelike_urlparse(url: str) -> ParseResult: parsed = urlparse(url, allow_fragments=True) # https://url.spec.whatwg.org/#special-scheme is_special_url = parsed.scheme in ["http", "https", "ws", "wss", "ftp", "file"] if is_special_url: # special urls have a list path, list paths are serialized as follows: https://url.spec.whatwg.org/#url-path-serializer # urllib diverges, so we patch it here if parsed.path == "": parsed = parsed._replace(path="/") return parsed class HarLookupResult(TypedDict, total=False): action: Literal["error", "redirect", "fulfill", "noentry"] message: Optional[str] redirectURL: Optional[str] status: Optional[int] headers: Optional["HeadersArray"] body: Optional[str] DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1 class TimeoutSettings: @staticmethod def launch_timeout(timeout: Optional[float] = None) -> float: return ( timeout if timeout is not None else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS ) def __init__(self, parent: Optional["TimeoutSettings"]) -> None: self._parent = parent self._default_timeout: Optional[float] = None self._default_navigation_timeout: Optional[float] = None def set_default_timeout(self, timeout: Optional[float]) -> None: self._default_timeout = timeout def timeout(self, timeout: float = None) -> float: if timeout is not None: return timeout if self._default_timeout is not None: return self._default_timeout if self._parent: return self._parent.timeout() return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def set_default_navigation_timeout( self, navigation_timeout: Optional[float] ) -> None: self._default_navigation_timeout = navigation_timeout def default_navigation_timeout(self) -> Optional[float]: return self._default_navigation_timeout def default_timeout(self) -> Optional[float]: return self._default_timeout def navigation_timeout(self, timeout: float = None) -> float: if timeout is not None: return timeout if self._default_navigation_timeout is not None: return self._default_navigation_timeout if self._default_timeout is not None: return self._default_timeout if self._parent: return self._parent.navigation_timeout() return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: return ErrorPayload( message=str(ex), name="Error", stack="".join(traceback.format_tb(tb)) ) def parse_error(error: ErrorPayload, log: Optional[str] = None) -> Error: base_error_class = Error if error.get("name") == "TimeoutError": base_error_class = TimeoutError if error.get("name") == "TargetClosedError": base_error_class = TargetClosedError if not log: log = "" exc = base_error_class(patch_error_message(error["message"]) + log) exc._name = error["name"] exc._stack = error["stack"] return exc def patch_error_message(message: str) -> str: match = re.match(r"(\w+)(: expected .*)", message) if match: message = to_snake_case(match.group(1)) + match.group(2) message = message.replace( "Pass { acceptDownloads: true }", "Pass 'accept_downloads=True'" ) return message def locals_to_params(args: Dict) -> Dict: copy = {} for key in args: if key == "self": continue if args[key] is not None: copy[key] = ( args[key] if not isinstance(args[key], Dict) else locals_to_params(args[key]) ) return copy def monotonic_time() -> int: return math.floor(time.monotonic() * 1000) class RouteHandlerInvocation: complete: "asyncio.Future" route: "Route" def __init__(self, complete: "asyncio.Future", route: "Route") -> None: self.complete = complete self.route = route class RouteHandler: def __init__( self, base_url: Optional[str], url: URLMatch, handler: RouteHandlerCallback, is_sync: bool, times: Optional[int] = None, ): self._base_url = base_url self.url = url self.handler = handler self._times = times if times else math.inf self._handled_count = 0 self._is_sync = is_sync self._ignore_exception = False self._active_invocations: Set[RouteHandlerInvocation] = set() def matches(self, request_url: str) -> bool: return url_matches(self._base_url, request_url, self.url) async def handle(self, route: "Route") -> bool: handler_invocation = RouteHandlerInvocation( asyncio.get_running_loop().create_future(), route ) self._active_invocations.add(handler_invocation) try: return await self._handle_internal(route) except Exception as e: # If the handler was stopped (without waiting for completion), we ignore all exceptions. if self._ignore_exception: return False if is_target_closed_error(e): # We are failing in the handler because the target has closed. # Give user a hint! optional_async_prefix = "await " if not self._is_sync else "" raise rewrite_error( e, f"\"{str(e)}\" while running route callback.\nConsider awaiting `{optional_async_prefix}page.unroute_all(behavior='ignoreErrors')`\nbefore the end of the test to ignore remaining routes in flight.", ) raise e finally: handler_invocation.complete.set_result(None) self._active_invocations.remove(handler_invocation) async def _handle_internal(self, route: "Route") -> bool: handled_future = route._start_handling() self._handled_count += 1 if self._is_sync: handler_finished_future = route._loop.create_future() def _handler() -> None: try: self.handler(route, route.request) # type: ignore handler_finished_future.set_result(None) except Exception as e: handler_finished_future.set_exception(e) # As with event handlers, each route handler is a potentially blocking context # so it needs a fiber. g = RouteGreenlet(_handler) g.switch() await handler_finished_future else: coro_or_future = self.handler(route, route.request) # type: ignore if coro_or_future: # separate task so that we get a proper stack trace for exceptions / tracing api_name extraction await asyncio.ensure_future(coro_or_future) return await handled_future async def stop(self, behavior: Literal["ignoreErrors", "wait"]) -> None: # When a handler is manually unrouted or its page/context is closed we either # - wait for the current handler invocations to finish # - or do not wait, if the user opted out of it, but swallow all exceptions # that happen after the unroute/close. if behavior == "ignoreErrors": self._ignore_exception = True else: tasks = [] for activation in self._active_invocations: if not activation.route._did_throw: tasks.append(activation.complete) await asyncio.gather(*tasks) @property def will_expire(self) -> bool: return self._handled_count + 1 >= self._times @staticmethod def prepare_interception_patterns( handlers: List["RouteHandler"], ) -> List[Dict[str, str]]: patterns = [] all = False for handler in handlers: if isinstance(handler.url, str): patterns.append({"glob": handler.url}) elif isinstance(handler.url, re.Pattern): patterns.append( { "regexSource": handler.url.pattern, "regexFlags": escape_regex_flags(handler.url), } ) else: all = True if all: return [{"glob": "**/*"}] return patterns to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") def to_snake_case(name: str) -> str: return to_snake_case_regex.sub(r"_\1", name).lower() def make_dirs_for_file(path: Union[Path, str]) -> None: if not os.path.isabs(path): path = Path.cwd() / path os.makedirs(os.path.dirname(path), exist_ok=True) async def async_writefile(file: Union[str, Path], data: Union[str, bytes]) -> None: def inner() -> None: with open(file, "w" if isinstance(data, str) else "wb") as fh: fh.write(data) loop = asyncio.get_running_loop() await loop.run_in_executor(None, inner) async def async_readfile(file: Union[str, Path]) -> bytes: def inner() -> bytes: with open(file, "rb") as fh: return fh.read() loop = asyncio.get_running_loop() return await loop.run_in_executor(None, inner) T = TypeVar("T") def to_impl(obj: T) -> T: if hasattr(obj, "_impl_obj"): return cast(Any, obj)._impl_obj return obj def object_to_array(obj: Optional[Dict]) -> Optional[List[NameValue]]: if not obj: return None result = [] for key, value in obj.items(): result.append(NameValue(name=key, value=str(value))) return result def is_file_payload(value: Optional[Any]) -> bool: return ( isinstance(value, dict) and "name" in value and "mimeType" in value and "buffer" in value ) TEXTUAL_MIME_TYPE = re.compile( r"^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$" ) def is_textual_mime_type(mime_type: str) -> bool: return bool(TEXTUAL_MIME_TYPE.match(mime_type)) ================================================ FILE: playwright/_impl/_impl_to_api_mapping.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect from typing import Any, Callable, Dict, List, Optional, Sequence, Union from playwright._impl._errors import Error from playwright._impl._map import Map API_ATTR = "_pw_api_instance_" IMPL_ATTR = "_pw_impl_instance_" class ImplWrapper: def __init__(self, impl_obj: Any) -> None: self._impl_obj = impl_obj def __repr__(self) -> str: return self._impl_obj.__repr__() class ImplToApiMapping: def __init__(self) -> None: self._mapping: Dict[type, type] = {} def register(self, impl_class: type, api_class: type) -> None: self._mapping[impl_class] = api_class def from_maybe_impl( self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None ) -> Any: # Python does share default arguments between calls, so we need to # create a new map if it is not provided. if not visited: visited = Map() if not obj: return obj if isinstance(obj, dict): if obj in visited: return visited[obj] o: Dict = {} visited[obj] = o for name, value in obj.items(): o[name] = self.from_maybe_impl(value, visited) return o if isinstance(obj, list): if obj in visited: return visited[obj] a: List = [] visited[obj] = a for item in obj: a.append(self.from_maybe_impl(item, visited)) return a api_class = self._mapping.get(type(obj)) if api_class: api_instance = getattr(obj, API_ATTR, None) if not api_instance: api_instance = api_class(obj) setattr(obj, API_ATTR, api_instance) return api_instance else: return obj def from_impl(self, obj: Any) -> Any: assert obj result = self.from_maybe_impl(obj) assert result return result def from_impl_nullable(self, obj: Any = None) -> Optional[Any]: return self.from_impl(obj) if obj else None def from_impl_list(self, items: Sequence[Any]) -> List[Any]: return list(map(lambda a: self.from_impl(a), items)) def from_impl_dict(self, map: Dict[str, Any]) -> Dict[str, Any]: return {name: self.from_impl(value) for name, value in map.items()} def to_impl( self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None ) -> Any: if visited is None: visited = Map() try: if not obj: return obj if isinstance(obj, dict): if obj in visited: return visited[obj] o: Dict = {} visited[obj] = o for name, value in obj.items(): o[name] = self.to_impl(value, visited) return o if isinstance(obj, list): if obj in visited: return visited[obj] a: List = [] visited[obj] = a for item in obj: a.append(self.to_impl(item, visited)) return a if isinstance(obj, ImplWrapper): return obj._impl_obj return obj except RecursionError: raise Error("Maximum argument depth exceeded") def wrap_handler(self, handler: Callable[..., Any]) -> Callable[..., None]: def wrapper_func(*args: Any) -> Any: arg_count = len(inspect.signature(handler).parameters) return handler( *list(map(lambda a: self.from_maybe_impl(a), args))[:arg_count] ) if inspect.ismethod(handler): wrapper = getattr(handler.__self__, IMPL_ATTR + handler.__name__, None) if not wrapper: wrapper = wrapper_func setattr( handler.__self__, IMPL_ATTR + handler.__name__, wrapper, ) return wrapper wrapper = getattr(handler, IMPL_ATTR, None) if not wrapper: wrapper = wrapper_func setattr(handler, IMPL_ATTR, wrapper) return wrapper ================================================ FILE: playwright/_impl/_input.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from playwright._impl._connection import Channel from playwright._impl._helper import MouseButton, locals_to_params class Keyboard: def __init__(self, channel: Channel) -> None: self._channel = channel self._loop = channel._connection._loop self._dispatcher_fiber = channel._connection._dispatcher_fiber async def down(self, key: str) -> None: await self._channel.send("keyboardDown", None, locals_to_params(locals())) async def up(self, key: str) -> None: await self._channel.send("keyboardUp", None, locals_to_params(locals())) async def insert_text(self, text: str) -> None: await self._channel.send("keyboardInsertText", None, locals_to_params(locals())) async def type(self, text: str, delay: float = None) -> None: await self._channel.send("keyboardType", None, locals_to_params(locals())) async def press(self, key: str, delay: float = None) -> None: await self._channel.send("keyboardPress", None, locals_to_params(locals())) class Mouse: def __init__(self, channel: Channel) -> None: self._channel = channel self._loop = channel._connection._loop self._dispatcher_fiber = channel._connection._dispatcher_fiber async def move(self, x: float, y: float, steps: int = None) -> None: await self._channel.send("mouseMove", None, locals_to_params(locals())) async def down( self, button: MouseButton = None, clickCount: int = None, ) -> None: await self._channel.send("mouseDown", None, locals_to_params(locals())) async def up( self, button: MouseButton = None, clickCount: int = None, ) -> None: await self._channel.send("mouseUp", None, locals_to_params(locals())) async def _click( self, x: float, y: float, delay: float = None, button: MouseButton = None, clickCount: int = None, title: str = None, ) -> None: await self._channel.send( "mouseClick", None, locals_to_params(locals()), title=title ) async def click( self, x: float, y: float, delay: float = None, button: MouseButton = None, clickCount: int = None, ) -> None: params = locals() del params["self"] await self._click(**params) async def dblclick( self, x: float, y: float, delay: float = None, button: MouseButton = None, ) -> None: await self._click( x, y, delay=delay, button=button, clickCount=2, title="Double click" ) async def wheel(self, deltaX: float, deltaY: float) -> None: await self._channel.send("mouseWheel", None, locals_to_params(locals())) class Touchscreen: def __init__(self, channel: Channel) -> None: self._channel = channel self._loop = channel._connection._loop self._dispatcher_fiber = channel._connection._dispatcher_fiber async def tap(self, x: float, y: float) -> None: await self._channel.send("touchscreenTap", None, locals_to_params(locals())) ================================================ FILE: playwright/_impl/_js_handle.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import collections.abc import datetime import math import struct import traceback from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from urllib.parse import ParseResult, urlparse, urlunparse from playwright._impl._connection import Channel, ChannelOwner, from_channel from playwright._impl._errors import Error, is_target_closed_error from playwright._impl._map import Map if TYPE_CHECKING: # pragma: no cover from playwright._impl._element_handle import ElementHandle Serializable = Any class VisitorInfo: visited: Map[Any, int] last_id: int def __init__(self) -> None: self.visited = Map() self.last_id = 0 def visit(self, obj: Any) -> int: assert obj not in self.visited self.last_id += 1 self.visited[obj] = self.last_id return self.last_id class JSHandle(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._preview = self._initializer["preview"] self._channel.on( "previewUpdated", lambda params: self._on_preview_updated(params["preview"]) ) def __repr__(self) -> str: return f"" def __str__(self) -> str: return self._preview def _on_preview_updated(self, preview: str) -> None: self._preview = preview async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) async def evaluate_handle( self, expression: str, arg: Serializable = None ) -> "JSHandle": return from_channel( await self._channel.send( "evaluateExpressionHandle", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) async def get_property(self, propertyName: str) -> "JSHandle": return from_channel( await self._channel.send("getProperty", None, dict(name=propertyName)) ) async def get_properties(self) -> Dict[str, "JSHandle"]: return { prop["name"]: from_channel(prop["value"]) for prop in await self._channel.send( "getPropertyList", None, ) } def as_element(self) -> Optional["ElementHandle"]: return None async def dispose(self) -> None: try: await self._channel.send( "dispose", None, ) except Exception as e: if not is_target_closed_error(e): raise e async def json_value(self) -> Any: return parse_result( await self._channel.send( "jsonValue", None, ) ) def serialize_value( value: Any, handles: List[Channel], visitor_info: Optional[VisitorInfo] = None ) -> Any: if visitor_info is None: visitor_info = VisitorInfo() if isinstance(value, JSHandle): h = len(handles) handles.append(value._channel) return dict(h=h) if value is None: return dict(v="null") if isinstance(value, float): if value == float("inf"): return dict(v="Infinity") if value == float("-inf"): return dict(v="-Infinity") if value == float("-0"): return dict(v="-0") if math.isnan(value): return dict(v="NaN") if isinstance(value, datetime.datetime): # Node.js Date objects are always in UTC. return { "d": datetime.datetime.strftime( value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ" ) } if isinstance(value, Exception): return { "e": { "m": str(value), "n": ( (value.name or "") if isinstance(value, Error) else value.__class__.__name__ ), "s": ( (value.stack or "") if isinstance(value, Error) else "".join( traceback.format_exception(type(value), value=value, tb=None) ) ), } } if isinstance(value, bool): return {"b": value} if isinstance(value, (int, float)): return {"n": value} if isinstance(value, str): return {"s": value} if isinstance(value, ParseResult): return {"u": urlunparse(value)} if value in visitor_info.visited: return dict(ref=visitor_info.visited[value]) if isinstance(value, collections.abc.Sequence) and not isinstance(value, str): id = visitor_info.visit(value) a = [] for e in value: a.append(serialize_value(e, handles, visitor_info)) return dict(a=a, id=id) if isinstance(value, dict): id = visitor_info.visit(value) o = [] for name in value: o.append( {"k": name, "v": serialize_value(value[name], handles, visitor_info)} ) return dict(o=o, id=id) return dict(v="undefined") def serialize_argument(arg: Serializable = None) -> Any: handles: List[Channel] = [] value = serialize_value(arg, handles) return dict(value=value, handles=handles) def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: if refs is None: refs = {} if value is None: return None if isinstance(value, dict): if "ref" in value: return refs[value["ref"]] if "v" in value: v = value["v"] if v == "Infinity": return float("inf") if v == "-Infinity": return float("-inf") if v == "-0": return float("-0") if v == "NaN": return float("nan") if v == "undefined": return None if v == "null": return None return v if "u" in value: return urlparse(value["u"]) if "bi" in value: return int(value["bi"]) if "e" in value: error = Error(value["e"]["m"]) error._name = value["e"]["n"] error._stack = value["e"]["s"] return error if "a" in value: a: List = [] refs[value["id"]] = a for e in value["a"]: a.append(parse_value(e, refs)) return a if "d" in value: # Node.js Date objects are always in UTC. return datetime.datetime.strptime( value["d"], "%Y-%m-%dT%H:%M:%S.%fZ" ).replace(tzinfo=datetime.timezone.utc) if "o" in value: o: Dict = {} refs[value["id"]] = o for e in value["o"]: o[e["k"]] = parse_value(e["v"], refs) return o if "n" in value: return value["n"] if "s" in value: return value["s"] if "b" in value: return value["b"] if "ta" in value: encoded_bytes = value["ta"]["b"] decoded_bytes = base64.b64decode(encoded_bytes) array_type = value["ta"]["k"] if array_type == "i8": word_size = 1 fmt = "b" elif array_type == "ui8" or array_type == "ui8c": word_size = 1 fmt = "B" elif array_type == "i16": word_size = 2 fmt = "h" elif array_type == "ui16": word_size = 2 fmt = "H" elif array_type == "i32": word_size = 4 fmt = "i" elif array_type == "ui32": word_size = 4 fmt = "I" elif array_type == "f32": word_size = 4 fmt = "f" elif array_type == "f64": word_size = 8 fmt = "d" elif array_type == "bi64": word_size = 8 fmt = "q" elif array_type == "bui64": word_size = 8 fmt = "Q" else: raise ValueError(f"Unsupported array type: {array_type}") byte_len = len(decoded_bytes) if byte_len % word_size != 0: raise ValueError( f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" ) if byte_len == 0: return [] array_len = byte_len // word_size # "<" denotes little-endian format_string = f"<{array_len}{fmt}" return list(struct.unpack(format_string, decoded_bytes)) return value def parse_result(result: Any) -> Any: return parse_value(result) def add_source_url_to_script(source: str, path: Union[str, Path]) -> str: return source + "\n//# sourceURL=" + str(path).replace("\n", "") ================================================ FILE: playwright/_impl/_json_pipe.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from typing import Dict, Optional, cast from pyee.asyncio import AsyncIOEventEmitter from playwright._impl._connection import Channel from playwright._impl._errors import TargetClosedError from playwright._impl._helper import Error, ParsedMessagePayload from playwright._impl._transport import Transport class JsonPipeTransport(AsyncIOEventEmitter, Transport): def __init__( self, loop: asyncio.AbstractEventLoop, pipe_channel: Channel, ) -> None: super().__init__(loop) Transport.__init__(self, loop) self._stop_requested = False self._pipe_channel = pipe_channel def request_stop(self) -> None: self._stop_requested = True self._pipe_channel.send_no_reply("close", None, {}) def dispose(self) -> None: self.on_error_future.cancel() self._stopped_future.cancel() async def wait_until_stopped(self) -> None: await self._stopped_future async def connect(self) -> None: self._stopped_future: asyncio.Future = asyncio.Future() def handle_message(message: Dict) -> None: if self._stop_requested: return self.on_message(cast(ParsedMessagePayload, message)) def handle_closed(reason: Optional[str]) -> None: self.emit("close", reason) if reason: self.on_error_future.set_exception(TargetClosedError(reason)) self._stopped_future.set_result(None) self._pipe_channel.on( "message", lambda params: handle_message(params["message"]), ) self._pipe_channel.on( "closed", lambda params: handle_closed(params.get("reason")), ) async def run(self) -> None: await self._stopped_future def send(self, message: Dict) -> None: if self._stop_requested: raise Error("Playwright connection closed") self._pipe_channel.send_no_reply("send", None, {"message": message}) ================================================ FILE: playwright/_impl/_local_utils.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 from typing import Dict, List, Optional, cast from playwright._impl._api_structures import HeadersArray from playwright._impl._connection import ChannelOwner, StackFrame from playwright._impl._helper import HarLookupResult, locals_to_params class LocalUtils(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self.devices = { device["name"]: parse_device_descriptor(device["descriptor"]) for device in initializer["deviceDescriptors"] } async def zip(self, params: Dict) -> None: await self._channel.send("zip", None, params) async def har_open(self, file: str) -> None: params = locals_to_params(locals()) await self._channel.send("harOpen", None, params) async def har_lookup( self, harId: str, url: str, method: str, headers: HeadersArray, isNavigationRequest: bool, postData: Optional[bytes] = None, ) -> HarLookupResult: params = locals_to_params(locals()) if "postData" in params: params["postData"] = base64.b64encode(params["postData"]).decode() return cast( HarLookupResult, await self._channel.send_return_as_dict("harLookup", None, params), ) async def har_close(self, harId: str) -> None: params = locals_to_params(locals()) await self._channel.send("harClose", None, params) async def har_unzip(self, zipFile: str, harFile: str) -> None: params = locals_to_params(locals()) await self._channel.send("harUnzip", None, params) async def tracing_started(self, tracesDir: Optional[str], traceName: str) -> str: params = locals_to_params(locals()) return await self._channel.send("tracingStarted", None, params) async def trace_discarded(self, stacks_id: str) -> None: return await self._channel.send("traceDiscarded", None, {"stacksId": stacks_id}) def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None: self._channel.send_no_reply( "addStackToTracingNoReply", None, { "callData": { "stack": frames, "id": id, } }, ) def parse_device_descriptor(dict: Dict) -> Dict: return { "user_agent": dict["userAgent"], "viewport": dict["viewport"], "device_scale_factor": dict["deviceScaleFactor"], "is_mobile": dict["isMobile"], "has_touch": dict["hasTouch"], "default_browser_type": dict["defaultBrowserType"], } ================================================ FILE: playwright/_impl/_locator.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import pathlib import re from typing import ( TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Literal, Optional, Pattern, Sequence, Tuple, TypeVar, Union, ) from playwright._impl._api_structures import ( AriaRole, FilePayload, FloatRect, FrameExpectOptions, FrameExpectResult, Position, ) from playwright._impl._element_handle import ElementHandle from playwright._impl._helper import ( Error, KeyboardModifier, MouseButton, locals_to_params, monotonic_time, to_impl, ) from playwright._impl._js_handle import Serializable from playwright._impl._str_utils import ( escape_for_attribute_selector, escape_for_text_selector, ) if TYPE_CHECKING: # pragma: no cover from playwright._impl._frame import Frame from playwright._impl._js_handle import JSHandle from playwright._impl._page import Page T = TypeVar("T") class Locator: def __init__( self, frame: "Frame", selector: str, has_text: Union[str, Pattern[str]] = None, has_not_text: Union[str, Pattern[str]] = None, has: "Locator" = None, has_not: "Locator" = None, visible: bool = None, ) -> None: self._frame = frame self._selector = selector self._loop = frame._loop self._dispatcher_fiber = frame._connection._dispatcher_fiber if has_text: self._selector += f" >> internal:has-text={escape_for_text_selector(has_text, exact=False)}" if has: if has._frame != frame: raise Error('Inner "has" locator must belong to the same frame.') self._selector += " >> internal:has=" + json.dumps( has._selector, ensure_ascii=False ) if has_not_text: self._selector += f" >> internal:has-not-text={escape_for_text_selector(has_not_text, exact=False)}" if has_not: locator = has_not if locator._frame != frame: raise Error('Inner "has_not" locator must belong to the same frame.') self._selector += " >> internal:has-not=" + json.dumps(locator._selector) if visible is not None: self._selector += f" >> visible={bool_to_js_bool(visible)}" def __repr__(self) -> str: return f"" async def _with_element( self, task: Callable[[ElementHandle, float], Awaitable[T]], timeout: float = None, ) -> T: timeout = self._frame._timeout(timeout) deadline = (monotonic_time() + timeout) if timeout else 0 handle = await self.element_handle(timeout=timeout) if not handle: raise Error(f"Could not resolve {self._selector} to DOM Element") try: return await task( handle, (deadline - monotonic_time()) if deadline else 0, ) finally: await handle.dispose() def _equals(self, locator: "Locator") -> bool: return self._frame == locator._frame and self._selector == locator._selector @property def page(self) -> "Page": return self._frame.page async def bounding_box(self, timeout: float = None) -> Optional[FloatRect]: return await self._with_element( lambda h, _: h.bounding_box(), timeout, ) async def check( self, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.check(self._selector, strict=True, **params) async def click( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, clickCount: int = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, steps: int = None, ) -> None: params = locals_to_params(locals()) return await self._frame._click(self._selector, strict=True, **params) async def dblclick( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, steps: int = None, ) -> None: params = locals_to_params(locals()) return await self._frame.dblclick(self._selector, strict=True, **params) async def dispatch_event( self, type: str, eventInit: Dict = None, timeout: float = None, ) -> None: params = locals_to_params(locals()) return await self._frame.dispatch_event(self._selector, strict=True, **params) async def evaluate( self, expression: str, arg: Serializable = None, timeout: float = None ) -> Any: return await self._with_element( lambda h, _: h.evaluate(expression, arg), timeout, ) async def evaluate_all(self, expression: str, arg: Serializable = None) -> Any: params = locals_to_params(locals()) return await self._frame.eval_on_selector_all(self._selector, **params) async def evaluate_handle( self, expression: str, arg: Serializable = None, timeout: float = None ) -> "JSHandle": return await self._with_element( lambda h, _: h.evaluate_handle(expression, arg), timeout ) async def fill( self, value: str, timeout: float = None, noWaitAfter: bool = None, force: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.fill(self._selector, strict=True, **params) async def clear( self, timeout: float = None, noWaitAfter: bool = None, force: bool = None, ) -> None: params = locals_to_params(locals()) await self._frame._fill(self._selector, value="", title="Clear", **params) def locator( self, selectorOrLocator: Union[str, "Locator"], hasText: Union[str, Pattern[str]] = None, hasNotText: Union[str, Pattern[str]] = None, has: "Locator" = None, hasNot: "Locator" = None, ) -> "Locator": if isinstance(selectorOrLocator, str): return Locator( self._frame, f"{self._selector} >> {selectorOrLocator}", has_text=hasText, has_not_text=hasNotText, has_not=hasNot, has=has, ) selectorOrLocator = to_impl(selectorOrLocator) if selectorOrLocator._frame != self._frame: raise Error("Locators must belong to the same frame.") return Locator( self._frame, f"{self._selector} >> internal:chain={json.dumps(selectorOrLocator._selector)}", has_text=hasText, has_not_text=hasNotText, has_not=hasNot, has=has, ) def get_by_alt_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_alt_text_selector(text, exact=exact)) def get_by_label( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_label_selector(text, exact=exact)) def get_by_placeholder( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_placeholder_selector(text, exact=exact)) def get_by_role( self, role: AriaRole, checked: bool = None, disabled: bool = None, expanded: bool = None, includeHidden: bool = None, level: int = None, name: Union[str, Pattern[str]] = None, pressed: bool = None, selected: bool = None, exact: bool = None, ) -> "Locator": return self.locator( get_by_role_selector( role, checked=checked, disabled=disabled, expanded=expanded, includeHidden=includeHidden, level=level, name=name, pressed=pressed, selected=selected, exact=exact, ) ) def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) def get_by_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_text_selector(text, exact=exact)) def get_by_title( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_title_selector(text, exact=exact)) def frame_locator(self, selector: str) -> "FrameLocator": return FrameLocator(self._frame, self._selector + " >> " + selector) async def element_handle( self, timeout: float = None, ) -> ElementHandle: params = locals_to_params(locals()) handle = await self._frame.wait_for_selector( self._selector, strict=True, state="attached", **params ) assert handle return handle async def element_handles(self) -> List[ElementHandle]: return await self._frame.query_selector_all(self._selector) @property def first(self) -> "Locator": return Locator(self._frame, f"{self._selector} >> nth=0") @property def last(self) -> "Locator": return Locator(self._frame, f"{self._selector} >> nth=-1") def nth(self, index: int) -> "Locator": return Locator(self._frame, f"{self._selector} >> nth={index}") @property def content_frame(self) -> "FrameLocator": return FrameLocator(self._frame, self._selector) def describe(self, description: str) -> "Locator": return Locator( self._frame, f"{self._selector} >> internal:describe={json.dumps(description)}", ) @property def description(self) -> Optional[str]: try: match = re.search( r' >> internal:describe=("(?:[^"\\]|\\.)*")$', self._selector ) if match: description = json.loads(match.group(1)) if isinstance(description, str): return description except (json.JSONDecodeError, ValueError): pass return None def filter( self, hasText: Union[str, Pattern[str]] = None, hasNotText: Union[str, Pattern[str]] = None, has: "Locator" = None, hasNot: "Locator" = None, visible: bool = None, ) -> "Locator": return Locator( self._frame, self._selector, has_text=hasText, has_not_text=hasNotText, has=has, has_not=hasNot, visible=visible, ) def or_(self, locator: "Locator") -> "Locator": if locator._frame != self._frame: raise Error("Locators must belong to the same frame.") return Locator( self._frame, self._selector + " >> internal:or=" + json.dumps(locator._selector), ) def and_(self, locator: "Locator") -> "Locator": if locator._frame != self._frame: raise Error("Locators must belong to the same frame.") return Locator( self._frame, self._selector + " >> internal:and=" + json.dumps(locator._selector), ) async def focus(self, timeout: float = None) -> None: params = locals_to_params(locals()) return await self._frame.focus(self._selector, strict=True, **params) async def blur(self, timeout: float = None) -> None: await self._frame._channel.send( "blur", self._frame._timeout, { "selector": self._selector, "strict": True, **locals_to_params(locals()), }, ) async def all( self, ) -> List["Locator"]: result = [] for index in range(await self.count()): result.append(self.nth(index)) return result async def count( self, ) -> int: return await self._frame._query_count(self._selector) async def drag_to( self, target: "Locator", force: bool = None, noWaitAfter: bool = None, timeout: float = None, trial: bool = None, sourcePosition: Position = None, targetPosition: Position = None, steps: int = None, ) -> None: params = locals_to_params(locals()) del params["target"] return await self._frame.drag_and_drop( self._selector, target._selector, strict=True, **params ) async def get_attribute(self, name: str, timeout: float = None) -> Optional[str]: params = locals_to_params(locals()) return await self._frame.get_attribute( self._selector, strict=True, **params, ) async def hover( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, trial: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.hover( self._selector, strict=True, **params, ) async def inner_html(self, timeout: float = None) -> str: params = locals_to_params(locals()) return await self._frame.inner_html( self._selector, strict=True, **params, ) async def inner_text(self, timeout: float = None) -> str: params = locals_to_params(locals()) return await self._frame.inner_text( self._selector, strict=True, **params, ) async def input_value(self, timeout: float = None) -> str: params = locals_to_params(locals()) return await self._frame.input_value( self._selector, strict=True, **params, ) async def is_checked(self, timeout: float = None) -> bool: params = locals_to_params(locals()) return await self._frame.is_checked( self._selector, strict=True, **params, ) async def is_disabled(self, timeout: float = None) -> bool: params = locals_to_params(locals()) return await self._frame.is_disabled( self._selector, strict=True, **params, ) async def is_editable(self, timeout: float = None) -> bool: params = locals_to_params(locals()) return await self._frame.is_editable( self._selector, strict=True, **params, ) async def is_enabled(self, timeout: float = None) -> bool: params = locals_to_params(locals()) return await self._frame.is_enabled( self._selector, strict=True, **params, ) async def is_hidden(self, timeout: float = None) -> bool: # timeout is deprecated and does nothing return await self._frame.is_hidden( self._selector, strict=True, ) async def is_visible(self, timeout: float = None) -> bool: # timeout is deprecated and does nothing return await self._frame.is_visible( self._selector, strict=True, ) async def press( self, key: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.press(self._selector, strict=True, **params) async def screenshot( self, timeout: float = None, type: Literal["jpeg", "png"] = None, path: Union[str, pathlib.Path] = None, quality: int = None, omitBackground: bool = None, animations: Literal["allow", "disabled"] = None, caret: Literal["hide", "initial"] = None, scale: Literal["css", "device"] = None, mask: Sequence["Locator"] = None, maskColor: str = None, style: str = None, ) -> bytes: params = locals_to_params(locals()) return await self._with_element( lambda h, timeout: h.screenshot( **{**params, "timeout": timeout}, ), ) async def aria_snapshot(self, timeout: float = None) -> str: return await self._frame._channel.send( "ariaSnapshot", self._frame._timeout, { "selector": self._selector, **locals_to_params(locals()), }, ) async def scroll_into_view_if_needed( self, timeout: float = None, ) -> None: return await self._with_element( lambda h, timeout: h.scroll_into_view_if_needed(timeout=timeout), timeout, ) async def select_option( self, value: Union[str, Sequence[str]] = None, index: Union[int, Sequence[int]] = None, label: Union[str, Sequence[str]] = None, element: Union["ElementHandle", Sequence["ElementHandle"]] = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, ) -> List[str]: params = locals_to_params(locals()) return await self._frame.select_option( self._selector, strict=True, **params, ) async def select_text(self, force: bool = None, timeout: float = None) -> None: params = locals_to_params(locals()) return await self._with_element( lambda h, timeout: h.select_text(**{**params, "timeout": timeout}), timeout, ) async def set_input_files( self, files: Union[ str, pathlib.Path, FilePayload, Sequence[Union[str, pathlib.Path]], Sequence[FilePayload], ], timeout: float = None, noWaitAfter: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.set_input_files( self._selector, strict=True, **params, ) async def tap( self, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.tap( self._selector, strict=True, **params, ) async def text_content(self, timeout: float = None) -> Optional[str]: params = locals_to_params(locals()) return await self._frame.text_content( self._selector, strict=True, **params, ) async def type( self, text: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.type( self._selector, strict=True, **params, ) async def press_sequentially( self, text: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: await self.type(text, delay=delay, timeout=timeout) async def uncheck( self, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: params = locals_to_params(locals()) return await self._frame.uncheck( self._selector, strict=True, **params, ) async def all_inner_texts( self, ) -> List[str]: return await self._frame.eval_on_selector_all( self._selector, "ee => ee.map(e => e.innerText)" ) async def all_text_contents( self, ) -> List[str]: return await self._frame.eval_on_selector_all( self._selector, "ee => ee.map(e => e.textContent || '')" ) async def wait_for( self, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None, ) -> None: await self._frame.wait_for_selector( self._selector, strict=True, timeout=timeout, state=state ) async def set_checked( self, checked: bool, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, ) -> None: if checked: await self.check( position=position, timeout=timeout, force=force, trial=trial, ) else: await self.uncheck( position=position, timeout=timeout, force=force, trial=trial, ) async def _expect( self, expression: str, options: FrameExpectOptions, title: str = None, ) -> FrameExpectResult: return await self._frame._expect(self._selector, expression, options, title) async def highlight(self) -> None: await self._frame._highlight(self._selector) class FrameLocator: def __init__(self, frame: "Frame", frame_selector: str) -> None: self._frame = frame self._loop = frame._loop self._dispatcher_fiber = frame._connection._dispatcher_fiber self._frame_selector = frame_selector def locator( self, selectorOrLocator: Union["Locator", str], hasText: Union[str, Pattern[str]] = None, hasNotText: Union[str, Pattern[str]] = None, has: Locator = None, hasNot: Locator = None, ) -> Locator: if isinstance(selectorOrLocator, str): return Locator( self._frame, f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator}", has_text=hasText, has_not_text=hasNotText, has=has, has_not=hasNot, ) selectorOrLocator = to_impl(selectorOrLocator) if selectorOrLocator._frame != self._frame: raise ValueError("Locators must belong to the same frame.") return Locator( self._frame, f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator._selector}", has_text=hasText, has_not_text=hasNotText, has=has, has_not=hasNot, ) def get_by_alt_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_alt_text_selector(text, exact=exact)) def get_by_label( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_label_selector(text, exact=exact)) def get_by_placeholder( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_placeholder_selector(text, exact=exact)) def get_by_role( self, role: AriaRole, checked: bool = None, disabled: bool = None, expanded: bool = None, includeHidden: bool = None, level: int = None, name: Union[str, Pattern[str]] = None, pressed: bool = None, selected: bool = None, exact: bool = None, ) -> "Locator": return self.locator( get_by_role_selector( role, checked=checked, disabled=disabled, expanded=expanded, includeHidden=includeHidden, level=level, name=name, pressed=pressed, selected=selected, exact=exact, ) ) def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) def get_by_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_text_selector(text, exact=exact)) def get_by_title( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self.locator(get_by_title_selector(text, exact=exact)) def frame_locator(self, selector: str) -> "FrameLocator": return FrameLocator( self._frame, f"{self._frame_selector} >> internal:control=enter-frame >> {selector}", ) @property def first(self) -> "FrameLocator": return FrameLocator(self._frame, f"{self._frame_selector} >> nth=0") @property def last(self) -> "FrameLocator": return FrameLocator(self._frame, f"{self._frame_selector} >> nth=-1") @property def owner(self) -> "Locator": return Locator(self._frame, self._frame_selector) def nth(self, index: int) -> "FrameLocator": return FrameLocator(self._frame, f"{self._frame_selector} >> nth={index}") def __repr__(self) -> str: return f"" _test_id_attribute_name: str = "data-testid" def test_id_attribute_name() -> str: return _test_id_attribute_name def set_test_id_attribute_name(attribute_name: str) -> None: global _test_id_attribute_name _test_id_attribute_name = attribute_name def get_by_test_id_selector( test_id_attribute_name: str, test_id: Union[str, Pattern[str]] ) -> str: return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]" def get_by_attribute_text_selector( attr_name: str, text: Union[str, Pattern[str]], exact: bool = None ) -> str: return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]" def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: return "internal:label=" + escape_for_text_selector(text, exact=exact) def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: return get_by_attribute_text_selector("alt", text, exact=exact) def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: return get_by_attribute_text_selector("title", text, exact=exact) def get_by_placeholder_selector( text: Union[str, Pattern[str]], exact: bool = None ) -> str: return get_by_attribute_text_selector("placeholder", text, exact=exact) def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: return "internal:text=" + escape_for_text_selector(text, exact=exact) def bool_to_js_bool(value: bool) -> str: return "true" if value else "false" def get_by_role_selector( role: AriaRole, checked: bool = None, disabled: bool = None, expanded: bool = None, includeHidden: bool = None, level: int = None, name: Union[str, Pattern[str]] = None, pressed: bool = None, selected: bool = None, exact: bool = None, ) -> str: props: List[Tuple[str, str]] = [] if checked is not None: props.append(("checked", bool_to_js_bool(checked))) if disabled is not None: props.append(("disabled", bool_to_js_bool(disabled))) if selected is not None: props.append(("selected", bool_to_js_bool(selected))) if expanded is not None: props.append(("expanded", bool_to_js_bool(expanded))) if includeHidden is not None: props.append(("include-hidden", bool_to_js_bool(includeHidden))) if level is not None: props.append(("level", str(level))) if name is not None: props.append( ( "name", escape_for_attribute_selector(name, exact=exact), ) ) if pressed is not None: props.append(("pressed", bool_to_js_bool(pressed))) props_str = "".join([f"[{t[0]}={t[1]}]" for t in props]) return f"internal:role={role}{props_str}" ================================================ FILE: playwright/_impl/_map.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Generic, Tuple, TypeVar K = TypeVar("K") V = TypeVar("V") class Map(Generic[K, V]): def __init__(self) -> None: self._entries: Dict[int, Tuple[K, V]] = {} def __contains__(self, item: K) -> bool: return id(item) in self._entries def __setitem__(self, idx: K, value: V) -> None: self._entries[id(idx)] = (idx, value) def __getitem__(self, obj: K) -> V: return self._entries[id(obj)][1] ================================================ FILE: playwright/_impl/_network.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import base64 import inspect import json import json as json_utils import mimetypes import re from collections import defaultdict from pathlib import Path from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional, TypedDict, Union, cast, ) from urllib import parse from playwright._impl._api_structures import ( ClientCertificate, Headers, HeadersArray, RemoteAddr, RequestSizes, ResourceTiming, SecurityDetails, ) from playwright._impl._connection import ( ChannelOwner, from_channel, from_nullable_channel, ) from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._helper import ( URLMatch, WebSocketRouteHandlerCallback, async_readfile, locals_to_params, url_matches, ) from playwright._impl._str_utils import escape_regex_flags from playwright._impl._waiter import Waiter if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext from playwright._impl._fetch import APIResponse from playwright._impl._frame import Frame from playwright._impl._page import Page, Worker class FallbackOverrideParameters(TypedDict, total=False): url: Optional[str] method: Optional[str] headers: Optional[Dict[str, str]] postData: Optional[Union[str, bytes]] class SerializedFallbackOverrides: def __init__(self) -> None: self.url: Optional[str] = None self.method: Optional[str] = None self.headers: Optional[Dict[str, str]] = None self.post_data_buffer: Optional[bytes] = None def serialize_headers(headers: Dict[str, str]) -> HeadersArray: return [ {"name": name, "value": value} for name, value in headers.items() if value is not None ] async def to_client_certificates_protocol( clientCertificates: Optional[List[ClientCertificate]], ) -> Optional[List[Dict[str, str]]]: if not clientCertificates: return None out = [] for clientCertificate in clientCertificates: out_record = { "origin": clientCertificate["origin"], } if passphrase := clientCertificate.get("passphrase"): out_record["passphrase"] = passphrase if pfx := clientCertificate.get("pfx"): out_record["pfx"] = base64.b64encode(pfx).decode() if pfx_path := clientCertificate.get("pfxPath"): out_record["pfx"] = base64.b64encode( await async_readfile(pfx_path) ).decode() if cert := clientCertificate.get("cert"): out_record["cert"] = base64.b64encode(cert).decode() if cert_path := clientCertificate.get("certPath"): out_record["cert"] = base64.b64encode( await async_readfile(cert_path) ).decode() if key := clientCertificate.get("key"): out_record["key"] = base64.b64encode(key).decode() if key_path := clientCertificate.get("keyPath"): out_record["key"] = base64.b64encode( await async_readfile(key_path) ).decode() out.append(out_record) return out class Request(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._redirected_from: Optional["Request"] = from_nullable_channel( initializer.get("redirectedFrom") ) self._redirected_to: Optional["Request"] = None if self._redirected_from: self._redirected_from._redirected_to = self self._failure_text: Optional[str] = None self._timing: ResourceTiming = { "startTime": 0, "domainLookupStart": -1, "domainLookupEnd": -1, "connectStart": -1, "secureConnectionStart": -1, "connectEnd": -1, "requestStart": -1, "responseStart": -1, "responseEnd": -1, } self._provisional_headers = RawHeaders(self._initializer["headers"]) self._all_headers_future: Optional[asyncio.Future[RawHeaders]] = None self._fallback_overrides: SerializedFallbackOverrides = ( SerializedFallbackOverrides() ) def __repr__(self) -> str: return f"" def _apply_fallback_overrides(self, overrides: FallbackOverrideParameters) -> None: self._fallback_overrides.url = overrides.get( "url", self._fallback_overrides.url ) self._fallback_overrides.method = overrides.get( "method", self._fallback_overrides.method ) self._fallback_overrides.headers = overrides.get( "headers", self._fallback_overrides.headers ) post_data = overrides.get("postData") if isinstance(post_data, str): self._fallback_overrides.post_data_buffer = post_data.encode() elif isinstance(post_data, bytes): self._fallback_overrides.post_data_buffer = post_data elif post_data is not None: self._fallback_overrides.post_data_buffer = json.dumps(post_data).encode() @property def url(self) -> str: return cast(str, self._fallback_overrides.url or self._initializer["url"]) @property def resource_type(self) -> str: return self._initializer["resourceType"] @property def service_worker(self) -> Optional["Worker"]: return cast( Optional["Worker"], from_nullable_channel(self._initializer.get("serviceWorker")), ) @property def method(self) -> str: return cast(str, self._fallback_overrides.method or self._initializer["method"]) async def sizes(self) -> RequestSizes: response = await self.response() if not response: raise Error("Unable to fetch sizes for failed request") return await response._channel.send( "sizes", None, ) @property def post_data(self) -> Optional[str]: data = self._fallback_overrides.post_data_buffer if data: return data.decode() base64_post_data = self._initializer.get("postData") if base64_post_data is not None: return base64.b64decode(base64_post_data).decode() return None @property def post_data_json(self) -> Optional[Any]: post_data = self.post_data if not post_data: return None content_type = self.headers["content-type"] if "application/x-www-form-urlencoded" in content_type: return dict(parse.parse_qsl(post_data)) try: return json.loads(post_data) except Exception: raise Error(f"POST data is not a valid JSON object: {post_data}") @property def post_data_buffer(self) -> Optional[bytes]: if self._fallback_overrides.post_data_buffer: return self._fallback_overrides.post_data_buffer if self._initializer.get("postData"): return base64.b64decode(self._initializer["postData"]) return None async def response(self) -> Optional["Response"]: return from_nullable_channel( await self._channel.send( "response", None, ) ) @property def frame(self) -> "Frame": if not self._initializer.get("frame"): raise Error("Service Worker requests do not have an associated frame.") frame = cast("Frame", from_channel(self._initializer["frame"])) if not frame._page: raise Error( "\n".join( [ "Frame for this navigation request is not available, because the request", "was issued before the frame is created. You can check whether the request", "is a navigation request by calling isNavigationRequest() method.", ] ) ) return frame def is_navigation_request(self) -> bool: return self._initializer["isNavigationRequest"] @property def redirected_from(self) -> Optional["Request"]: return self._redirected_from @property def redirected_to(self) -> Optional["Request"]: return self._redirected_to @property def failure(self) -> Optional[str]: return self._failure_text @property def timing(self) -> ResourceTiming: return self._timing def _set_response_end_timing(self, response_end_timing: float) -> None: self._timing["responseEnd"] = response_end_timing if self._timing["responseStart"] == -1: self._timing["responseStart"] = response_end_timing @property def headers(self) -> Headers: override = self._fallback_overrides.headers if override: return RawHeaders._from_headers_dict_lossy(override).headers() return self._provisional_headers.headers() async def all_headers(self) -> Headers: return (await self._actual_headers()).headers() async def headers_array(self) -> HeadersArray: return (await self._actual_headers()).headers_array() async def header_value(self, name: str) -> Optional[str]: return (await self._actual_headers()).get(name) async def _actual_headers(self) -> "RawHeaders": override = self._fallback_overrides.headers if override: return RawHeaders(serialize_headers(override)) if not self._all_headers_future: self._all_headers_future = asyncio.Future() headers = await self._channel.send( "rawRequestHeaders", None, is_internal=True ) self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future def _target_closed_future(self) -> asyncio.Future: frame = cast( Optional["Frame"], from_nullable_channel(self._initializer.get("frame")) ) if not frame: return asyncio.Future() page = frame._page if not page: return asyncio.Future() return page._closed_or_crashed_future def _safe_page(self) -> "Optional[Page]": frame = from_nullable_channel(self._initializer.get("frame")) if not frame: return None return cast("Frame", frame)._page class Route(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._handling_future: Optional[asyncio.Future["bool"]] = None self._context: "BrowserContext" = cast("BrowserContext", None) self._did_throw = False def _start_handling(self) -> "asyncio.Future[bool]": self._handling_future = asyncio.Future() return self._handling_future def _report_handled(self, done: bool) -> None: chain = self._handling_future assert chain self._handling_future = None chain.set_result(done) def _check_not_handled(self) -> None: if not self._handling_future: raise Error("Route is already handled!") def __repr__(self) -> str: return f"" @property def request(self) -> Request: return from_channel(self._initializer["request"]) async def abort(self, errorCode: str = None) -> None: await self._handle_route( lambda: self._race_with_page_close( self._channel.send( "abort", None, { "errorCode": errorCode, }, ) ) ) async def fulfill( self, status: int = None, headers: Dict[str, str] = None, body: Union[str, bytes] = None, json: Any = None, path: Union[str, Path] = None, contentType: str = None, response: "APIResponse" = None, ) -> None: await self._handle_route( lambda: self._inner_fulfill( status, headers, body, json, path, contentType, response ) ) async def _inner_fulfill( self, status: int = None, headers: Dict[str, str] = None, body: Union[str, bytes] = None, json: Any = None, path: Union[str, Path] = None, contentType: str = None, response: "APIResponse" = None, ) -> None: params = locals_to_params(locals()) if json is not None: if body is not None: raise Error("Can specify either body or json parameters") body = json_utils.dumps(json) if response: del params["response"] params["status"] = ( params["status"] if params.get("status") else response.status ) params["headers"] = ( params["headers"] if params.get("headers") else response.headers ) from playwright._impl._fetch import APIResponse if body is None and path is None and isinstance(response, APIResponse): if response._request._connection is self._connection: params["fetchResponseUid"] = response._fetch_uid else: body = await response.body() length = 0 if isinstance(body, str): params["body"] = body params["isBase64"] = False length = len(body.encode()) elif isinstance(body, bytes): params["body"] = base64.b64encode(body).decode() params["isBase64"] = True length = len(body) elif path: del params["path"] file_content = Path(path).read_bytes() params["body"] = base64.b64encode(file_content).decode() params["isBase64"] = True length = len(file_content) headers = {k.lower(): str(v) for k, v in params.get("headers", {}).items()} if params.get("contentType"): headers["content-type"] = params["contentType"] elif json: headers["content-type"] = "application/json" elif path: headers["content-type"] = ( mimetypes.guess_type(str(Path(path)))[0] or "application/octet-stream" ) if length and "content-length" not in headers: headers["content-length"] = str(length) params["headers"] = serialize_headers(headers) await self._race_with_page_close(self._channel.send("fulfill", None, params)) async def _handle_route(self, callback: Callable) -> None: self._check_not_handled() try: await callback() self._report_handled(True) except Exception as e: self._did_throw = True raise e async def fetch( self, url: str = None, method: str = None, headers: Dict[str, str] = None, postData: Union[Any, str, bytes] = None, maxRedirects: int = None, maxRetries: int = None, timeout: float = None, ) -> "APIResponse": return await self._connection.wrap_api_call( lambda: self._context.request._inner_fetch( self.request, url, method, headers, postData, maxRedirects=maxRedirects, maxRetries=maxRetries, timeout=timeout, ) ) async def fallback( self, url: str = None, method: str = None, headers: Dict[str, str] = None, postData: Union[Any, str, bytes] = None, ) -> None: overrides = cast(FallbackOverrideParameters, locals_to_params(locals())) self._check_not_handled() self.request._apply_fallback_overrides(overrides) self._report_handled(False) async def continue_( self, url: str = None, method: str = None, headers: Dict[str, str] = None, postData: Union[Any, str, bytes] = None, ) -> None: overrides = cast(FallbackOverrideParameters, locals_to_params(locals())) async def _inner() -> None: self.request._apply_fallback_overrides(overrides) await self._inner_continue(False) return await self._handle_route(_inner) async def _inner_continue(self, is_fallback: bool = False) -> None: options = self.request._fallback_overrides await self._race_with_page_close( self._channel.send( "continue", None, { "url": options.url, "method": options.method, "headers": ( serialize_headers(options.headers) if options.headers else None ), "postData": ( base64.b64encode(options.post_data_buffer).decode() if options.post_data_buffer is not None else None ), "isFallback": is_fallback, }, ) ) async def _redirected_navigation_request(self, url: str) -> None: await self._handle_route( lambda: self._race_with_page_close( self._channel.send("redirectNavigationRequest", None, {"url": url}) ) ) async def _race_with_page_close(self, future: Coroutine) -> None: fut = asyncio.create_task(future) # Rewrite the user's stack to the new task which runs in the background. setattr( fut, "__pw_stack__", getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack(0)), ) target_closed_future = self.request._target_closed_future() await asyncio.wait( [fut, target_closed_future], return_when=asyncio.FIRST_COMPLETED, ) if fut.done() and fut.exception(): raise cast(BaseException, fut.exception()) if target_closed_future.done(): await asyncio.gather(fut, return_exceptions=True) def _create_task_and_ignore_exception( loop: asyncio.AbstractEventLoop, coro: Coroutine ) -> None: async def _ignore_exception() -> None: try: await coro except Exception: pass loop.create_task(_ignore_exception()) class ServerWebSocketRoute: def __init__(self, ws: "WebSocketRoute"): self._ws = ws def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None: self._ws._on_server_message = handler def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None: self._ws._on_server_close = handler def connect_to_server(self) -> None: raise NotImplementedError( "connectToServer must be called on the page-side WebSocketRoute" ) @property def url(self) -> str: return self._ws._initializer["url"] def close(self, code: int = None, reason: str = None) -> None: _create_task_and_ignore_exception( self._ws._loop, self._ws._channel.send( "closeServer", None, { "code": code, "reason": reason, "wasClean": True, }, ), ) def send(self, message: Union[str, bytes]) -> None: if isinstance(message, str): _create_task_and_ignore_exception( self._ws._loop, self._ws._channel.send( "sendToServer", None, {"message": message, "isBase64": False} ), ) else: _create_task_and_ignore_exception( self._ws._loop, self._ws._channel.send( "sendToServer", None, {"message": base64.b64encode(message).decode(), "isBase64": True}, ), ) class WebSocketRoute(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._on_page_message: Optional[Callable[[Union[str, bytes]], Any]] = None self._on_page_close: Optional[Callable[[Optional[int], Optional[str]], Any]] = ( None ) self._on_server_message: Optional[Callable[[Union[str, bytes]], Any]] = None self._on_server_close: Optional[ Callable[[Optional[int], Optional[str]], Any] ] = None self._server = ServerWebSocketRoute(self) self._connected = False self._channel.on("messageFromPage", self._channel_message_from_page) self._channel.on("messageFromServer", self._channel_message_from_server) self._channel.on("closePage", self._channel_close_page) self._channel.on("closeServer", self._channel_close_server) def _channel_message_from_page(self, event: Dict) -> None: if self._on_page_message: self._on_page_message( base64.b64decode(event["message"]) if event["isBase64"] else event["message"] ) elif self._connected: _create_task_and_ignore_exception( self._loop, self._channel.send("sendToServer", None, event) ) def _channel_message_from_server(self, event: Dict) -> None: if self._on_server_message: self._on_server_message( base64.b64decode(event["message"]) if event["isBase64"] else event["message"] ) else: _create_task_and_ignore_exception( self._loop, self._channel.send("sendToPage", None, event) ) def _channel_close_page(self, event: Dict) -> None: if self._on_page_close: self._on_page_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( self._loop, self._channel.send("closeServer", None, event) ) def _channel_close_server(self, event: Dict) -> None: if self._on_server_close: self._on_server_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( self._loop, self._channel.send("closePage", None, event) ) @property def url(self) -> str: return self._initializer["url"] async def close(self, code: int = None, reason: str = None) -> None: try: await self._channel.send( "closePage", None, {"code": code, "reason": reason, "wasClean": True} ) except Exception: pass def connect_to_server(self) -> "WebSocketRoute": if self._connected: raise Error("Already connected to the server") self._connected = True asyncio.create_task( self._channel.send( "connect", None, ) ) return cast("WebSocketRoute", self._server) def send(self, message: Union[str, bytes]) -> None: if isinstance(message, str): _create_task_and_ignore_exception( self._loop, self._channel.send( "sendToPage", None, {"message": message, "isBase64": False} ), ) else: _create_task_and_ignore_exception( self._loop, self._channel.send( "sendToPage", None, { "message": base64.b64encode(message).decode(), "isBase64": True, }, ), ) def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None: self._on_page_message = handler def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None: self._on_page_close = handler async def _after_handle(self) -> None: if self._connected: return # Ensure that websocket is "open" and can send messages without an actual server connection. try: await self._channel.send( "ensureOpened", None, ) except Exception: pass class WebSocketRouteHandler: def __init__( self, base_url: Optional[str], url: URLMatch, handler: WebSocketRouteHandlerCallback, ): self._base_url = base_url self.url = url self.handler = handler @staticmethod def prepare_interception_patterns( handlers: List["WebSocketRouteHandler"], ) -> List[dict]: patterns = [] all_urls = False for handler in handlers: if isinstance(handler.url, str): patterns.append({"glob": handler.url}) elif isinstance(handler.url, re.Pattern): patterns.append( { "regexSource": handler.url.pattern, "regexFlags": escape_regex_flags(handler.url), } ) else: all_urls = True if all_urls: return [{"glob": "**/*"}] return patterns def matches(self, ws_url: str) -> bool: return url_matches(self._base_url, ws_url, self.url, True) async def handle(self, websocket_route: "WebSocketRoute") -> None: coro_or_future = self.handler(websocket_route) if asyncio.iscoroutine(coro_or_future): await coro_or_future await websocket_route._after_handle() class Response(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._request: Request = from_channel(self._initializer["request"]) timing = self._initializer["timing"] self._request._timing["startTime"] = timing["startTime"] self._request._timing["domainLookupStart"] = timing["domainLookupStart"] self._request._timing["domainLookupEnd"] = timing["domainLookupEnd"] self._request._timing["connectStart"] = timing["connectStart"] self._request._timing["secureConnectionStart"] = timing["secureConnectionStart"] self._request._timing["connectEnd"] = timing["connectEnd"] self._request._timing["requestStart"] = timing["requestStart"] self._request._timing["responseStart"] = timing["responseStart"] self._provisional_headers = RawHeaders( cast(HeadersArray, self._initializer["headers"]) ) self._raw_headers_future: Optional[asyncio.Future[RawHeaders]] = None self._finished_future: asyncio.Future[bool] = asyncio.Future() def __repr__(self) -> str: return f"" @property def url(self) -> str: return self._initializer["url"] @property def ok(self) -> bool: # Status 0 is for file:// URLs return self._initializer["status"] == 0 or ( self._initializer["status"] >= 200 and self._initializer["status"] <= 299 ) @property def status(self) -> int: return self._initializer["status"] @property def status_text(self) -> str: return self._initializer["statusText"] @property def headers(self) -> Headers: return self._provisional_headers.headers() @property def from_service_worker(self) -> bool: return self._initializer["fromServiceWorker"] async def all_headers(self) -> Headers: return (await self._actual_headers()).headers() async def headers_array(self) -> HeadersArray: return (await self._actual_headers()).headers_array() async def header_value(self, name: str) -> Optional[str]: return (await self._actual_headers()).get(name) async def header_values(self, name: str) -> List[str]: return (await self._actual_headers()).get_all(name) async def _actual_headers(self) -> "RawHeaders": if not self._raw_headers_future: self._raw_headers_future = asyncio.Future() headers = cast( HeadersArray, await self._channel.send( "rawResponseHeaders", None, ), ) self._raw_headers_future.set_result(RawHeaders(headers)) return await self._raw_headers_future async def server_addr(self) -> Optional[RemoteAddr]: return await self._channel.send( "serverAddr", None, ) async def security_details(self) -> Optional[SecurityDetails]: return await self._channel.send( "securityDetails", None, ) async def finished(self) -> None: async def on_finished() -> None: await self._request._target_closed_future() raise Error("Target closed") on_finished_task = asyncio.create_task(on_finished()) await asyncio.wait( cast( List[Union[asyncio.Task, asyncio.Future]], [self._finished_future, on_finished_task], ), return_when=asyncio.FIRST_COMPLETED, ) if on_finished_task.done(): await on_finished_task async def body(self) -> bytes: binary = await self._channel.send( "body", None, ) return base64.b64decode(binary) async def text(self) -> str: content = await self.body() return content.decode() async def json(self) -> Any: return json.loads(await self.text()) @property def request(self) -> Request: return self._request @property def frame(self) -> "Frame": return self._request.frame class WebSocket(ChannelOwner): Events = SimpleNamespace( Close="close", FrameReceived="framereceived", FrameSent="framesent", Error="socketerror", ) def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._is_closed = False self._page = cast("Page", parent) self._channel.on( "frameSent", lambda params: self._on_frame_sent(params["opcode"], params["data"]), ) self._channel.on( "frameReceived", lambda params: self._on_frame_received(params["opcode"], params["data"]), ) self._channel.on( "socketError", lambda params: self.emit(WebSocket.Events.Error, params["error"]), ) self._channel.on("close", lambda params: self._on_close()) def __repr__(self) -> str: return f"" @property def url(self) -> str: return self._initializer["url"] def expect_event( self, event: str, predicate: Callable = None, timeout: float = None, ) -> EventContextManagerImpl: if timeout is None: timeout = cast(Any, self._parent)._timeout_settings.timeout() waiter = Waiter(self, f"web_socket.expect_event({event})") waiter.reject_on_timeout( cast(float, timeout), f'Timeout {timeout}ms exceeded while waiting for event "{event}"', ) if event != WebSocket.Events.Close: waiter.reject_on_event(self, WebSocket.Events.Close, Error("Socket closed")) if event != WebSocket.Events.Error: waiter.reject_on_event(self, WebSocket.Events.Error, Error("Socket error")) waiter.reject_on_event( self._page, "close", lambda: self._page._close_error_with_reason() ) waiter.wait_for_event(self, event, predicate) return EventContextManagerImpl(waiter.result()) async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None ) -> Any: async with self.expect_event(event, predicate, timeout) as event_info: pass return await event_info def _on_frame_sent(self, opcode: int, data: str) -> None: if opcode == 2: self.emit(WebSocket.Events.FrameSent, base64.b64decode(data)) elif opcode == 1: self.emit(WebSocket.Events.FrameSent, data) def _on_frame_received(self, opcode: int, data: str) -> None: if opcode == 2: self.emit(WebSocket.Events.FrameReceived, base64.b64decode(data)) elif opcode == 1: self.emit(WebSocket.Events.FrameReceived, data) def is_closed(self) -> bool: return self._is_closed def _on_close(self) -> None: self._is_closed = True self.emit(WebSocket.Events.Close, self) class RawHeaders: def __init__(self, headers: HeadersArray) -> None: self._headers_array = headers self._headers_map: Dict[str, Dict[str, bool]] = defaultdict(dict) for header in headers: self._headers_map[header["name"].lower()][header["value"]] = True @staticmethod def _from_headers_dict_lossy(headers: Dict[str, str]) -> "RawHeaders": return RawHeaders(serialize_headers(headers)) def get(self, name: str) -> Optional[str]: values = self.get_all(name) if not values: return None separator = "\n" if name.lower() == "set-cookie" else ", " return separator.join(values) def get_all(self, name: str) -> List[str]: return list(self._headers_map[name.lower()].keys()) def headers(self) -> Dict[str, str]: result = {} for name in self._headers_map.keys(): result[name] = cast(str, self.get(name)) return result def headers_array(self) -> HeadersArray: return self._headers_array ================================================ FILE: playwright/_impl/_object_factory.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, cast from playwright._impl._artifact import Artifact from playwright._impl._browser import Browser from playwright._impl._browser_context import BrowserContext from playwright._impl._browser_type import BrowserType from playwright._impl._cdp_session import CDPSession from playwright._impl._connection import ChannelOwner from playwright._impl._dialog import Dialog from playwright._impl._element_handle import ElementHandle from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame from playwright._impl._js_handle import JSHandle from playwright._impl._local_utils import LocalUtils from playwright._impl._network import ( Request, Response, Route, WebSocket, WebSocketRoute, ) from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._playwright import Playwright from playwright._impl._stream import Stream from playwright._impl._tracing import Tracing from playwright._impl._writable_stream import WritableStream class DummyObject(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) def create_remote_object( parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> ChannelOwner: if type == "Artifact": return Artifact(parent, type, guid, initializer) if type == "APIRequestContext": return APIRequestContext(parent, type, guid, initializer) if type == "BindingCall": return BindingCall(parent, type, guid, initializer) if type == "Browser": return Browser(cast(BrowserType, parent), type, guid, initializer) if type == "BrowserType": return BrowserType(parent, type, guid, initializer) if type == "BrowserContext": return BrowserContext(parent, type, guid, initializer) if type == "CDPSession": return CDPSession(parent, type, guid, initializer) if type == "Dialog": return Dialog(parent, type, guid, initializer) if type == "ElementHandle": return ElementHandle(parent, type, guid, initializer) if type == "Frame": return Frame(parent, type, guid, initializer) if type == "JSHandle": return JSHandle(parent, type, guid, initializer) if type == "LocalUtils": local_utils = LocalUtils(parent, type, guid, initializer) if not local_utils._connection._local_utils: local_utils._connection._local_utils = local_utils return local_utils if type == "Page": return Page(parent, type, guid, initializer) if type == "Playwright": return Playwright(parent, type, guid, initializer) if type == "Request": return Request(parent, type, guid, initializer) if type == "Response": return Response(parent, type, guid, initializer) if type == "Route": return Route(parent, type, guid, initializer) if type == "Stream": return Stream(parent, type, guid, initializer) if type == "Tracing": return Tracing(parent, type, guid, initializer) if type == "WebSocket": return WebSocket(parent, type, guid, initializer) if type == "WebSocketRoute": return WebSocketRoute(parent, type, guid, initializer) if type == "Worker": return Worker(parent, type, guid, initializer) if type == "WritableStream": return WritableStream(parent, type, guid, initializer) return DummyObject(parent, type, guid, initializer) ================================================ FILE: playwright/_impl/_page.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import base64 import inspect import re import sys from pathlib import Path from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Pattern, Sequence, Union, cast, ) from playwright._impl._api_structures import ( AriaRole, FilePayload, FloatRect, PdfMargins, Position, ViewportSize, ) from playwright._impl._artifact import Artifact from playwright._impl._clock import Clock from playwright._impl._connection import ( ChannelOwner, from_channel, from_nullable_channel, ) from playwright._impl._console_message import ConsoleMessage from playwright._impl._download import Download from playwright._impl._element_handle import ElementHandle, determine_screenshot_type from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser from playwright._impl._frame import Frame from playwright._impl._greenlets import LocatorHandlerGreenlet from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( ColorScheme, Contrast, DocumentLoadState, ForcedColors, HarMode, KeyboardModifier, MouseButton, ReducedMotion, RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, TimeoutSettings, URLMatch, URLMatchRequest, URLMatchResponse, WebSocketRouteHandlerCallback, async_readfile, async_writefile, locals_to_params, make_dirs_for_file, parse_error, serialize_error, url_matches, ) from playwright._impl._input import Keyboard, Mouse, Touchscreen from playwright._impl._js_handle import ( JSHandle, Serializable, add_source_url_to_script, parse_result, serialize_argument, ) from playwright._impl._network import ( Request, Response, Route, WebSocketRoute, WebSocketRouteHandler, serialize_headers, ) from playwright._impl._video import Video from playwright._impl._waiter import Waiter if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext from playwright._impl._fetch import APIRequestContext from playwright._impl._locator import FrameLocator, Locator from playwright._impl._network import WebSocket class LocatorHandler: locator: "Locator" handler: Union[Callable[["Locator"], Any], Callable[..., Any]] times: Union[int, None] def __init__( self, locator: "Locator", handler: Callable[..., Any], times: Union[int, None] ) -> None: self.locator = locator self._handler = handler self.times = times def __call__(self) -> Any: arg_count = len(inspect.signature(self._handler).parameters) if arg_count == 0: return self._handler() return self._handler(self.locator) class Page(ChannelOwner): Events = SimpleNamespace( Close="close", Crash="crash", Console="console", Dialog="dialog", Download="download", FileChooser="filechooser", DOMContentLoaded="domcontentloaded", PageError="pageerror", Request="request", Response="response", RequestFailed="requestfailed", RequestFinished="requestfinished", FrameAttached="frameattached", FrameDetached="framedetached", FrameNavigated="framenavigated", Load="load", Popup="popup", WebSocket="websocket", Worker="worker", ) keyboard: Keyboard mouse: Mouse touchscreen: Touchscreen def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._browser_context = cast("BrowserContext", parent) self.keyboard = Keyboard(self._channel) self.mouse = Mouse(self._channel) self.touchscreen = Touchscreen(self._channel) self._main_frame: Frame = from_channel(initializer["mainFrame"]) self._main_frame._page = self self._frames = [self._main_frame] self._viewport_size: Optional[ViewportSize] = initializer.get("viewportSize") self._is_closed = False self._workers: List["Worker"] = [] self._bindings: Dict[str, Any] = {} self._routes: List[RouteHandler] = [] self._web_socket_routes: List[WebSocketRouteHandler] = [] self._owned_context: Optional["BrowserContext"] = None self._timeout_settings: TimeoutSettings = TimeoutSettings( self._browser_context._timeout_settings ) self._video: Optional[Video] = None self._opener = cast("Page", from_nullable_channel(initializer.get("opener"))) self._close_reason: Optional[str] = None self._close_was_called = False self._har_routers: List[HarRouter] = [] self._locator_handlers: Dict[str, LocatorHandler] = {} self._channel.on( "bindingCall", lambda params: self._on_binding(from_channel(params["binding"])), ) self._channel.on("close", lambda _: self._on_close()) self._channel.on("crash", lambda _: self._on_crash()) self._channel.on("download", lambda params: self._on_download(params)) self._channel.on( "fileChooser", lambda params: self.emit( Page.Events.FileChooser, FileChooser( self, from_channel(params["element"]), params["isMultiple"] ), ), ) self._channel.on( "frameAttached", lambda params: self._on_frame_attached(from_channel(params["frame"])), ) self._channel.on( "frameDetached", lambda params: self._on_frame_detached(from_channel(params["frame"])), ) self._channel.on( "locatorHandlerTriggered", lambda params: self._loop.create_task( self._on_locator_handler_triggered(params["uid"]) ), ) self._channel.on( "route", lambda params: self._loop.create_task( self._on_route(from_channel(params["route"])) ), ) self._channel.on( "webSocketRoute", lambda params: self._loop.create_task( self._on_web_socket_route(from_channel(params["webSocketRoute"])) ), ) self._channel.on("video", lambda params: self._on_video(params)) self._channel.on("viewportSizeChanged", self._on_viewport_size_changed) self._channel.on( "webSocket", lambda params: self.emit( Page.Events.WebSocket, from_channel(params["webSocket"]) ), ) self._channel.on( "worker", lambda params: self._on_worker(from_channel(params["worker"])) ) self._closed_or_crashed_future: asyncio.Future = asyncio.Future() self.on( Page.Events.Close, lambda _: ( self._closed_or_crashed_future.set_result( self._close_error_with_reason() ) if not self._closed_or_crashed_future.done() else None ), ) self.on( Page.Events.Crash, lambda _: ( self._closed_or_crashed_future.set_result(TargetClosedError()) if not self._closed_or_crashed_future.done() else None ), ) self._set_event_to_subscription_mapping( { Page.Events.Console: "console", Page.Events.Dialog: "dialog", Page.Events.Request: "request", Page.Events.Response: "response", Page.Events.RequestFinished: "requestFinished", Page.Events.RequestFailed: "requestFailed", Page.Events.FileChooser: "fileChooser", } ) def __repr__(self) -> str: return f"" def _on_frame_attached(self, frame: Frame) -> None: frame._page = self self._frames.append(frame) self.emit(Page.Events.FrameAttached, frame) def _on_frame_detached(self, frame: Frame) -> None: self._frames.remove(frame) frame._detached = True self.emit(Page.Events.FrameDetached, frame) async def _on_route(self, route: Route) -> None: route._context = self.context route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page was closed we stall all requests right away. if self._close_was_called or self.context._closing_or_closed: return if not route_handler.matches(route.request.url): continue if route_handler not in self._routes: continue if route_handler.will_expire: self._routes.remove(route_handler) try: handled = await route_handler.handle(route) finally: if len(self._routes) == 0: async def _update_interceptor_patterns_ignore_exceptions() -> None: try: await self._update_interception_patterns() except Error: pass asyncio.create_task( self._connection.wrap_api_call( _update_interceptor_patterns_ignore_exceptions, True ) ) if handled: return await self._browser_context._on_route(route) async def _on_web_socket_route(self, web_socket_route: WebSocketRoute) -> None: route_handler = next( ( route_handler for route_handler in self._web_socket_routes if route_handler.matches(web_socket_route.url) ), None, ) if route_handler: await route_handler.handle(web_socket_route) else: await self._browser_context._on_web_socket_route(web_socket_route) def _on_binding(self, binding_call: "BindingCall") -> None: func = self._bindings.get(binding_call._initializer["name"]) if func: asyncio.create_task(binding_call.call(func)) self._browser_context._on_binding(binding_call) def _on_worker(self, worker: "Worker") -> None: self._workers.append(worker) worker._page = self self.emit(Page.Events.Worker, worker) def _on_close(self) -> None: self._is_closed = True if self in self._browser_context._pages: self._browser_context._pages.remove(self) self._dispose_har_routers() self.emit(Page.Events.Close, self) def _on_crash(self) -> None: self.emit(Page.Events.Crash, self) def _on_download(self, params: Any) -> None: url = params["url"] suggested_filename = params["suggestedFilename"] artifact = cast(Artifact, from_channel(params["artifact"])) self.emit( Page.Events.Download, Download(self, url, suggested_filename, artifact) ) def _on_video(self, params: Any) -> None: artifact = from_channel(params["artifact"]) self._force_video()._artifact_ready(artifact) def _on_viewport_size_changed(self, params: Any) -> None: self._viewport_size = params["viewportSize"] @property def context(self) -> "BrowserContext": return self._browser_context @property def clock(self) -> Clock: return self._browser_context.clock async def opener(self) -> Optional["Page"]: if self._opener and self._opener.is_closed(): return None return self._opener @property def main_frame(self) -> Frame: return self._main_frame def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: for frame in self._frames: if name and frame.name == name: return frame if url and url_matches(self._browser_context._base_url, frame.url, url): return frame return None @property def frames(self) -> List[Frame]: return self._frames.copy() def set_default_navigation_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) def set_default_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_timeout(timeout) async def query_selector( self, selector: str, strict: bool = None, ) -> Optional[ElementHandle]: return await self._main_frame.query_selector(selector, strict) async def query_selector_all(self, selector: str) -> List[ElementHandle]: return await self._main_frame.query_selector_all(selector) async def wait_for_selector( self, selector: str, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None, strict: bool = None, ) -> Optional[ElementHandle]: return await self._main_frame.wait_for_selector(**locals_to_params(locals())) async def is_checked( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._main_frame.is_checked(**locals_to_params(locals())) async def is_disabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._main_frame.is_disabled(**locals_to_params(locals())) async def is_editable( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._main_frame.is_editable(**locals_to_params(locals())) async def is_enabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: return await self._main_frame.is_enabled(**locals_to_params(locals())) async def is_hidden( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: # timeout is deprecated and does nothing return await self._main_frame.is_hidden(selector=selector, strict=strict) async def is_visible( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: # timeout is deprecated and does nothing return await self._main_frame.is_visible(selector=selector, strict=strict) async def dispatch_event( self, selector: str, type: str, eventInit: Dict = None, timeout: float = None, strict: bool = None, ) -> None: return await self._main_frame.dispatch_event(**locals_to_params(locals())) async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return await self._main_frame.evaluate(expression, arg) async def evaluate_handle( self, expression: str, arg: Serializable = None ) -> JSHandle: return await self._main_frame.evaluate_handle(expression, arg) async def eval_on_selector( self, selector: str, expression: str, arg: Serializable = None, strict: bool = None, ) -> Any: return await self._main_frame.eval_on_selector( selector, expression, arg, strict ) async def eval_on_selector_all( self, selector: str, expression: str, arg: Serializable = None, ) -> Any: return await self._main_frame.eval_on_selector_all(selector, expression, arg) async def add_script_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None, type: str = None, ) -> ElementHandle: return await self._main_frame.add_script_tag(**locals_to_params(locals())) async def add_style_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None ) -> ElementHandle: return await self._main_frame.add_style_tag(**locals_to_params(locals())) async def expose_function(self, name: str, callback: Callable) -> None: await self.expose_binding(name, lambda source, *args: callback(*args)) async def expose_binding( self, name: str, callback: Callable, handle: bool = None ) -> None: if name in self._bindings: raise Error(f'Function "{name}" has been already registered') if name in self._browser_context._bindings: raise Error( f'Function "{name}" has been already registered in the browser context' ) self._bindings[name] = callback await self._channel.send( "exposeBinding", None, dict(name=name, needsHandle=handle or False), ) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( "setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers)), ) @property def url(self) -> str: return self._main_frame.url async def content(self) -> str: return await self._main_frame.content() async def set_content( self, html: str, timeout: float = None, waitUntil: DocumentLoadState = None, ) -> None: return await self._main_frame.set_content(**locals_to_params(locals())) async def goto( self, url: str, timeout: float = None, waitUntil: DocumentLoadState = None, referer: str = None, ) -> Optional[Response]: return await self._main_frame.goto(**locals_to_params(locals())) async def reload( self, timeout: float = None, waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( "reload", self._timeout_settings.navigation_timeout, locals_to_params(locals()), ) ) async def wait_for_load_state( self, state: Literal["domcontentloaded", "load", "networkidle"] = None, timeout: float = None, ) -> None: return await self._main_frame.wait_for_load_state(**locals_to_params(locals())) async def wait_for_url( self, url: URLMatch, waitUntil: DocumentLoadState = None, timeout: float = None, ) -> None: return await self._main_frame.wait_for_url(**locals_to_params(locals())) async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None ) -> Any: async with self.expect_event(event, predicate, timeout) as event_info: pass return await event_info async def go_back( self, timeout: float = None, waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( "goBack", self._timeout_settings.navigation_timeout, locals_to_params(locals()), ) ) async def go_forward( self, timeout: float = None, waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( "goForward", self._timeout_settings.navigation_timeout, locals_to_params(locals()), ) ) async def request_gc(self) -> None: await self._channel.send("requestGC", None) async def emulate_media( self, media: Literal["null", "print", "screen"] = None, colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, contrast: Contrast = None, ) -> None: params = locals_to_params(locals()) if "media" in params: params["media"] = "no-override" if params["media"] == "null" else media if "colorScheme" in params: params["colorScheme"] = ( "no-override" if params["colorScheme"] == "null" else colorScheme ) if "reducedMotion" in params: params["reducedMotion"] = ( "no-override" if params["reducedMotion"] == "null" else reducedMotion ) if "forcedColors" in params: params["forcedColors"] = ( "no-override" if params["forcedColors"] == "null" else forcedColors ) if "contrast" in params: params["contrast"] = ( "no-override" if params["contrast"] == "null" else contrast ) await self._channel.send("emulateMedia", None, params) async def set_viewport_size(self, viewportSize: ViewportSize) -> None: self._viewport_size = viewportSize await self._channel.send( "setViewportSize", None, locals_to_params(locals()), ) @property def viewport_size(self) -> Optional[ViewportSize]: return self._viewport_size async def bring_to_front(self) -> None: await self._channel.send("bringToFront", None) async def add_init_script( self, script: str = None, path: Union[str, Path] = None ) -> None: if path: script = add_source_url_to_script( (await async_readfile(path)).decode(), path ) if not isinstance(script, str): raise Error("Either path or script parameter must be specified") await self._channel.send("addInitScript", None, dict(source=script)) async def route( self, url: URLMatch, handler: RouteHandlerCallback, times: int = None ) -> None: self._routes.insert( 0, RouteHandler( self._browser_context._base_url, url, handler, True if self._dispatcher_fiber else False, times, ), ) await self._update_interception_patterns() async def unroute( self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None ) -> None: removed = [] remaining = [] for route in self._routes: if route.url != url or (handler and route.handler != handler): remaining.append(route) else: removed.append(route) await self._unroute_internal(removed, remaining, "default") async def _unroute_internal( self, removed: List[RouteHandler], remaining: List[RouteHandler], behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining if behavior is not None and behavior != "default": await asyncio.gather( *map( lambda route: route.stop(behavior), # type: ignore removed, ) ) await self._update_interception_patterns() async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, WebSocketRouteHandler(self._browser_context._base_url, url, handler), ) await self._update_web_socket_interception_patterns() def _dispose_har_routers(self) -> None: for router in self._har_routers: router.dispose() self._har_routers = [] async def unroute_all( self, behavior: Literal["default", "ignoreErrors", "wait"] = None ) -> None: await self._unroute_internal(self._routes, [], behavior) self._dispose_har_routers() async def route_from_har( self, har: Union[Path, str], url: Union[Pattern[str], str] = None, notFound: RouteFromHarNotFoundPolicy = None, update: bool = None, updateContent: Literal["attach", "embed"] = None, updateMode: HarMode = None, ) -> None: if update: await self._browser_context._record_into_har( har=har, page=self, url=url, update_content=updateContent, update_mode=updateMode, ) return router = await HarRouter.create( local_utils=self._connection.local_utils, file=str(har), not_found_action=notFound or "abort", url_matcher=url, ) self._har_routers.append(router) await router.add_page_route(self) async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( "setNetworkInterceptionPatterns", None, {"patterns": patterns}, ) async def _update_web_socket_interception_patterns(self) -> None: patterns = WebSocketRouteHandler.prepare_interception_patterns( self._web_socket_routes ) await self._channel.send( "setWebSocketInterceptionPatterns", None, {"patterns": patterns}, ) async def screenshot( self, timeout: float = None, type: Literal["jpeg", "png"] = None, path: Union[str, Path] = None, quality: int = None, omitBackground: bool = None, fullPage: bool = None, clip: FloatRect = None, animations: Literal["allow", "disabled"] = None, caret: Literal["hide", "initial"] = None, scale: Literal["css", "device"] = None, mask: Sequence["Locator"] = None, maskColor: str = None, style: str = None, ) -> bytes: params = locals_to_params(locals()) if "path" in params: if "type" not in params: params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( map( lambda locator: ( { "frame": locator._frame._channel, "selector": locator._selector, } ), params["mask"], ) ) encoded_binary = await self._channel.send( "screenshot", self._timeout_settings.timeout, params ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) await async_writefile(path, decoded_binary) return decoded_binary async def title(self) -> str: return await self._main_frame.title() async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None: self._close_reason = reason self._close_was_called = True try: await self._channel.send("close", None, locals_to_params(locals())) if self._owned_context: await self._owned_context.close() except Exception as e: if not is_target_closed_error(e) and not runBeforeUnload: raise e def is_closed(self) -> bool: return self._is_closed async def click( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, clickCount: int = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, trial: bool = None, strict: bool = None, ) -> None: return await self._main_frame._click(**locals_to_params(locals())) async def dblclick( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, delay: float = None, button: MouseButton = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.dblclick(**locals_to_params(locals())) async def tap( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.tap(**locals_to_params(locals())) async def fill( self, selector: str, value: str, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, force: bool = None, ) -> None: return await self._main_frame.fill(**locals_to_params(locals())) def locator( self, selector: str, hasText: Union[str, Pattern[str]] = None, hasNotText: Union[str, Pattern[str]] = None, has: "Locator" = None, hasNot: "Locator" = None, ) -> "Locator": return self._main_frame.locator( selector, hasText=hasText, hasNotText=hasNotText, has=has, hasNot=hasNot, ) def get_by_alt_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self._main_frame.get_by_alt_text(text, exact=exact) def get_by_label( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self._main_frame.get_by_label(text, exact=exact) def get_by_placeholder( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self._main_frame.get_by_placeholder(text, exact=exact) def get_by_role( self, role: AriaRole, checked: bool = None, disabled: bool = None, expanded: bool = None, includeHidden: bool = None, level: int = None, name: Union[str, Pattern[str]] = None, pressed: bool = None, selected: bool = None, exact: bool = None, ) -> "Locator": return self._main_frame.get_by_role( role, checked=checked, disabled=disabled, expanded=expanded, includeHidden=includeHidden, level=level, name=name, pressed=pressed, selected=selected, exact=exact, ) def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": return self._main_frame.get_by_test_id(testId) def get_by_text( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self._main_frame.get_by_text(text, exact=exact) def get_by_title( self, text: Union[str, Pattern[str]], exact: bool = None ) -> "Locator": return self._main_frame.get_by_title(text, exact=exact) def frame_locator(self, selector: str) -> "FrameLocator": return self.main_frame.frame_locator(selector) async def focus( self, selector: str, strict: bool = None, timeout: float = None ) -> None: return await self._main_frame.focus(**locals_to_params(locals())) async def text_content( self, selector: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._main_frame.text_content(**locals_to_params(locals())) async def inner_text( self, selector: str, strict: bool = None, timeout: float = None ) -> str: return await self._main_frame.inner_text(**locals_to_params(locals())) async def inner_html( self, selector: str, strict: bool = None, timeout: float = None ) -> str: return await self._main_frame.inner_html(**locals_to_params(locals())) async def get_attribute( self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._main_frame.get_attribute(**locals_to_params(locals())) async def hover( self, selector: str, modifiers: Sequence[KeyboardModifier] = None, position: Position = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.hover(**locals_to_params(locals())) async def drag_and_drop( self, source: str, target: str, sourcePosition: Position = None, targetPosition: Position = None, force: bool = None, noWaitAfter: bool = None, timeout: float = None, strict: bool = None, trial: bool = None, steps: int = None, ) -> None: return await self._main_frame.drag_and_drop(**locals_to_params(locals())) async def select_option( self, selector: str, value: Union[str, Sequence[str]] = None, index: Union[int, Sequence[int]] = None, label: Union[str, Sequence[str]] = None, element: Union["ElementHandle", Sequence["ElementHandle"]] = None, timeout: float = None, noWaitAfter: bool = None, force: bool = None, strict: bool = None, ) -> List[str]: params = locals_to_params(locals()) return await self._main_frame.select_option(**params) async def input_value( self, selector: str, strict: bool = None, timeout: float = None ) -> str: params = locals_to_params(locals()) return await self._main_frame.input_value(**params) async def set_input_files( self, selector: str, files: Union[ str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] ], timeout: float = None, strict: bool = None, noWaitAfter: bool = None, ) -> None: return await self._main_frame.set_input_files(**locals_to_params(locals())) async def type( self, selector: str, text: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, ) -> None: return await self._main_frame.type(**locals_to_params(locals())) async def press( self, selector: str, key: str, delay: float = None, timeout: float = None, noWaitAfter: bool = None, strict: bool = None, ) -> None: return await self._main_frame.press(**locals_to_params(locals())) async def check( self, selector: str, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.check(**locals_to_params(locals())) async def uncheck( self, selector: str, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.uncheck(**locals_to_params(locals())) async def wait_for_timeout(self, timeout: float) -> None: await self._main_frame.wait_for_timeout(timeout) async def wait_for_function( self, expression: str, arg: Serializable = None, timeout: float = None, polling: Union[float, Literal["raf"]] = None, ) -> JSHandle: return await self._main_frame.wait_for_function(**locals_to_params(locals())) @property def workers(self) -> List["Worker"]: return self._workers.copy() @property def request(self) -> "APIRequestContext": return self.context.request async def pause(self) -> None: default_navigation_timeout = ( self._browser_context._timeout_settings.default_navigation_timeout() ) default_timeout = self._browser_context._timeout_settings.default_timeout() self._browser_context.set_default_navigation_timeout(0) self._browser_context.set_default_timeout(0) try: await asyncio.wait( [ asyncio.create_task( self._browser_context._channel.send("pause", None) ), self._closed_or_crashed_future, ], return_when=asyncio.FIRST_COMPLETED, ) finally: self._browser_context._set_default_navigation_timeout_impl( default_navigation_timeout ) self._browser_context._set_default_timeout_impl(default_timeout) async def pdf( self, scale: float = None, displayHeaderFooter: bool = None, headerTemplate: str = None, footerTemplate: str = None, printBackground: bool = None, landscape: bool = None, pageRanges: str = None, format: str = None, width: Union[str, float] = None, height: Union[str, float] = None, preferCSSPageSize: bool = None, margin: PdfMargins = None, path: Union[str, Path] = None, outline: bool = None, tagged: bool = None, ) -> bytes: params = locals_to_params(locals()) if "path" in params: del params["path"] encoded_binary = await self._channel.send("pdf", None, params) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) await async_writefile(path, decoded_binary) return decoded_binary def _force_video(self) -> Video: if not self._video: self._video = Video(self) return self._video @property def video( self, ) -> Optional[Video]: # Note: we are creating Video object lazily, because we do not know # BrowserContextOptions when constructing the page - it is assigned # too late during launchPersistentContext. if not self._browser_context._videos_dir: return None return self._force_video() def _close_error_with_reason(self) -> TargetClosedError: return TargetClosedError( self._close_reason or self._browser_context._effective_close_reason() ) def expect_event( self, event: str, predicate: Callable = None, timeout: float = None, ) -> EventContextManagerImpl: return self._expect_event( event, predicate, timeout, f'waiting for event "{event}"' ) def _expect_event( self, event: str, predicate: Callable = None, timeout: float = None, log_line: str = None, ) -> EventContextManagerImpl: if timeout is None: timeout = self._timeout_settings.timeout() waiter = Waiter(self, f"page.expect_event({event})") waiter.reject_on_timeout( timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' ) if log_line: waiter.log(log_line) if event != Page.Events.Crash: waiter.reject_on_event(self, Page.Events.Crash, Error("Page crashed")) if event != Page.Events.Close: waiter.reject_on_event( self, Page.Events.Close, lambda: self._close_error_with_reason() ) waiter.wait_for_event(self, event, predicate) return EventContextManagerImpl(waiter.result()) def expect_console_message( self, predicate: Callable[[ConsoleMessage], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[ConsoleMessage]: return self.expect_event(Page.Events.Console, predicate, timeout) def expect_download( self, predicate: Callable[[Download], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[Download]: return self.expect_event(Page.Events.Download, predicate, timeout) def expect_file_chooser( self, predicate: Callable[[FileChooser], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[FileChooser]: return self.expect_event(Page.Events.FileChooser, predicate, timeout) def expect_navigation( self, url: URLMatch = None, waitUntil: DocumentLoadState = None, timeout: float = None, ) -> EventContextManagerImpl[Response]: return self.main_frame.expect_navigation(url, waitUntil, timeout) def expect_popup( self, predicate: Callable[["Page"], bool] = None, timeout: float = None, ) -> EventContextManagerImpl["Page"]: return self.expect_event(Page.Events.Popup, predicate, timeout) def expect_request( self, urlOrPredicate: URLMatchRequest, timeout: float = None, ) -> EventContextManagerImpl[Request]: def my_predicate(request: Request) -> bool: if not callable(urlOrPredicate): return url_matches( self._browser_context._base_url, request.url, urlOrPredicate, ) return urlOrPredicate(request) trimmed_url = trim_url(urlOrPredicate) log_line = f"waiting for request {trimmed_url}" if trimmed_url else None return self._expect_event( Page.Events.Request, predicate=my_predicate, timeout=timeout, log_line=log_line, ) def expect_request_finished( self, predicate: Callable[["Request"], bool] = None, timeout: float = None, ) -> EventContextManagerImpl[Request]: return self.expect_event( Page.Events.RequestFinished, predicate=predicate, timeout=timeout ) def expect_response( self, urlOrPredicate: URLMatchResponse, timeout: float = None, ) -> EventContextManagerImpl[Response]: def my_predicate(request: Response) -> bool: if not callable(urlOrPredicate): return url_matches( self._browser_context._base_url, request.url, urlOrPredicate, ) return urlOrPredicate(request) trimmed_url = trim_url(urlOrPredicate) log_line = f"waiting for response {trimmed_url}" if trimmed_url else None return self._expect_event( Page.Events.Response, predicate=my_predicate, timeout=timeout, log_line=log_line, ) def expect_websocket( self, predicate: Callable[["WebSocket"], bool] = None, timeout: float = None, ) -> EventContextManagerImpl["WebSocket"]: return self.expect_event("websocket", predicate, timeout) def expect_worker( self, predicate: Callable[["Worker"], bool] = None, timeout: float = None, ) -> EventContextManagerImpl["Worker"]: return self.expect_event("worker", predicate, timeout) async def set_checked( self, selector: str, checked: bool, position: Position = None, timeout: float = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, trial: bool = None, ) -> None: if checked: await self.check( selector=selector, position=position, timeout=timeout, force=force, strict=strict, trial=trial, ) else: await self.uncheck( selector=selector, position=position, timeout=timeout, force=force, strict=strict, trial=trial, ) async def add_locator_handler( self, locator: "Locator", handler: Union[Callable[["Locator"], Any], Callable[[], Any]], noWaitAfter: bool = None, times: int = None, ) -> None: if locator._frame != self._main_frame: raise Error("Locator must belong to the main frame of this page") if times == 0: return uid = await self._channel.send( "registerLocatorHandler", None, { "selector": locator._selector, "noWaitAfter": noWaitAfter, }, ) self._locator_handlers[uid] = LocatorHandler( handler=handler, times=times, locator=locator ) async def _on_locator_handler_triggered(self, uid: str) -> None: remove = False try: handler = self._locator_handlers.get(uid) if handler and handler.times != 0: if handler.times is not None: handler.times -= 1 if self._dispatcher_fiber: handler_finished_future = self._loop.create_future() def _handler() -> None: try: handler() handler_finished_future.set_result(None) except Exception as e: handler_finished_future.set_exception(e) g = LocatorHandlerGreenlet(_handler) g.switch() await handler_finished_future else: coro_or_future = handler() if coro_or_future: await coro_or_future remove = handler.times == 0 finally: if remove: del self._locator_handlers[uid] try: await self._connection.wrap_api_call( lambda: self._channel.send( "resolveLocatorHandlerNoReply", None, {"uid": uid, "remove": remove}, ), is_internal=True, ) except Error: pass async def remove_locator_handler(self, locator: "Locator") -> None: for uid, data in self._locator_handlers.copy().items(): if data.locator._equals(locator): del self._locator_handlers[uid] self._channel.send_no_reply( "unregisterLocatorHandler", None, {"uid": uid}, ) async def requests(self) -> List[Request]: request_objects = await self._channel.send("requests", None) return [from_channel(r) for r in request_objects] async def console_messages(self) -> List[ConsoleMessage]: message_dicts = await self._channel.send("consoleMessages", None) return [ ConsoleMessage( {**event, "page": self._channel}, self._loop, self._dispatcher_fiber ) for event in message_dicts ] async def page_errors(self) -> List[Error]: error_objects = await self._channel.send("pageErrors", None) return [parse_error(error["error"]) for error in error_objects] class Worker(ChannelOwner): Events = SimpleNamespace(Close="close", Console="console") def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._set_event_to_subscription_mapping({Worker.Events.Console: "console"}) self._channel.on("close", lambda _: self._on_close()) self._page: Optional[Page] = None self._context: Optional["BrowserContext"] = None def __repr__(self) -> str: return f"" def _on_close(self) -> None: if self._page: self._page._workers.remove(self) if self._context: self._context._service_workers.remove(self) self.emit(Worker.Events.Close, self) @property def url(self) -> str: return self._initializer["url"] async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) async def evaluate_handle( self, expression: str, arg: Serializable = None ) -> JSHandle: return from_channel( await self._channel.send( "evaluateExpressionHandle", None, dict( expression=expression, arg=serialize_argument(arg), ), ) ) def expect_event( self, event: str, predicate: Callable = None, timeout: float = None, ) -> EventContextManagerImpl: if timeout is None: if self._page: timeout = self._page._timeout_settings.timeout() elif self._context: timeout = self._context._timeout_settings.timeout() else: timeout = 30000 waiter = Waiter(self, f"worker.expect_event({event})") waiter.reject_on_timeout( cast(float, timeout), f'Timeout {timeout}ms exceeded while waiting for event "{event}"', ) if event != Worker.Events.Close: waiter.reject_on_event( self, Worker.Events.Close, lambda: TargetClosedError() ) waiter.wait_for_event(self, event, predicate) return EventContextManagerImpl(waiter.result()) class BindingCall(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) async def call(self, func: Callable) -> None: try: frame = from_channel(self._initializer["frame"]) source = dict(context=frame._page.context, page=frame._page, frame=frame) if self._initializer.get("handle"): result = func(source, from_channel(self._initializer["handle"])) else: func_args = list(map(parse_result, self._initializer["args"])) result = func(source, *func_args) if inspect.iscoroutine(result): result = await result await self._channel.send( "resolve", None, dict(result=serialize_argument(result)) ) except Exception as e: tb = sys.exc_info()[2] asyncio.create_task( self._channel.send( "reject", None, dict(error=dict(error=serialize_error(e, tb))) ) ) def trim_url(param: Union[URLMatchRequest, URLMatchResponse]) -> Optional[str]: if isinstance(param, re.Pattern): return trim_end(param.pattern) if isinstance(param, str): return trim_end(param) return None def trim_end(s: str) -> str: if len(s) > 50: return s[:50] + "\u2026" return s ================================================ FILE: playwright/_impl/_path_utils.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect from pathlib import Path from types import FrameType from typing import cast def get_file_dirname() -> Path: """Returns the callee (`__file__`) directory name""" frame = cast(FrameType, inspect.currentframe()).f_back module = inspect.getmodule(frame) assert module assert module.__file__ return Path(module.__file__).parent.absolute() ================================================ FILE: playwright/_impl/_playwright.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict from playwright._impl._browser_type import BrowserType from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._fetch import APIRequest from playwright._impl._selectors import Selectors class Playwright(ChannelOwner): devices: Dict selectors: Selectors chromium: BrowserType firefox: BrowserType webkit: BrowserType request: APIRequest def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self.request = APIRequest(self) self.chromium = from_channel(initializer["chromium"]) self.chromium._playwright = self self.firefox = from_channel(initializer["firefox"]) self.firefox._playwright = self self.webkit = from_channel(initializer["webkit"]) self.webkit._playwright = self self.selectors = Selectors(self._loop, self._dispatcher_fiber) self.devices = self._connection.local_utils.devices def __getitem__(self, value: str) -> "BrowserType": if value == "chromium": return self.chromium elif value == "firefox": return self.firefox elif value == "webkit": return self.webkit raise ValueError("Invalid browser " + value) def _set_selectors(self, selectors: Selectors) -> None: self.selectors = selectors async def stop(self) -> None: pass ================================================ FILE: playwright/_impl/_selectors.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from pathlib import Path from typing import Any, Dict, List, Optional, Set, Union from playwright._impl._browser_context import BrowserContext from playwright._impl._errors import Error from playwright._impl._helper import async_readfile from playwright._impl._locator import set_test_id_attribute_name class Selectors: def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None: self._loop = loop self._contexts_for_selectors: Set[BrowserContext] = set() self._selector_engines: List[Dict] = [] self._dispatcher_fiber = dispatcher_fiber self._test_id_attribute_name: Optional[str] = None async def register( self, name: str, script: str = None, path: Union[str, Path] = None, contentScript: bool = None, ) -> None: if any(engine for engine in self._selector_engines if engine["name"] == name): raise Error( f'Selectors.register: "{name}" selector engine has been already registered' ) if not script and not path: raise Error("Either source or path should be specified") if path: script = (await async_readfile(path)).decode() engine: Dict[str, Any] = dict(name=name, source=script) if contentScript: engine["contentScript"] = contentScript for context in self._contexts_for_selectors: await context._channel.send( "registerSelectorEngine", None, {"selectorEngine": engine}, ) self._selector_engines.append(engine) def set_test_id_attribute(self, attributeName: str) -> None: set_test_id_attribute_name(attributeName) self._test_id_attribute_name = attributeName for context in self._contexts_for_selectors: context._channel.send_no_reply( "setTestIdAttributeName", None, {"testIdAttributeName": attributeName}, ) ================================================ FILE: playwright/_impl/_set_input_files_helpers.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import collections.abc import os import stat from pathlib import Path from typing import ( TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, TypedDict, Union, cast, ) from playwright._impl._connection import Channel, from_channel from playwright._impl._helper import Error from playwright._impl._writable_stream import WritableStream if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext from playwright._impl._api_structures import FilePayload SIZE_LIMIT_IN_BYTES = 50 * 1024 * 1024 class InputFilesList(TypedDict, total=False): streams: Optional[List[Channel]] directoryStream: Optional[Channel] localDirectory: Optional[str] localPaths: Optional[List[str]] payloads: Optional[List[Dict[str, Union[str, bytes]]]] def _list_files(directory: str) -> List[str]: files = [] for root, _, filenames in os.walk(directory): for filename in filenames: files.append(os.path.join(root, filename)) return files async def convert_input_files( files: Union[ str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] ], context: "BrowserContext", ) -> InputFilesList: items = ( files if isinstance(files, collections.abc.Sequence) and not isinstance(files, str) else [files] ) if any([isinstance(item, (str, Path)) for item in items]): if not all([isinstance(item, (str, Path)) for item in items]): raise Error("File paths cannot be mixed with buffers") (local_paths, local_directory) = resolve_paths_and_directory_for_input_files( cast(Sequence[Union[str, Path]], items) ) if context._channel._connection.is_remote: files_to_stream = cast( List[str], (_list_files(local_directory) if local_directory else local_paths), ) streams = [] result = await context._connection.wrap_api_call( lambda: context._channel.send_return_as_dict( "createTempFiles", None, { "rootDirName": ( os.path.basename(local_directory) if local_directory else None ), "items": list( map( lambda file: dict( name=( os.path.relpath(file, local_directory) if local_directory else os.path.basename(file) ), lastModifiedMs=int(os.path.getmtime(file) * 1000), ), files_to_stream, ) ), }, ) ) for i, file in enumerate(result["writableStreams"]): stream: WritableStream = from_channel(file) await stream.copy(files_to_stream[i]) streams.append(stream._channel) return InputFilesList( streams=None if local_directory else streams, directoryStream=result.get("rootDir"), ) return InputFilesList(localPaths=local_paths, localDirectory=local_directory) file_payload_exceeds_size_limit = ( sum([len(f.get("buffer", "")) for f in items if not isinstance(f, (str, Path))]) > SIZE_LIMIT_IN_BYTES ) if file_payload_exceeds_size_limit: raise Error( "Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead." ) return InputFilesList( payloads=[ { "name": item["name"], "mimeType": item["mimeType"], "buffer": base64.b64encode(item["buffer"]).decode(), } for item in cast(List[FilePayload], items) ] ) def resolve_paths_and_directory_for_input_files( items: Sequence[Union[str, Path]], ) -> Tuple[Optional[List[str]], Optional[str]]: local_paths: Optional[List[str]] = None local_directory: Optional[str] = None for item in items: item_stat = os.stat(item) # Raises FileNotFoundError if doesn't exist if stat.S_ISDIR(item_stat.st_mode): if local_directory: raise Error("Multiple directories are not supported") local_directory = str(Path(item).resolve()) else: local_paths = local_paths or [] local_paths.append(str(Path(item).resolve())) if local_paths and local_directory: raise Error("File paths must be all files or a single directory") return (local_paths, local_directory) ================================================ FILE: playwright/_impl/_str_utils.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import re from typing import Pattern, Union def escape_regex_flags(pattern: Pattern) -> str: flags = "" if pattern.flags != 0: flags = "" if (pattern.flags & int(re.IGNORECASE)) != 0: flags += "i" if (pattern.flags & int(re.DOTALL)) != 0: flags += "s" if (pattern.flags & int(re.MULTILINE)) != 0: flags += "m" assert ( pattern.flags & ~(int(re.MULTILINE) | int(re.IGNORECASE) | int(re.DOTALL) | int(re.UNICODE)) == 0 ), "Unexpected re.Pattern flag, only MULTILINE, IGNORECASE and DOTALL are supported." return flags def escape_for_regex(text: str) -> str: return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text) def escape_regex_for_selector(text: Pattern) -> str: # Even number of backslashes followed by the quote -> insert a backslash. return ( "/" + re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace( ">>", "\\>\\>" ) + "/" + escape_regex_flags(text) ) def escape_for_text_selector( text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None ) -> str: if isinstance(text, Pattern): return escape_regex_for_selector(text) return json.dumps(text) + ("s" if exact else "i") def escape_for_attribute_selector( value: Union[str, Pattern], exact: bool = None ) -> str: if isinstance(value, Pattern): return escape_regex_for_selector(value) # TODO: this should actually be # cssEscape(value).replace(/\\ /g, ' ') # However, our attribute selectors do not conform to CSS parsing spec, # so we escape them differently. return ( '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' + ("s" if exact else "i") ) ================================================ FILE: playwright/_impl/_stream.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 from pathlib import Path from typing import Dict, Union from playwright._impl._connection import ChannelOwner class Stream(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) async def save_as(self, path: Union[str, Path]) -> None: file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) while True: binary = await self._channel.send("read", None, {"size": 1024 * 1024}) if not binary: break await self._loop.run_in_executor( None, lambda: file.write(base64.b64decode(binary)) ) await self._loop.run_in_executor(None, lambda: file.close()) async def read_all(self) -> bytes: binary = b"" while True: chunk = await self._channel.send("read", None, {"size": 1024 * 1024}) if not chunk: break binary += base64.b64decode(chunk) return binary ================================================ FILE: playwright/_impl/_sync_base.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import inspect import traceback from contextlib import AbstractContextManager from types import TracebackType from typing import ( Any, Callable, Coroutine, Generator, Generic, Optional, Type, TypeVar, Union, cast, ) import greenlet from playwright._impl._helper import Error from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper mapping = ImplToApiMapping() T = TypeVar("T") Self = TypeVar("Self", bound="SyncContextManager") class EventInfo(Generic[T]): def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: self._sync_base = sync_base self._future = future g_self = greenlet.getcurrent() self._future.add_done_callback(lambda _: g_self.switch()) @property def value(self) -> T: while not self._future.done(): self._sync_base._dispatcher_fiber.switch() asyncio._set_running_loop(self._sync_base._loop) exception = self._future.exception() if exception: raise exception return cast(T, mapping.from_maybe_impl(self._future.result())) def _cancel(self) -> None: self._future.cancel() def is_done(self) -> bool: return self._future.done() class EventContextManager(Generic[T], AbstractContextManager): def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: self._event = EventInfo[T](sync_base, future) def __enter__(self) -> EventInfo[T]: return self._event def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: if exc_val: self._event._cancel() else: self._event.value class SyncBase(ImplWrapper): def __init__(self, impl_obj: Any) -> None: super().__init__(impl_obj) self._loop: asyncio.AbstractEventLoop = impl_obj._loop self._dispatcher_fiber = impl_obj._dispatcher_fiber def __str__(self) -> str: return self._impl_obj.__str__() def _sync( self, coro: Union[Coroutine[Any, Any, Any], Generator[Any, Any, Any]], ) -> Any: __tracebackhide__ = True if self._loop.is_closed(): coro.close() raise Error("Event loop is closed! Is Playwright already stopped?") g_self = greenlet.getcurrent() task: asyncio.tasks.Task[Any] = self._loop.create_task(coro) setattr(task, "__pw_stack__", inspect.stack(0)) setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) task.add_done_callback(lambda _: g_self.switch()) while not task.done(): self._dispatcher_fiber.switch() asyncio._set_running_loop(self._loop) return task.result() def _wrap_handler( self, handler: Union[Callable[..., Any], Any] ) -> Callable[..., None]: if callable(handler): return mapping.wrap_handler(handler) return handler def on(self, event: Any, f: Any) -> None: """Registers the function ``f`` to the event name ``event``.""" self._impl_obj.on(event, self._wrap_handler(f)) def once(self, event: Any, f: Any) -> None: """The same as ``self.on``, except that the listener is automatically removed after being called. """ self._impl_obj.once(event, self._wrap_handler(f)) def remove_listener(self, event: Any, f: Any) -> None: """Removes the function ``f`` from ``event``.""" self._impl_obj.remove_listener(event, self._wrap_handler(f)) class SyncContextManager(SyncBase): def __enter__(self: Self) -> Self: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], _traceback: Optional[TracebackType], ) -> None: self.close() def close(self) -> None: ... ================================================ FILE: playwright/_impl/_tracing.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib from typing import Dict, Optional, Union, cast from playwright._impl._api_structures import TracingGroupLocation from playwright._impl._artifact import Artifact from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._helper import locals_to_params class Tracing(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) self._include_sources: bool = False self._stacks_id: Optional[str] = None self._is_tracing: bool = False self._traces_dir: Optional[str] = None async def start( self, name: str = None, title: str = None, snapshots: bool = None, screenshots: bool = None, sources: bool = None, ) -> None: params = locals_to_params(locals()) self._include_sources = bool(sources) await self._channel.send("tracingStart", None, params) trace_name = await self._channel.send( "tracingStartChunk", None, {"title": title, "name": name} ) await self._start_collecting_stacks(trace_name) async def start_chunk(self, title: str = None, name: str = None) -> None: params = locals_to_params(locals()) trace_name = await self._channel.send("tracingStartChunk", None, params) await self._start_collecting_stacks(trace_name) async def _start_collecting_stacks(self, trace_name: str) -> None: if not self._is_tracing: self._is_tracing = True self._connection.set_is_tracing(True) self._stacks_id = await self._connection.local_utils.tracing_started( self._traces_dir, trace_name ) async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) async def stop(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) await self._channel.send( "tracingStop", None, ) async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None: self._reset_stack_counter() if not file_path: # Not interested in any artifacts await self._channel.send("tracingStopChunk", None, {"mode": "discard"}) if self._stacks_id: await self._connection.local_utils.trace_discarded(self._stacks_id) return is_local = not self._connection.is_remote if is_local: result = await self._channel.send_return_as_dict( "tracingStopChunk", None, {"mode": "entries"} ) await self._connection.local_utils.zip( { "zipFile": str(file_path), "entries": result["entries"], "stacksId": self._stacks_id, "mode": "write", "includeSources": self._include_sources, } ) return result = await self._channel.send_return_as_dict( "tracingStopChunk", None, { "mode": "archive", }, ) artifact = cast( Optional[Artifact], from_nullable_channel(result.get("artifact")), ) # The artifact may be missing if the browser closed while stopping tracing. if not artifact: if self._stacks_id: await self._connection.local_utils.trace_discarded(self._stacks_id) return # Save trace to the final local file. await artifact.save_as(file_path) await artifact.delete() await self._connection.local_utils.zip( { "zipFile": str(file_path), "entries": [], "stacksId": self._stacks_id, "mode": "append", "includeSources": self._include_sources, } ) def _reset_stack_counter(self) -> None: if self._is_tracing: self._is_tracing = False self._connection.set_is_tracing(False) async def group(self, name: str, location: TracingGroupLocation = None) -> None: await self._channel.send("tracingGroup", None, locals_to_params(locals())) async def group_end(self) -> None: await self._channel.send( "tracingGroupEnd", None, ) ================================================ FILE: playwright/_impl/_transport.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import io import json import os import subprocess import sys from abc import ABC, abstractmethod from typing import Callable, Dict, Optional, Union from playwright._impl._driver import compute_driver_executable, get_driver_env from playwright._impl._helper import ParsedMessagePayload # Sourced from: https://github.com/pytest-dev/pytest/blob/da01ee0a4bb0af780167ecd228ab3ad249511302/src/_pytest/faulthandler.py#L69-L77 def _get_stderr_fileno() -> Optional[int]: try: # when using pythonw, sys.stderr is None. # when Pyinstaller is used, there is no closed attribute because Pyinstaller monkey-patches it with a NullWriter class if sys.stderr is None or not hasattr(sys.stderr, "closed"): return None if sys.stderr.closed: return None return sys.stderr.fileno() except (NotImplementedError, AttributeError, io.UnsupportedOperation): # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors # This is potentially dangerous, but the best we can do. if not hasattr(sys, "__stderr__") or not sys.__stderr__: return None return sys.__stderr__.fileno() class Transport(ABC): def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self.on_message: Callable[[ParsedMessagePayload], None] = lambda _: None self.on_error_future: asyncio.Future = loop.create_future() @abstractmethod def request_stop(self) -> None: pass def dispose(self) -> None: pass @abstractmethod async def wait_until_stopped(self) -> None: pass @abstractmethod async def connect(self) -> None: pass @abstractmethod async def run(self) -> None: pass @abstractmethod def send(self, message: Dict) -> None: pass def serialize_message(self, message: Dict) -> bytes: msg = json.dumps(message) if "DEBUGP" in os.environ: # pragma: no cover print("\x1b[32mSEND>\x1b[0m", json.dumps(message, indent=2)) return msg.encode() def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload: obj = json.loads(data) if "DEBUGP" in os.environ: # pragma: no cover print("\x1b[33mRECV>\x1b[0m", json.dumps(obj, indent=2)) return obj class PipeTransport(Transport): def __init__(self, loop: asyncio.AbstractEventLoop) -> None: super().__init__(loop) self._stopped = False def request_stop(self) -> None: assert self._output self._stopped = True self._output.close() async def wait_until_stopped(self) -> None: await self._stopped_future async def connect(self) -> None: self._stopped_future: asyncio.Future = asyncio.Future() try: # For pyinstaller and Nuitka env = get_driver_env() if getattr(sys, "frozen", False) or globals().get("__compiled__"): env.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0") startupinfo = None if sys.platform == "win32": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE executable_path, entrypoint_path = compute_driver_executable() self._proc = await asyncio.create_subprocess_exec( executable_path, entrypoint_path, "run-driver", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=_get_stderr_fileno(), limit=32768, env=env, startupinfo=startupinfo, ) except Exception as exc: self.on_error_future.set_exception(exc) raise exc self._output = self._proc.stdin async def run(self) -> None: assert self._proc.stdout assert self._proc.stdin while not self._stopped: try: buffer = await self._proc.stdout.readexactly(4) if self._stopped: break length = int.from_bytes(buffer, byteorder="little", signed=False) buffer = bytes(0) while length: to_read = min(length, 32768) data = await self._proc.stdout.readexactly(to_read) if self._stopped: break length -= to_read if len(buffer): buffer = buffer + data else: buffer = data if self._stopped: break obj = self.deserialize_message(buffer) self.on_message(obj) except asyncio.IncompleteReadError: if not self._stopped: self.on_error_future.set_exception( Exception("Connection closed while reading from the driver") ) break await asyncio.sleep(0) await self._proc.communicate() self._stopped_future.set_result(None) def send(self, message: Dict) -> None: assert self._output data = self.serialize_message(message) self._output.write( len(data).to_bytes(4, byteorder="little", signed=False) + data ) ================================================ FILE: playwright/_impl/_video.py ================================================ # Copyright (c) Microsoft Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib from typing import TYPE_CHECKING, Union from playwright._impl._artifact import Artifact from playwright._impl._helper import Error if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page class Video: def __init__(self, page: "Page") -> None: self._loop = page._loop self._dispatcher_fiber = page._dispatcher_fiber self._page = page self._artifact_future = page._loop.create_future() if page.is_closed(): self._page_closed() else: page.on("close", lambda page: self._page_closed()) def __repr__(self) -> str: return f"