├── .azure-pipelines └── publish.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── config.yml │ ├── feature_request.md │ └── regression.md └── workflows │ ├── ci.yml │ ├── publish.yml │ ├── publish_canary_docker.yml │ ├── publish_release_docker.yml │ ├── test_docker.yml │ └── trigger_internal_tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROLLING.md ├── SECURITY.md ├── assets ├── creep_js.png └── nowsecure_nl.png ├── build_patched.py ├── conda_build_config.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 ├── main.py ├── main_sync.py ├── meta.yaml ├── patch_check.py ├── pyproject.toml ├── scripts ├── documentation_provider.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.py │ ├── consolelog.html │ ├── csp.html │ ├── digits │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── dom.html │ ├── download-blob.html │ ├── drag-n-drop.html │ ├── dummy_bad_browser_executable.js │ ├── empty.html │ ├── error.html │ ├── es6 │ │ ├── .eslintrc │ │ ├── es6import.js │ │ ├── es6module.js │ │ └── es6pathimport.js │ ├── file-to-upload-2.txt │ ├── file-to-upload.txt │ ├── frames │ │ ├── child-redirect.html │ │ ├── frame.html │ │ ├── frameset.html │ │ ├── nested-frames.html │ │ ├── one-frame.html │ │ ├── redirect-my-parent.html │ │ ├── script.js │ │ ├── style.css │ │ └── two-frames.html │ ├── geolocation.html │ ├── global-var.html │ ├── grid.html │ ├── har-fulfill.har │ ├── har-redirect.har │ ├── har-sha1-main-response.txt │ ├── har-sha1.har │ ├── har.html │ ├── historyapi.html │ ├── injectedfile.js │ ├── injectedstyle.css │ ├── input │ │ ├── animating-button.html │ │ ├── button.html │ │ ├── checkbox.html │ │ ├── fileupload-multi.html │ │ ├── fileupload.html │ │ ├── keyboard.html │ │ ├── mouse-helper.js │ │ ├── rotatedButton.html │ │ ├── scrollable.html │ │ ├── select.html │ │ ├── textarea.html │ │ └── touches.html │ ├── mobile.html │ ├── networkidle.html │ ├── networkidle.js │ ├── offscreenbuttons.html │ ├── one-style.css │ ├── one-style.html │ ├── playground.html │ ├── popup │ │ ├── popup.html │ │ └── window-open.html │ ├── pptr.png │ ├── react.html │ ├── react │ │ ├── react-dom@16.13.1.production.min.js │ │ └── react@16.13.1.production.min.js │ ├── sectionselectorengine.js │ ├── self-request.html │ ├── serviceworkers │ │ ├── empty │ │ │ ├── sw.html │ │ │ └── sw.js │ │ ├── fetch │ │ │ ├── style.css │ │ │ ├── sw.html │ │ │ └── sw.js │ │ └── fetchdummy │ │ │ ├── sw.html │ │ │ └── sw.js │ ├── shadow.html │ ├── simple-extension │ │ ├── content-script.js │ │ ├── index.js │ │ └── manifest.json │ ├── simple.json │ ├── title.html │ ├── worker │ │ ├── worker.html │ │ └── worker.js │ └── wrappedlink.html ├── async │ ├── __init__.py │ ├── conftest.py │ ├── test_accessibility.py │ ├── test_add_init_script.py │ ├── test_assertions.py │ ├── test_asyncio.py │ ├── test_browser.py │ ├── test_browsercontext.py │ ├── test_browsercontext_add_cookies.py │ ├── test_browsercontext_clearcookies.py │ ├── test_browsercontext_cookies.py │ ├── test_browsercontext_events.py │ ├── test_browsercontext_proxy.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_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_evaluate.py │ ├── test_expect_misc.py │ ├── test_fetch_browser_context.py │ ├── test_fetch_global.py │ ├── test_fill.py │ ├── test_focus.py │ ├── test_frames.py │ ├── test_geolocation.py │ ├── test_har.py │ ├── test_headful.py │ ├── test_ignore_https_errors.py │ ├── test_input.py │ ├── test_interception.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_base_url.py │ ├── test_page_network_request.py │ ├── test_page_network_response.py │ ├── test_page_request_fallback.py │ ├── test_page_request_intercept.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_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_video.py │ ├── test_wait_for_function.py │ ├── test_wait_for_url.py │ ├── test_websocket.py │ ├── test_worker.py │ └── utils.py ├── common │ ├── __init__.py │ ├── test_collect_handles.py │ ├── test_events.py │ ├── test_signals.py │ └── test_threads.py ├── conftest.py ├── golden-chromium │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── golden-firefox │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── golden-webkit │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── server.py ├── sync │ ├── __init__.py │ ├── conftest.py │ ├── test_accessibility.py │ ├── test_add_init_script.py │ ├── test_assertions.py │ ├── test_browser.py │ ├── test_browsercontext_events.py │ ├── test_browsercontext_request_fallback.py │ ├── test_browsercontext_request_intercept.py │ ├── test_browsercontext_service_worker_policy.py │ ├── test_browsercontext_storage_state.py │ ├── test_browsertype_connect.py │ ├── test_browsertype_connect_cdp.py │ ├── test_cdp_session.py │ ├── test_check.py │ ├── test_console.py │ ├── test_context_manager.py │ ├── test_element_handle.py │ ├── test_element_handle_wait_for_element_state.py │ ├── test_expect_misc.py │ ├── test_fetch_browser_context.py │ ├── test_fetch_global.py │ ├── test_fill.py │ ├── test_har.py │ ├── test_input.py │ ├── test_listeners.py │ ├── test_locator_get_by.py │ ├── test_locators.py │ ├── test_network.py │ ├── test_page.py │ ├── test_page_network_response.py │ ├── test_page_request_fallback.py │ ├── test_page_request_intercept.py │ ├── test_page_select_option.py │ ├── test_pdf.py │ ├── test_queryselector.py │ ├── test_request_fulfill.py │ ├── test_request_intercept.py │ ├── test_resource_timing.py │ ├── test_selectors_misc.py │ ├── test_sync.py │ ├── test_tap.py │ ├── test_tracing.py │ ├── test_video.py │ └── utils.py ├── test_installation.py ├── test_reference_count_async.py ├── testserver │ ├── cert.pem │ └── key.pem └── utils.py ├── undetected_playwright ├── __init__.py ├── __main__.py ├── _impl │ ├── __init__.py │ ├── __pyinstaller │ │ ├── __init__.py │ │ ├── hook-playwright.async_api.py │ │ └── hook-playwright.sync_api.py │ ├── _accessibility.py │ ├── _api_structures.py │ ├── _artifact.py │ ├── _assertions.py │ ├── _async_base.py │ ├── _browser.py │ ├── _browser_context.py │ ├── _browser_type.py │ ├── _cdp_session.py │ ├── _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 │ ├── _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 ├── upload.md └── utils ├── docker ├── Dockerfile.focal ├── Dockerfile.jammy ├── build.sh └── publish_docker.sh └── linting └── check_file_header.py /.azure-pipelines/publish.yml: -------------------------------------------------------------------------------- 1 | # don't trigger for Pull Requests 2 | pr: none 3 | 4 | trigger: 5 | tags: 6 | include: 7 | - '*' 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | steps: 13 | - task: UsePythonVersion@0 14 | inputs: 15 | versionSpec: '3.8' 16 | displayName: 'Use Python' 17 | 18 | - script: | 19 | python -m pip install --upgrade pip 20 | pip install -r local-requirements.txt 21 | pip install -e . 22 | python setup.py bdist_wheel --all 23 | displayName: 'Install & Build' 24 | 25 | - task: EsrpRelease@4 26 | inputs: 27 | ConnectedServiceName: 'Playwright-ESRP' 28 | Intent: 'PackageDistribution' 29 | ContentType: 'PyPi' 30 | ContentSource: 'Folder' 31 | FolderLocation: './dist/' 32 | WaitForReleaseCompletion: true 33 | Owners: 'maxschmitt@microsoft.com' 34 | Approvers: 'maxschmitt@microsoft.com' 35 | ServiceEndpointUrl: 'https://api.esrp.microsoft.com' 36 | MainPublisher: 'Playwright' 37 | DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47' 38 | displayName: 'ESRP Release to PIP' 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # text files must be lf for golden file tests to work 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Something doesn't work like it should? Tell us! 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ### System info 17 | - Playwright Version: [v1.XX] 18 | - Operating System: [All, Windows 11, Ubuntu 20, macOS 13.2, etc.] 19 | - Browser: [All, Chromium, Firefox, WebKit] 20 | - Other info: 21 | 22 | ### Source code 23 | 24 | - [ ] I provided exact source code that allows reproducing the issue locally. 25 | 26 | 27 | 28 | 29 | 30 | 31 | **Link to the GitHub repository with the repro** 32 | 33 | [https://github.com/your_profile/playwright_issue_title] 34 | 35 | or 36 | 37 | **Test file (self-contained)** 38 | 39 | ```python 40 | from undetected_playwright.sync_api import sync_playwright 41 | 42 | with sync_playwright() as p: 43 | browser = p.chromium.launch() 44 | page = browser.new_page() 45 | # ... 46 | browser.close() 47 | ``` 48 | 49 | **Steps** 50 | - [Run the test] 51 | - [...] 52 | 53 | **Expected** 54 | 55 | [Describe expected behavior] 56 | 57 | **Actual** 58 | 59 | [Describe actual behavior] 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Join our Discord Server 3 | url: https://aka.ms/playwright/discord 4 | about: Ask questions and discuss with other community members 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request new features to be added 4 | title: "[Feature]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Let us know what functionality you'd like to see in Playwright and what your use case is. 11 | Do you think others might benefit from this as well? 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report regression 3 | about: Functionality that used to work and does not any more 4 | title: "[REGRESSION]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Context:** 11 | - GOOD Playwright Version: [what Playwright version worked nicely?] 12 | - BAD Playwright Version: [what Playwright version doesn't work any more?] 13 | - Operating System: [e.g. Windows, Linux or Mac] 14 | - Extra: [any specific details about your environment] 15 | 16 | **Code Snippet** 17 | 18 | Help us help you! Put down a short code snippet that illustrates your bug and 19 | that we can run and debug locally. For example: 20 | 21 | ```python 22 | from undetected_playwright.sync_api import sync_playwright 23 | 24 | with sync_playwright() as p: 25 | browser = p.chromium.launch() 26 | page = browser.new_page() 27 | # ... 28 | browser.close() 29 | ``` 30 | 31 | **Describe the bug** 32 | 33 | Add any other details about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy-conda: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macos-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Get conda 16 | uses: conda-incubator/setup-miniconda@v2 17 | with: 18 | python-version: 3.9 19 | channels: conda-forge 20 | - name: Prepare 21 | run: conda install anaconda-client conda-build conda-verify 22 | - name: Build and Upload 23 | env: 24 | ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }} 25 | run: | 26 | conda config --set anaconda_upload yes 27 | conda build --user microsoft . 28 | -------------------------------------------------------------------------------- /.github/workflows/publish_canary_docker.yml: -------------------------------------------------------------------------------- 1 | name: "publish canary docker" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "10 0 * * *" 7 | 8 | jobs: 9 | publish-canary: 10 | name: "Publish canary Docker" 11 | runs-on: ubuntu-20.04 12 | if: github.repository == 'microsoft/undetected_playwright-python' 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | - name: Install dependencies & browsers 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r local-requirements.txt 23 | pip install -e . 24 | - uses: azure/docker-login@v1 25 | with: 26 | login-server: undetected_playwright.azurecr.io 27 | username: undetected_playwright 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | - name: Set up Docker QEMU for arm64 docker builds 30 | uses: docker/setup-qemu-action@v2 31 | with: 32 | platforms: arm64 33 | - name: publish docker canary 34 | run: ./utils/docker/publish_docker.sh canary 35 | -------------------------------------------------------------------------------- /.github/workflows/publish_release_docker.yml: -------------------------------------------------------------------------------- 1 | name: "publish release - Docker" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | is_release: 7 | required: false 8 | type: boolean 9 | description: "Is this a release image?" 10 | 11 | release: 12 | types: [published] 13 | 14 | jobs: 15 | publish-docker-release: 16 | name: "publish to DockerHub" 17 | runs-on: ubuntu-20.04 18 | if: github.repository == 'microsoft/undetected_playwright-python' 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: "3.10" 25 | - uses: azure/docker-login@v1 26 | with: 27 | login-server: undetected_playwright.azurecr.io 28 | username: undetected_playwright 29 | password: ${{ secrets.DOCKER_PASSWORD }} 30 | - name: Set up Docker QEMU for arm64 docker builds 31 | uses: docker/setup-qemu-action@v2 32 | with: 33 | platforms: arm64 34 | - name: Install dependencies & browsers 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -r local-requirements.txt 38 | pip install -e . 39 | - run: ./utils/docker/publish_docker.sh stable 40 | if: (github.event_name != 'workflow_dispatch' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release == 'true') 41 | - run: ./utils/docker/publish_docker.sh canary 42 | if: (github.event_name != 'workflow_dispatch' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release != 'true') 43 | -------------------------------------------------------------------------------- /.github/workflows/test_docker.yml: -------------------------------------------------------------------------------- 1 | name: Test Docker 2 | on: 3 | push: 4 | paths: 5 | - '.github/workflows/test_docker.yml' 6 | - 'setup.py' 7 | - '**/Dockerfile.*' 8 | branches: 9 | - main 10 | - release-* 11 | pull_request: 12 | paths: 13 | - '.github/workflows/test_docker.yml' 14 | - 'setup.py' 15 | - '**/Dockerfile.*' 16 | branches: 17 | - main 18 | - release-* 19 | jobs: 20 | build: 21 | timeout-minutes: 120 22 | runs-on: ubuntu-22.04 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | docker-image-variant: 27 | - focal 28 | - jammy 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Set up Python 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: "3.10" 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -r local-requirements.txt 39 | pip install -e . 40 | - name: Build Docker image 41 | run: bash utils/docker/build.sh --amd64 ${{ matrix.docker-image-variant }} undetected_playwright-python:localbuild-${{ matrix.docker-image-variant }} 42 | - name: Test 43 | run: | 44 | CONTAINER_ID="$(docker run --rm -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" 45 | # Fix permissions for Git inside the container 46 | docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright 47 | docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt 48 | docker exec "${CONTAINER_ID}" pip install -e . 49 | docker exec "${CONTAINER_ID}" python setup.py bdist_wheel 50 | docker exec "${CONTAINER_ID}" xvfb-run pytest -vv tests/sync/ 51 | docker exec "${CONTAINER_ID}" xvfb-run pytest -vv tests/async/ 52 | -------------------------------------------------------------------------------- /.github/workflows/trigger_internal_tests.yml: -------------------------------------------------------------------------------- 1 | name: "Internal Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | 9 | jobs: 10 | trigger: 11 | name: "trigger" 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - run: | 15 | curl -X POST \ 16 | -H "Accept: application/vnd.github.v3+json" \ 17 | -H "Authorization: token ${GH_TOKEN}" \ 18 | --data "{\"event_type\": \"playwright_tests_python\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \ 19 | https://api.github.com/repos/microsoft/playwright-browsers/dispatches 20 | env: 21 | GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | driver/ 3 | undetected_playwright/driver/ 4 | playwright.egg-info/ 5 | build/ 6 | dist/ 7 | venv/ 8 | .idea/ 9 | **/*.pyc 10 | env/ 11 | htmlcov/ 12 | .coverage* 13 | .DS_Store 14 | .vscode/ 15 | .eggs 16 | _repo_version.py 17 | coverage.xml 18 | junit/ 19 | htmldocs/ 20 | utils/docker/dist/ 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | exclude: tests/assets/har-sha1-main-response.txt 10 | - id: check-yaml 11 | - id: check-toml 12 | - id: requirements-txt-fixer 13 | - id: check-ast 14 | - id: check-builtin-literals 15 | - id: check-executables-have-shebangs 16 | - id: check-merge-conflict 17 | - repo: https://github.com/psf/black 18 | rev: 23.9.1 19 | hooks: 20 | - id: black 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.5.1 23 | hooks: 24 | - id: mypy 25 | additional_dependencies: [types-pyOpenSSL==23.2.0.2, types-requests==2.31.0.10] 26 | - repo: https://github.com/pycqa/flake8 27 | rev: 6.1.0 28 | hooks: 29 | - id: flake8 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.12.0 32 | hooks: 33 | - id: isort 34 | - repo: local 35 | hooks: 36 | - id: pyright 37 | name: pyright 38 | entry: pyright 39 | language: node 40 | pass_filenames: false 41 | types: [python] 42 | additional_dependencies: ["pyright@1.1.278"] 43 | - repo: local 44 | hooks: 45 | - id: check-license-header 46 | name: Check License Header 47 | entry: ./utils/linting/check_file_header.py 48 | language: python 49 | types: [python] 50 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to Contribute 4 | 5 | ### Configuring python environment 6 | 7 | The project development requires Python version 3.9+. To set it as default in the environment run the following commands: 8 | 9 | ```sh 10 | # You may need to install python 3.9 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.9-venv` 11 | python3.9 -m venv env 12 | source ./env/bin/activate 13 | ``` 14 | 15 | Install required dependencies: 16 | 17 | ```sh 18 | python -m pip install --upgrade pip 19 | pip install -r local-requirements.txt 20 | ``` 21 | 22 | Build and install drivers: 23 | 24 | ```sh 25 | pip install -e . 26 | python setup.py bdist_wheel 27 | # For all platforms 28 | python setup.py bdist_wheel --all 29 | ``` 30 | 31 | Run tests: 32 | 33 | ```sh 34 | pytest --browser chromium 35 | ``` 36 | 37 | Checking for typing errors 38 | 39 | ```sh 40 | mypy undetected_playwright 41 | ``` 42 | 43 | Format the code 44 | 45 | ```sh 46 | pre-commit install 47 | pre-commit run --all-files 48 | ``` 49 | 50 | For more details look at the [CI configuration](./blob/main/.github/workflows/ci.yml). 51 | 52 | Collect coverage 53 | 54 | ```sh 55 | pytest --browser chromium --cov-report html --cov=undetected_playwright 56 | open htmlcov/index.html 57 | ``` 58 | 59 | ### Regenerating APIs 60 | 61 | ```bash 62 | ./scripts/update_api.sh 63 | pre-commit run --all-files 64 | ``` 65 | 66 | ## Contributor License Agreement 67 | 68 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 69 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 70 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 71 | 72 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 73 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 74 | provided by the bot. You will only need to do this once across all repos using our CLA. 75 | 76 | ## Code of Conduct 77 | 78 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 79 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 80 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 81 | -------------------------------------------------------------------------------- /ROLLING.md: -------------------------------------------------------------------------------- 1 | # Rolling Playwright-Python to the latest Playwright driver 2 | 3 | * checkout repo: `git clone https://github.com/microsoft/playwright-python` 4 | * make sure local python is 3.9 5 | * create virtual environment, if don't have one: `python -m venv env` 6 | * activate venv: `source env/bin/activate` 7 | * install all deps: 8 | - `python -m pip install --upgrade pip` 9 | - `pip install -r local-requirements.txt` 10 | - `pre-commit install` 11 | - `pip install -e .` 12 | * change driver version in `setup.py` 13 | * download new driver: `python setup.py bdist_wheel` 14 | * generate API: `./scripts/update_api.sh` 15 | * commit changes & send PR 16 | * wait for bots to pass & merge the PR 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | 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/). 6 | 7 | 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. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | 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). 16 | 17 | 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). 18 | 19 | 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: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | 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. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/creep_js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/assets/creep_js.png -------------------------------------------------------------------------------- /assets/nowsecure_nl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/assets/nowsecure_nl.png -------------------------------------------------------------------------------- /conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | - 3.8 3 | - 3.9 4 | - "3.10" 5 | - "3.11" 6 | - "3.12" 7 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/examples/todomvc/mvctests/__init__.py -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_clear_completed_button.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from undetected_playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, create_default_todos 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | create_default_todos(page) 28 | # run the actual test 29 | yield 30 | # run any cleanup code 31 | 32 | 33 | def test_should_display_the_correct_text(page: Page) -> None: 34 | page.locator(".todo-list li .toggle").first.check() 35 | expect(page.locator(".clear-completed")).to_have_text("Clear completed") 36 | 37 | 38 | def test_should_clear_completed_items_when_clicked(page: Page) -> None: 39 | todo_items = page.locator(".todo-list li") 40 | todo_items.nth(1).locator(".toggle").check() 41 | page.locator(".clear-completed").click() 42 | expect(todo_items).to_have_count(2) 43 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[2]]) 44 | 45 | 46 | def test_should_be_hidden_when_there_are_no_items_that_are_completed( 47 | page: Page, 48 | ) -> None: 49 | page.locator(".todo-list li .toggle").first.check() 50 | page.locator(".clear-completed").click() 51 | expect(page.locator(".clear-completed")).to_be_hidden() 52 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_counter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from undetected_playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, assert_number_of_todos_in_local_storage 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | # run the actual test 28 | yield 29 | # run any cleanup code 30 | 31 | 32 | def test_should_display_the_current_number_of_todo_items(page: Page) -> None: 33 | page.locator(".new-todo").fill(TODO_ITEMS[0]) 34 | page.locator(".new-todo").press("Enter") 35 | expect(page.locator(".todo-count")).to_contain_text("1") 36 | 37 | page.locator(".new-todo").fill(TODO_ITEMS[1]) 38 | page.locator(".new-todo").press("Enter") 39 | expect(page.locator(".todo-count")).to_contain_text("2") 40 | 41 | assert_number_of_todos_in_local_storage(page, 2) 42 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_persistence.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from undetected_playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, check_number_of_completed_todos_in_local_storage 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | # run the actual test 28 | yield 29 | # run any cleanup code 30 | 31 | 32 | def test_should_persist_its_data(page: Page) -> None: 33 | for item in TODO_ITEMS[:2]: 34 | page.locator(".new-todo").fill(item) 35 | page.locator(".new-todo").press("Enter") 36 | 37 | todo_items = page.locator(".todo-list li") 38 | todo_items.nth(0).locator(".toggle").check() 39 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) 40 | expect(todo_items).to_have_class(["completed", ""]) 41 | 42 | # Ensure there is 1 completed item. 43 | check_number_of_completed_todos_in_local_storage(page, 1) 44 | 45 | # Now reload. 46 | page.reload() 47 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) 48 | expect(todo_items).to_have_class(["completed", ""]) 49 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from undetected_playwright.sync_api import Page 15 | 16 | TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] 17 | 18 | 19 | def create_default_todos(page: Page) -> None: 20 | for item in TODO_ITEMS: 21 | page.locator(".new-todo").fill(item) 22 | page.locator(".new-todo").press("Enter") 23 | 24 | 25 | def check_number_of_completed_todos_in_local_storage(page: Page, expected: int) -> None: 26 | assert ( 27 | page.evaluate( 28 | "JSON.parse(localStorage['react-todos']).filter(i => i.completed).length" 29 | ) 30 | == expected 31 | ) 32 | 33 | 34 | def assert_number_of_todos_in_local_storage(page: Page, expected: int) -> None: 35 | assert len(page.evaluate("JSON.parse(localStorage['react-todos'])")) == expected 36 | 37 | 38 | def check_todos_in_local_storage(page: Page, title: str) -> None: 39 | assert title in page.evaluate( 40 | "JSON.parse(localStorage['react-todos']).map(i => i.title)" 41 | ) 42 | -------------------------------------------------------------------------------- /examples/todomvc/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-playwright==0.3.0 2 | -------------------------------------------------------------------------------- /local-requirements.txt: -------------------------------------------------------------------------------- 1 | auditwheel==5.4.0 2 | autobahn==23.1.2 3 | black==23.9.1 4 | flake8==6.1.0 5 | flaky==3.7.0 6 | mypy==1.5.1 7 | objgraph==3.6.0 8 | Pillow==10.0.1 9 | pixelmatch==0.3.0 10 | pre-commit==3.4.0 11 | pyOpenSSL==23.2.0 12 | pytest==7.4.2 13 | pytest-asyncio==0.21.1 14 | pytest-cov==4.1.0 15 | pytest-repeat==0.9.1 16 | pytest-timeout==2.1.0 17 | pytest-xdist==3.3.1 18 | requests==2.31.0 19 | service_identity==23.1.0 20 | setuptools==68.2.2 21 | twisted==23.10.0 22 | types-pyOpenSSL==23.2.0.2 23 | types-requests==2.31.0.10 24 | wheel==0.41.2 25 | 26 | # undetected-playwright 27 | twine 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | # undetected-undetected_playwright here! 4 | from undetected_playwright.async_api import async_playwright, Playwright 5 | 6 | 7 | async def run(playwright: Playwright): 8 | args = [] 9 | args.append("--disable-blink-features=AutomationControlled") 10 | browser = await playwright.chromium.launch(headless=False, 11 | args=args) 12 | page = await browser.new_page() 13 | await page.goto("https://nowsecure.nl/#relax") 14 | input("Press ENTER to continue to Creep-JS:") 15 | await page.goto("https://nowsecure.nl/#relax") 16 | await page.goto("https://abrahamjuliot.github.io/creepjs/") 17 | input("Press ENTER to exit:") 18 | await browser.close() 19 | 20 | 21 | async def main(): 22 | async with async_playwright() as playwright: 23 | await run(playwright) 24 | 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /main_sync.py: -------------------------------------------------------------------------------- 1 | # undetected-undetected_playwright here! 2 | from undetected_playwright.sync_api import sync_playwright 3 | 4 | 5 | with sync_playwright() as p: 6 | args = [] 7 | args.append("--disable-blink-features=AutomationControlled") 8 | browser = p.chromium.launch(args=args, headless=False) 9 | page = browser.new_page() 10 | page.goto("https://nowsecure.nl/#relax") 11 | input("Press ENTER to continue to Creep-JS:") 12 | page.goto("https://nowsecure.nl/#relax") 13 | page.goto("https://abrahamjuliot.github.io/creepjs/") 14 | input("Press ENTER to exit:") 15 | browser.close() 16 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: undetected_playwright 3 | version: "{{ environ.get('GIT_DESCRIBE_TAG') | replace('v', '') }}" 4 | 5 | source: 6 | path: . 7 | 8 | build: 9 | number: 0 10 | script: "{{ PYTHON }} -m pip install . --no-deps -vv" 11 | skip: true # [py<37] 12 | binary_relocation: False 13 | missing_dso_whitelist: "*" 14 | entry_points: 15 | - undetected_playwright = undetected_playwright.__main__:main 16 | 17 | requirements: 18 | host: 19 | - python 20 | - wheel 21 | - pip 22 | - curl 23 | - setuptools_scm 24 | run: 25 | - python 26 | - greenlet ==3.0.1 27 | - pyee ==11.0.1 28 | - typing_extensions # [py<39] 29 | test: 30 | requires: 31 | - pip 32 | imports: 33 | - undetected_playwright 34 | - undetected_playwright.sync_api 35 | - undetected_playwright.async_api 36 | commands: 37 | - undetected_playwright --help 38 | 39 | about: 40 | home: https://github.com/microsoft/playwright-python 41 | license: Apache-2.0 42 | license_family: Apache 43 | license_file: LICENSE 44 | summary: Python version of the Playwright testing and automation library. 45 | description: | 46 | Playwright is a Python library to automate Chromium, 47 | Firefox and WebKit browsers with a single API. Playwright 48 | delivers automation that is ever-green, capable, reliable 49 | and fast. 50 | doc_url: https://playwright.dev/python/docs/intro/ 51 | dev_url: https://github.com/microsoft/playwright-python 52 | -------------------------------------------------------------------------------- /patch_check.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from undetected_playwright.async_api import async_playwright, Playwright 3 | 4 | 5 | async def run(playwright: Playwright): 6 | args = [] 7 | args.append("--disable-blink-features=AutomationControlled") 8 | browser = await playwright.chromium.launch( headless=False, args=args) 9 | page = await browser.new_page() 10 | await page.goto("https://hmaker.github.io/selenium-detector/") 11 | await page.evaluate("document") 12 | await page.evaluate("document.body") 13 | await page.evaluate("document.documentElement.outerHTML") 14 | await page.goto("https://nowsecure.nl/#relax") 15 | await page.evaluate("document") 16 | res = await page.evaluate("document.documentElement.outerHTML") 17 | await page.goto("https://abrahamjuliot.github.io/creepjs/") 18 | await browser.close() 19 | 20 | 21 | async def main(): 22 | async with async_playwright() as playwright: 23 | await run(playwright) 24 | 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools==68.2.2", "setuptools-scm==8.0.4", "wheel==0.41.2", "auditwheel==5.4.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # [tool.setuptools_scm] 6 | # version_file = "undetected_playwright/_repo_version.py" 7 | 8 | [tool.pytest.ini_options] 9 | addopts = "-Wall -rsx -vv -s" 10 | markers = [ 11 | "skip_browser", 12 | "only_browser", 13 | "skip_platform", 14 | "only_platform" 15 | ] 16 | junit_family = "xunit2" 17 | asyncio_mode = "auto" 18 | 19 | [tool.mypy] 20 | ignore_missing_imports = true 21 | python_version = "3.8" 22 | warn_unused_ignores = false 23 | warn_redundant_casts = true 24 | warn_unused_configs = true 25 | check_untyped_defs = true 26 | disallow_untyped_defs = true 27 | no_implicit_optional = false 28 | exclude = [ 29 | "build/", 30 | "env/", 31 | ] 32 | 33 | [tool.isort] 34 | profile = "black" 35 | 36 | [tool.pyright] 37 | include = ["playwright", "tests", "scripts"] 38 | pythonVersion = "3.8" 39 | reportMissingImports = false 40 | reportTypedDictNotRequiredAccess = false 41 | reportCallInDefaultInitializer = true 42 | reportOptionalSubscript = false 43 | reportUnboundVariable = false 44 | strictParameterNoneValue = false 45 | -------------------------------------------------------------------------------- /scripts/expected_api_mismatch.txt: -------------------------------------------------------------------------------- 1 | # Playwright Python API 2 | 3 | # Hidden property 4 | Parameter not documented: Browser.new_context(default_browser_type=) 5 | Parameter not documented: Browser.new_page(default_browser_type=) 6 | 7 | # We don't expand the type of the return value here. 8 | Parameter type mismatch in Accessibility.snapshot(return=): documented as Union[{role: str, name: str, value: Union[float, str], description: str, keyshortcuts: str, roledescription: str, valuetext: str, disabled: bool, expanded: bool, focused: bool, modal: bool, multiline: bool, multiselectable: bool, readonly: bool, required: bool, selected: bool, checked: Union["mixed", bool], pressed: Union["mixed", bool], level: int, valuemin: float, valuemax: float, autocomplete: str, haspopup: str, invalid: str, orientation: str, children: List[Dict]}, None], code has Union[Dict, None] 9 | 10 | # One vs two arguments in the callback, Python explicitly unions. 11 | Parameter type mismatch in BrowserContext.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]] 12 | Parameter type mismatch in BrowserContext.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] 13 | Parameter type mismatch in Page.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]] 14 | Parameter type mismatch in Page.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] 15 | -------------------------------------------------------------------------------- /scripts/update_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function update_api { 4 | echo "Generating $1" 5 | file_name="$1" 6 | generate_script="$2" 7 | git checkout HEAD -- "$file_name" 8 | 9 | if PYTHONIOENCODING=utf-8 python "$generate_script" > .x; then 10 | mv .x "$file_name" 11 | pre-commit run --files $file_name 12 | echo "Regenerated APIs" 13 | else 14 | echo "Exited due to errors" 15 | exit 1 16 | fi 17 | } 18 | 19 | update_api "playwright/sync_api/_generated.py" "scripts/generate_sync_api.py" 20 | update_api "playwright/async_api/_generated.py" "scripts/generate_async_api.py" 21 | 22 | undetected_playwright install 23 | 24 | python scripts/update_versions.py 25 | -------------------------------------------------------------------------------- /scripts/update_versions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | from pathlib import Path 16 | 17 | from undetected_playwright.sync_api import sync_playwright 18 | 19 | 20 | def main() -> None: 21 | with sync_playwright() as p: 22 | readme = Path("README.md").resolve() 23 | text = readme.read_text(encoding="utf-8") 24 | for browser_type in [p.chromium, p.firefox, p.webkit]: 25 | rx = re.compile( 26 | r"([^<]+)" 29 | ) 30 | browser = browser_type.launch() 31 | text = rx.sub( 32 | f"{browser.version}", 33 | text, 34 | ) 35 | browser.close() 36 | readme.write_text(text, encoding="utf-8") 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version = attr: undetected_playwright.__version__ 3 | 4 | [flake8] 5 | ignore = 6 | E501 7 | W503 8 | E302 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/beforeunload.html: -------------------------------------------------------------------------------- 1 |
beforeunload demo.
2 | 10 | -------------------------------------------------------------------------------- /tests/assets/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | from pathlib import Path 17 | 18 | from undetected_playwright.sync_api import Playwright, sync_playwright 19 | 20 | 21 | def main(playwright: Playwright, browser_name: str) -> None: 22 | browser = playwright[browser_name].launch() 23 | page = browser.new_page() 24 | page.goto("data:text/html,Foobar") 25 | here = Path(__file__).parent.resolve() 26 | page.screenshot(path=here / f"{browser_name}.png") 27 | page.close() 28 | browser.close() 29 | 30 | 31 | if __name__ == "__main__": 32 | browser_name = sys.argv[1] 33 | with sync_playwright() as p: 34 | main(p, browser_name) 35 | -------------------------------------------------------------------------------- /tests/assets/consolelog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | console.log test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/csp.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/digits/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/0.png -------------------------------------------------------------------------------- /tests/assets/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/1.png -------------------------------------------------------------------------------- /tests/assets/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/2.png -------------------------------------------------------------------------------- /tests/assets/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/3.png -------------------------------------------------------------------------------- /tests/assets/digits/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/4.png -------------------------------------------------------------------------------- /tests/assets/digits/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/5.png -------------------------------------------------------------------------------- /tests/assets/digits/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/6.png -------------------------------------------------------------------------------- /tests/assets/digits/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/7.png -------------------------------------------------------------------------------- /tests/assets/digits/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/8.png -------------------------------------------------------------------------------- /tests/assets/digits/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/digits/9.png -------------------------------------------------------------------------------- /tests/assets/dom.html: -------------------------------------------------------------------------------- 1 |
Text, 2 | more text
3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/assets/download-blob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blob Download Example 5 | 6 | 7 | 27 | Download 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/assets/drag-n-drop.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 35 |
36 |

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

38 |
39 |
Drop Zone
40 | 41 | -------------------------------------------------------------------------------- /tests/assets/dummy_bad_browser_executable.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.exit(1); 4 | -------------------------------------------------------------------------------- /tests/assets/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/empty.html -------------------------------------------------------------------------------- /tests/assets/error.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/es6/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/assets/es6/es6import.js: -------------------------------------------------------------------------------- 1 | import num from './es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/assets/es6/es6module.js: -------------------------------------------------------------------------------- 1 | export default 42; 2 | -------------------------------------------------------------------------------- /tests/assets/es6/es6pathimport.js: -------------------------------------------------------------------------------- 1 | import num from './es6/es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/assets/file-to-upload-2.txt: -------------------------------------------------------------------------------- 1 | contents of the file 2 | -------------------------------------------------------------------------------- /tests/assets/file-to-upload.txt: -------------------------------------------------------------------------------- 1 | contents of the file 2 | -------------------------------------------------------------------------------- /tests/assets/frames/child-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 |
Hi, I'm frame
16 | -------------------------------------------------------------------------------- /tests/assets/frames/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/assets/frames/nested-frames.html: -------------------------------------------------------------------------------- 1 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/assets/frames/one-frame.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/redirect-my-parent.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/frames/script.js: -------------------------------------------------------------------------------- 1 | console.log('Cheers!'); 2 | -------------------------------------------------------------------------------- /tests/assets/frames/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/frames/two-frames.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/assets/geolocation.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests/assets/global-var.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | -------------------------------------------------------------------------------- /tests/assets/har-sha1-main-response.txt: -------------------------------------------------------------------------------- 1 | Hello, world -------------------------------------------------------------------------------- /tests/assets/har-sha1.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "Playwright", 6 | "version": "1.23.0-next" 7 | }, 8 | "browser": { 9 | "name": "chromium", 10 | "version": "103.0.5060.33" 11 | }, 12 | "pages": [ 13 | { 14 | "startedDateTime": "2022-06-10T04:27:32.125Z", 15 | "id": "page@b17b177f1c2e66459db3dcbe44636ffd", 16 | "title": "Hey", 17 | "pageTimings": { 18 | "onContentLoad": 70, 19 | "onLoad": 70 20 | } 21 | } 22 | ], 23 | "entries": [ 24 | { 25 | "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", 26 | "_monotonicTime": 270572145.898, 27 | "startedDateTime": "2022-06-10T04:27:32.146Z", 28 | "time": 8.286, 29 | "request": { 30 | "method": "GET", 31 | "url": "http://no.playwright/", 32 | "httpVersion": "HTTP/1.1", 33 | "cookies": [], 34 | "headers": [ 35 | { 36 | "name": "Accept", 37 | "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" 38 | }, 39 | { 40 | "name": "Upgrade-Insecure-Requests", 41 | "value": "1" 42 | }, 43 | { 44 | "name": "User-Agent", 45 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" 46 | } 47 | ], 48 | "queryString": [], 49 | "headersSize": 326, 50 | "bodySize": 0 51 | }, 52 | "response": { 53 | "status": 200, 54 | "statusText": "OK", 55 | "httpVersion": "HTTP/1.1", 56 | "cookies": [], 57 | "headers": [ 58 | { 59 | "name": "content-length", 60 | "value": "12" 61 | }, 62 | { 63 | "name": "content-type", 64 | "value": "text/html" 65 | } 66 | ], 67 | "content": { 68 | "size": 12, 69 | "mimeType": "text/html", 70 | "compression": 0, 71 | "_file": "har-sha1-main-response.txt" 72 | }, 73 | "headersSize": 64, 74 | "bodySize": 71, 75 | "redirectURL": "", 76 | "_transferSize": 71 77 | }, 78 | "cache": { 79 | "beforeRequest": null, 80 | "afterRequest": null 81 | }, 82 | "timings": { 83 | "dns": -1, 84 | "connect": -1, 85 | "ssl": -1, 86 | "send": 0, 87 | "wait": 8.286, 88 | "receive": -1 89 | }, 90 | "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", 91 | "_securityDetails": {} 92 | } 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/assets/har.html: -------------------------------------------------------------------------------- 1 | HAR Page 2 | 3 |
hello, world!
4 | -------------------------------------------------------------------------------- /tests/assets/historyapi.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.injected = 123; 3 | window.__injectedError = new Error('hi'); 4 | -------------------------------------------------------------------------------- /tests/assets/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/input/animating-button.html: -------------------------------------------------------------------------------- 1 | 8 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/assets/input/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload-multi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/input/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keyboard test 5 | 6 | 7 | 8 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/mouse-helper.js: -------------------------------------------------------------------------------- 1 | // This injects a box into the page that moves with the mouse; 2 | // Useful for debugging 3 | (function(){ 4 | const box = document.createElement('div'); 5 | box.classList.add('mouse-helper'); 6 | const styleElement = document.createElement('style'); 7 | styleElement.innerHTML = ` 8 | .mouse-helper { 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 20px; 14 | height: 20px; 15 | background: rgba(0,0,0,.4); 16 | border: 1px solid white; 17 | border-radius: 10px; 18 | margin-left: -10px; 19 | margin-top: -10px; 20 | transition: background .2s, border-radius .2s, border-color .2s; 21 | } 22 | .mouse-helper.button-1 { 23 | transition: none; 24 | background: rgba(0,0,0,0.9); 25 | } 26 | .mouse-helper.button-2 { 27 | transition: none; 28 | border-color: rgba(0,0,255,0.9); 29 | } 30 | .mouse-helper.button-3 { 31 | transition: none; 32 | border-radius: 4px; 33 | } 34 | .mouse-helper.button-4 { 35 | transition: none; 36 | border-color: rgba(255,0,0,0.9); 37 | } 38 | .mouse-helper.button-5 { 39 | transition: none; 40 | border-color: rgba(0,255,0,0.9); 41 | } 42 | `; 43 | document.head.appendChild(styleElement); 44 | document.body.appendChild(box); 45 | document.addEventListener('mousemove', event => { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /tests/assets/input/rotatedButton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rotated button test 5 | 6 | 7 | 8 | 9 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/assets/input/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/assets/input/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/assets/input/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 |
10 |
Plain div
11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/assets/input/touches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Touch test 5 | 6 | 7 | 8 | 9 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/assets/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | window.ws = new WebSocket('ws://localhost:' + window.location.port + '/ws'); 3 | window.ws.addEventListener('message', message => {}); 4 | 5 | fetch('fetch-request-a.js'); 6 | window.top.fetchSecond = () => { 7 | // Do not return the promise here. 8 | fetch('fetch-request-b.js'); 9 | }; 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /tests/assets/offscreenbuttons.html: -------------------------------------------------------------------------------- 1 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 56 | -------------------------------------------------------------------------------- /tests/assets/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world!
3 | -------------------------------------------------------------------------------- /tests/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Playground 5 | 6 | 7 | 8 | 9 |
First div
10 |
11 | Second div 12 | Inner span 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/assets/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 8 | 9 | 10 | I am a popup 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/popup/window-open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/pptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/pptr.png -------------------------------------------------------------------------------- /tests/assets/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 33 | 34 | -------------------------------------------------------------------------------- /tests/assets/sectionselectorengine.js: -------------------------------------------------------------------------------- 1 | ({ 2 | create(root, target) { 3 | }, 4 | query(root, selector) { 5 | return root.querySelector('section'); 6 | }, 7 | queryAll(root, selector) { 8 | return Array.from(root.querySelectorAll('section')); 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /tests/assets/self-request.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/assets/serviceworkers/empty/sw.js -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | event.respondWith(fetch(event.request)); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | event.waitUntil(clients.claim()); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | if (event.request.url.endsWith('.html') || event.request.url.includes('passthrough')) { 3 | event.respondWith(fetch(event.request)); 4 | return; 5 | } 6 | const slash = event.request.url.lastIndexOf('/'); 7 | const name = event.request.url.substring(slash + 1); 8 | const blob = new Blob(["responseFromServiceWorker:" + name], {type : 'text/css'}); 9 | const response = new Response(blob, { "status" : 200 , "statusText" : "OK" }); 10 | event.respondWith(response); 11 | }); 12 | 13 | self.addEventListener('activate', event => { 14 | event.waitUntil(clients.claim()); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/assets/shadow.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/content-script.js: -------------------------------------------------------------------------------- 1 | console.log('hey from the content-script'); 2 | self.thisIsTheContentScript = true; 3 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/index.js: -------------------------------------------------------------------------------- 1 | // Mock script for background extension 2 | window.MAGIC = 42; 3 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple extension", 3 | "version": "0.1", 4 | "background": { 5 | "scripts": ["index.js"] 6 | }, 7 | "content_scripts": [{ 8 | "matches": [""], 9 | "css": [], 10 | "js": ["content-script.js"] 11 | }], 12 | "permissions": ["background", "activeTab"], 13 | "manifest_version": 2 14 | } 15 | -------------------------------------------------------------------------------- /tests/assets/simple.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /tests/assets/title.html: -------------------------------------------------------------------------------- 1 | Woof-Woof 2 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worker test 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.js: -------------------------------------------------------------------------------- 1 | console.log('hello from the worker'); 2 | 3 | function workerFunction() { 4 | return 'worker function result'; 5 | } 6 | 7 | self.addEventListener('message', event => { 8 | console.log('got this data: ' + event.data); 9 | }); 10 | 11 | (async function() { 12 | while (true) { 13 | self.postMessage(workerFunction.toString()); 14 | await new Promise(x => setTimeout(x, 100)); 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /tests/assets/wrappedlink.html: -------------------------------------------------------------------------------- 1 | 25 |
26 | 123321 27 |
28 | 33 | -------------------------------------------------------------------------------- /tests/async/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/async/__init__.py -------------------------------------------------------------------------------- /tests/async/test_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | 17 | import pytest 18 | 19 | from undetected_playwright.async_api import Browser, BrowserType, Error 20 | 21 | 22 | async def test_should_create_new_page(browser: Browser) -> None: 23 | page1 = await browser.new_page() 24 | assert len(browser.contexts) == 1 25 | 26 | page2 = await browser.new_page() 27 | assert len(browser.contexts) == 2 28 | 29 | await page1.close() 30 | assert len(browser.contexts) == 1 31 | 32 | await page2.close() 33 | assert len(browser.contexts) == 0 34 | 35 | 36 | async def test_should_throw_upon_second_create_new_page(browser: Browser) -> None: 37 | page = await browser.new_page() 38 | with pytest.raises(Error) as exc: 39 | await page.context.new_page() 40 | await page.close() 41 | assert "Please use browser.new_context()" in exc.value.message 42 | 43 | 44 | async def test_version_should_work(browser: Browser, is_chromium: bool) -> None: 45 | version = browser.version 46 | if is_chromium: 47 | assert re.match(r"^\d+\.\d+\.\d+\.\d+$", version) 48 | else: 49 | assert re.match(r"^\d+\.\d+", version) 50 | 51 | 52 | async def test_should_return_browser_type( 53 | browser: Browser, browser_type: BrowserType 54 | ) -> None: 55 | assert browser.browser_type is browser_type 56 | -------------------------------------------------------------------------------- /tests/async/test_browsercontext_clearcookies.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.async_api import Browser, BrowserContext, Page 16 | from tests.server import Server 17 | 18 | 19 | async def test_should_clear_cookies( 20 | context: BrowserContext, page: Page, server: Server 21 | ) -> None: 22 | await page.goto(server.EMPTY_PAGE) 23 | await context.add_cookies( 24 | [{"url": server.EMPTY_PAGE, "name": "cookie1", "value": "1"}] 25 | ) 26 | assert await page.evaluate("document.cookie") == "cookie1=1" 27 | await context.clear_cookies() 28 | assert await context.cookies() == [] 29 | await page.reload() 30 | assert await page.evaluate("document.cookie") == "" 31 | 32 | 33 | async def test_should_isolate_cookies_when_clearing( 34 | context: BrowserContext, server: Server, browser: Browser 35 | ) -> None: 36 | another_context = await browser.new_context() 37 | await context.add_cookies( 38 | [{"url": server.EMPTY_PAGE, "name": "page1cookie", "value": "page1value"}] 39 | ) 40 | await another_context.add_cookies( 41 | [{"url": server.EMPTY_PAGE, "name": "page2cookie", "value": "page2value"}] 42 | ) 43 | 44 | assert len(await context.cookies()) == 1 45 | assert len(await another_context.cookies()) == 1 46 | 47 | await context.clear_cookies() 48 | assert len(await context.cookies()) == 0 49 | assert len(await another_context.cookies()) == 1 50 | 51 | await another_context.clear_cookies() 52 | assert len(await context.cookies()) == 0 53 | assert len(await another_context.cookies()) == 0 54 | await another_context.close() 55 | -------------------------------------------------------------------------------- /tests/async/test_browsercontext_service_worker_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from undetected_playwright.async_api import Browser 15 | from tests.server import Server 16 | 17 | 18 | async def test_should_allow_service_workers_by_default( 19 | browser: Browser, server: Server 20 | ) -> None: 21 | context = await browser.new_context() 22 | page = await context.new_page() 23 | await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 24 | await page.evaluate("() => window.activationPromise") 25 | await context.close() 26 | 27 | 28 | async def test_block_blocks_service_worker_registration( 29 | browser: Browser, server: Server 30 | ) -> None: 31 | context = await browser.new_context(service_workers="block") 32 | page = await context.new_page() 33 | async with page.expect_console_message( 34 | lambda m: "Service Worker registration blocked by Playwright" == m.text 35 | ): 36 | await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 37 | await context.close() 38 | -------------------------------------------------------------------------------- /tests/async/test_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from undetected_playwright.async_api import BrowserContext, BrowserType 20 | 21 | 22 | async def test_context_managers( 23 | browser_type: BrowserType, launch_arguments: Dict 24 | ) -> None: 25 | async with await browser_type.launch(**launch_arguments) as browser: 26 | async with await browser.new_context() as context: 27 | async with await context.new_page(): 28 | assert len(context.pages) == 1 29 | assert len(context.pages) == 0 30 | assert len(browser.contexts) == 1 31 | assert len(browser.contexts) == 0 32 | assert not browser.is_connected() 33 | 34 | 35 | async def test_context_managers_not_hang(context: BrowserContext) -> None: 36 | with pytest.raises(Exception, match="Oops!"): 37 | async with await context.new_page(): 38 | raise Exception("Oops!") 39 | -------------------------------------------------------------------------------- /tests/async/test_device_descriptors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict 15 | 16 | import pytest 17 | 18 | from undetected_playwright.async_api import Playwright 19 | 20 | 21 | @pytest.mark.only_browser("chromium") 22 | async def test_should_work(playwright: Playwright, launch_arguments: Dict) -> None: 23 | device_descriptor = playwright.devices["Pixel 2"] 24 | device_type = device_descriptor["default_browser_type"] 25 | browser = await playwright[device_type].launch(**launch_arguments) 26 | context = await browser.new_context( 27 | **device_descriptor, 28 | ) 29 | page = await context.new_page() 30 | assert device_descriptor["default_browser_type"] == "chromium" 31 | assert browser.browser_type.name == "chromium" 32 | 33 | assert "Pixel 2" in device_descriptor["user_agent"] 34 | assert "Pixel 2" in await page.evaluate("navigator.userAgent") 35 | 36 | assert device_descriptor["device_scale_factor"] > 2 37 | assert await page.evaluate("window.devicePixelRatio") > 2 38 | 39 | assert device_descriptor["viewport"]["height"] > 700 40 | assert device_descriptor["viewport"]["height"] < 800 41 | inner_height = await page.evaluate("window.screen.availHeight") 42 | assert inner_height > 700 43 | assert inner_height < 800 44 | 45 | assert device_descriptor["viewport"]["width"] > 400 46 | assert device_descriptor["viewport"]["width"] < 500 47 | inner_width = await page.evaluate("window.screen.availWidth") 48 | assert inner_width > 400 49 | assert inner_width < 500 50 | 51 | assert device_descriptor["has_touch"] 52 | assert device_descriptor["is_mobile"] 53 | 54 | await browser.close() 55 | -------------------------------------------------------------------------------- /tests/async/test_fill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.async_api import Page 16 | from tests.server import Server 17 | 18 | 19 | async def test_fill_textarea(page: Page, server: Server) -> None: 20 | await page.goto(f"{server.PREFIX}/input/textarea.html") 21 | await page.fill("textarea", "some value") 22 | assert await page.evaluate("result") == "some value" 23 | 24 | 25 | # 26 | 27 | 28 | async def test_fill_input(page: Page, server: Server) -> None: 29 | await page.goto(f"{server.PREFIX}/input/textarea.html") 30 | await page.fill("input", "some value") 31 | assert await page.evaluate("result") == "some value" 32 | -------------------------------------------------------------------------------- /tests/async/test_ignore_https_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from undetected_playwright.async_api import Browser, Error 18 | from tests.server import Server 19 | 20 | 21 | async def test_ignore_https_error_should_work( 22 | browser: Browser, https_server: Server 23 | ) -> None: 24 | context = await browser.new_context(ignore_https_errors=True) 25 | page = await context.new_page() 26 | response = await page.goto(https_server.EMPTY_PAGE) 27 | assert response 28 | assert response.ok 29 | await context.close() 30 | 31 | 32 | async def test_ignore_https_error_should_work_negative_case( 33 | browser: Browser, https_server: Server 34 | ) -> None: 35 | context = await browser.new_context() 36 | page = await context.new_page() 37 | with pytest.raises(Error): 38 | await page.goto(https_server.EMPTY_PAGE) 39 | await context.close() 40 | -------------------------------------------------------------------------------- /tests/async/test_issues.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import FIRST_COMPLETED, CancelledError, create_task, wait 16 | from typing import Dict 17 | 18 | import pytest 19 | 20 | from undetected_playwright.async_api import Browser, BrowserType, Page, Playwright 21 | 22 | 23 | @pytest.mark.only_browser("chromium") 24 | async def test_issue_189(browser_type: BrowserType, launch_arguments: Dict) -> None: 25 | browser = await browser_type.launch( 26 | **launch_arguments, ignore_default_args=["--mute-audio"] 27 | ) 28 | page = await browser.new_page() 29 | assert await page.evaluate("1 + 1") == 2 30 | await browser.close() 31 | 32 | 33 | @pytest.mark.only_browser("chromium") 34 | async def test_issue_195(playwright: Playwright, browser: Browser) -> None: 35 | iphone_11 = playwright.devices["iPhone 11"] 36 | context = await browser.new_context(**iphone_11) 37 | await context.close() 38 | 39 | 40 | async def test_connection_task_cancel(page: Page) -> None: 41 | await page.set_content("") 42 | done, pending = await wait( 43 | { 44 | create_task(page.wait_for_selector("input")), 45 | create_task(page.wait_for_selector("#will-never-resolve")), 46 | }, 47 | return_when=FIRST_COMPLETED, 48 | ) 49 | assert len(done) == 1 50 | assert len(pending) == 1 51 | for task in pending: 52 | task.cancel() 53 | with pytest.raises(CancelledError): 54 | await task 55 | assert list(pending)[0].cancelled() 56 | -------------------------------------------------------------------------------- /tests/async/test_listeners.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.async_api import Page, Response 16 | from tests.server import Server 17 | 18 | 19 | async def test_listeners(page: Page, server: Server) -> None: 20 | log = [] 21 | 22 | def print_response(response: Response) -> None: 23 | log.append(response) 24 | 25 | page.on("response", print_response) 26 | await page.goto(f"{server.PREFIX}/input/textarea.html") 27 | assert len(log) > 0 28 | page.remove_listener("response", print_response) 29 | 30 | log = [] 31 | await page.goto(f"{server.PREFIX}/input/textarea.html") 32 | assert len(log) == 0 33 | -------------------------------------------------------------------------------- /tests/async/test_page_network_request.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | 17 | import pytest 18 | 19 | from undetected_playwright.async_api import Error, Page, Request 20 | from tests.server import Server 21 | 22 | 23 | async def test_should_not_allow_to_access_frame_on_popup_main_request( 24 | page: Page, server: Server 25 | ) -> None: 26 | await page.set_content(f'click me') 27 | request_promise = asyncio.ensure_future(page.context.wait_for_event("request")) 28 | popup_promise = asyncio.ensure_future(page.context.wait_for_event("page")) 29 | clicked = asyncio.ensure_future(page.get_by_text("click me").click()) 30 | request: Request = await request_promise 31 | 32 | assert request.is_navigation_request() 33 | 34 | with pytest.raises(Error) as exc_info: 35 | request.frame 36 | assert ( 37 | "Frame for this navigation request is not available" in exc_info.value.message 38 | ) 39 | 40 | response = await request.response() 41 | assert response 42 | await response.finished() 43 | await popup_promise 44 | await clicked 45 | -------------------------------------------------------------------------------- /tests/async/test_page_network_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | 17 | import pytest 18 | 19 | from undetected_playwright.async_api import Error, Page 20 | from tests.server import Server, TestServerRequest 21 | 22 | 23 | async def test_should_reject_response_finished_if_page_closes( 24 | page: Page, server: Server 25 | ) -> None: 26 | await page.goto(server.EMPTY_PAGE) 27 | 28 | def handle_get(request: TestServerRequest) -> None: 29 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 30 | # from server. 31 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 32 | request.write(b"hello ") 33 | 34 | server.set_route("/get", handle_get) 35 | # send request and wait for server response 36 | [page_response, _] = await asyncio.gather( 37 | page.wait_for_event("response"), 38 | page.evaluate("() => fetch('./get', { method: 'GET' })"), 39 | ) 40 | 41 | finish_coroutine = page_response.finished() 42 | await page.close() 43 | with pytest.raises(Error) as exc_info: 44 | await finish_coroutine 45 | error = exc_info.value 46 | assert "closed" in error.message 47 | 48 | 49 | async def test_should_reject_response_finished_if_context_closes( 50 | page: Page, server: Server 51 | ) -> None: 52 | await page.goto(server.EMPTY_PAGE) 53 | 54 | def handle_get(request: TestServerRequest) -> None: 55 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 56 | # from server. 57 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 58 | request.write(b"hello ") 59 | 60 | server.set_route("/get", handle_get) 61 | # send request and wait for server response 62 | [page_response, _] = await asyncio.gather( 63 | page.wait_for_event("response"), 64 | page.evaluate("() => fetch('./get', { method: 'GET' })"), 65 | ) 66 | 67 | finish_coroutine = page_response.finished() 68 | await page.context.close() 69 | with pytest.raises(Error) as exc_info: 70 | await finish_coroutine 71 | error = exc_info.value 72 | assert "closed" in error.message 73 | -------------------------------------------------------------------------------- /tests/async/test_pdf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | 18 | import pytest 19 | 20 | from undetected_playwright.async_api import Page 21 | 22 | 23 | @pytest.mark.only_browser("chromium") 24 | async def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: 25 | output_file = tmpdir / "foo.png" 26 | await page.pdf(path=str(output_file)) 27 | assert os.path.getsize(output_file) > 0 28 | 29 | 30 | @pytest.mark.only_browser("chromium") 31 | async def test_should_be_able_capture_pdf_without_path(page: Page) -> None: 32 | buffer = await page.pdf() 33 | assert buffer 34 | -------------------------------------------------------------------------------- /tests/async/test_request_fulfill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.async_api import Page, Route 16 | from tests.server import Server 17 | 18 | 19 | async def test_should_fetch_original_request_and_fulfill( 20 | page: Page, server: Server 21 | ) -> None: 22 | async def handle(route: Route) -> None: 23 | response = await page.request.fetch(route.request) 24 | await route.fulfill(response=response) 25 | 26 | await page.route("**/*", handle) 27 | response = await page.goto(server.PREFIX + "/title.html") 28 | assert response 29 | assert response.status == 200 30 | assert await page.title() == "Woof-Woof" 31 | 32 | 33 | async def test_should_fulfill_json(page: Page, server: Server) -> None: 34 | async def handle(route: Route) -> None: 35 | await route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"}) 36 | 37 | await page.route("**/*", handle) 38 | 39 | response = await page.goto(server.EMPTY_PAGE) 40 | assert response 41 | assert response.status == 201 42 | assert response.headers["content-type"] == "application/json" 43 | assert await response.json() == {"bar": "baz"} 44 | 45 | 46 | async def test_should_fulfill_json_overriding_existing_response( 47 | page: Page, server: Server 48 | ) -> None: 49 | server.set_route( 50 | "/tags", 51 | lambda request: ( 52 | request.setHeader("foo", "bar"), 53 | request.write('{"tags": ["a", "b"]}'.encode()), 54 | request.finish(), 55 | ), 56 | ) 57 | 58 | original = {} 59 | 60 | async def handle(route: Route) -> None: 61 | response = await route.fetch() 62 | json = await response.json() 63 | original["tags"] = json["tags"] 64 | json["tags"] = ["c"] 65 | await route.fulfill(response=response, json=json) 66 | 67 | await page.route("**/*", handle) 68 | 69 | response = await page.goto(server.PREFIX + "/tags") 70 | assert response 71 | assert response.status == 200 72 | assert response.headers["content-type"] == "application/json" 73 | assert response.headers["foo"] == "bar" 74 | assert original["tags"] == ["a", "b"] 75 | assert await response.json() == {"tags": ["c"]} 76 | -------------------------------------------------------------------------------- /tests/async/test_screenshot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Callable 16 | 17 | from undetected_playwright.async_api import Page 18 | from tests.server import Server 19 | from tests.utils import must 20 | 21 | 22 | async def test_should_screenshot_with_mask( 23 | page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None] 24 | ) -> None: 25 | await page.set_viewport_size( 26 | { 27 | "width": 500, 28 | "height": 500, 29 | } 30 | ) 31 | await page.goto(server.PREFIX + "/grid.html") 32 | assert_to_be_golden( 33 | await page.screenshot(mask=[page.locator("div").nth(5)]), 34 | "mask-should-work-with-page.png", 35 | ) 36 | assert_to_be_golden( 37 | await page.locator("body").screenshot(mask=[page.locator("div").nth(5)]), 38 | "mask-should-work-with-locator.png", 39 | ) 40 | assert_to_be_golden( 41 | await must(await page.query_selector("body")).screenshot( 42 | mask=[page.locator("div").nth(5)] 43 | ), 44 | "mask-should-work-with-element-handle.png", 45 | ) 46 | -------------------------------------------------------------------------------- /tests/async/test_selector_generator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from undetected_playwright.async_api import Error, Page, Playwright 18 | 19 | 20 | async def test_should_use_data_test_id_in_strict_errors( 21 | page: Page, playwright: Playwright 22 | ) -> None: 23 | playwright.selectors.set_test_id_attribute("data-custom-id") 24 | await page.set_content( 25 | """ 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | """ 40 | ) 41 | with pytest.raises(Error) as exc_info: 42 | await page.locator(".foo").hover(timeout=200) 43 | assert "strict mode violation" in exc_info.value.message 44 | assert '
None: 19 | await page.set_content( 20 | """ 21 |
hello
world
22 | hello2world2 23 | """ 24 | ) 25 | assert ( 26 | await page.eval_on_selector_all( 27 | 'div >> internal:and="span"', "els => els.map(e => e.textContent)" 28 | ) 29 | ) == [] 30 | assert ( 31 | await page.eval_on_selector_all( 32 | 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" 33 | ) 34 | ) == ["hello"] 35 | assert ( 36 | await page.eval_on_selector_all( 37 | 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" 38 | ) 39 | ) == ["world"] 40 | assert ( 41 | await page.eval_on_selector_all( 42 | 'span >> internal:and="span"', "els => els.map(e => e.textContent)" 43 | ) 44 | ) == ["hello2", "world2"] 45 | assert ( 46 | await page.eval_on_selector_all( 47 | '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" 48 | ) 49 | ) == ["hello"] 50 | assert ( 51 | await page.eval_on_selector_all( 52 | '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" 53 | ) 54 | ) == ["world2"] 55 | -------------------------------------------------------------------------------- /tests/async/test_video.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | from typing import Dict 18 | 19 | from undetected_playwright.async_api import Browser, BrowserType 20 | from tests.server import Server 21 | 22 | 23 | async def test_should_expose_video_path( 24 | browser: Browser, tmpdir: Path, server: Server 25 | ) -> None: 26 | page = await browser.new_page(record_video_dir=tmpdir) 27 | await page.goto(server.PREFIX + "/grid.html") 28 | assert page.video 29 | path = await page.video.path() 30 | assert str(tmpdir) in str(path) 31 | await page.context.close() 32 | 33 | 34 | async def test_short_video_should_throw( 35 | browser: Browser, tmpdir: Path, server: Server 36 | ) -> None: 37 | page = await browser.new_page(record_video_dir=tmpdir) 38 | await page.goto(server.PREFIX + "/grid.html") 39 | assert page.video 40 | path = await page.video.path() 41 | assert str(tmpdir) in str(path) 42 | await page.wait_for_timeout(1000) 43 | await page.context.close() 44 | assert os.path.exists(path) 45 | 46 | 47 | async def test_short_video_should_throw_persistent_context( 48 | browser_type: BrowserType, tmpdir: Path, launch_arguments: Dict, server: Server 49 | ) -> None: 50 | context = await browser_type.launch_persistent_context( 51 | str(tmpdir), 52 | **launch_arguments, 53 | viewport={"width": 320, "height": 240}, 54 | record_video_dir=str(tmpdir) + "1", 55 | ) 56 | page = context.pages[0] 57 | await page.goto(server.PREFIX + "/grid.html") 58 | await page.wait_for_timeout(1000) 59 | await context.close() 60 | 61 | assert page.video 62 | path = await page.video.path() 63 | assert str(tmpdir) in str(path) 64 | 65 | 66 | async def test_should_not_error_if_page_not_closed_before_save_as( 67 | browser: Browser, tmpdir: Path, server: Server 68 | ) -> None: 69 | page = await browser.new_page(record_video_dir=tmpdir) 70 | await page.goto(server.PREFIX + "/grid.html") 71 | await page.wait_for_timeout(1000) # make sure video has some data 72 | out_path = tmpdir / "some-video.webm" 73 | assert page.video 74 | saved = page.video.save_as(out_path) 75 | await page.close() 76 | await saved 77 | await page.context.close() 78 | assert os.path.exists(out_path) 79 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/common/__init__.py -------------------------------------------------------------------------------- /tests/common/test_collect_handles.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/common/test_collect_handles.py -------------------------------------------------------------------------------- /tests/common/test_events.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict 15 | 16 | import pytest 17 | 18 | from undetected_playwright.sync_api import sync_playwright 19 | from tests.server import Server 20 | 21 | 22 | def test_events(browser_name: str, launch_arguments: Dict, server: Server) -> None: 23 | with pytest.raises(Exception, match="fail"): 24 | 25 | def fail() -> None: 26 | raise Exception("fail") 27 | 28 | with sync_playwright() as p: 29 | with p[browser_name].launch(**launch_arguments) as browser: 30 | with browser.new_page() as page: 31 | page.on("response", lambda _: fail()) 32 | page.goto(server.PREFIX + "/grid.html") 33 | -------------------------------------------------------------------------------- /tests/common/test_threads.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import threading 16 | from typing import Dict 17 | 18 | from undetected_playwright.sync_api import sync_playwright 19 | 20 | 21 | def test_running_in_thread(browser_name: str, launch_arguments: Dict) -> None: 22 | result = [] 23 | 24 | class TestThread(threading.Thread): 25 | def run(self) -> None: 26 | with sync_playwright() as playwright: 27 | browser = playwright[browser_name].launch(**launch_arguments) 28 | # This should not throw ^^. 29 | browser.new_page() 30 | browser.close() 31 | result.append("Success") 32 | 33 | test_thread = TestThread() 34 | test_thread.start() 35 | test_thread.join() 36 | assert "Success" in result 37 | -------------------------------------------------------------------------------- /tests/golden-chromium/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-chromium/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-chromium/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-chromium/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-chromium/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-chromium/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/golden-firefox/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-firefox/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-firefox/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-firefox/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-firefox/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-firefox/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/golden-webkit/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-webkit/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-webkit/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-webkit/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-webkit/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/golden-webkit/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/tests/sync/__init__.py -------------------------------------------------------------------------------- /tests/sync/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from typing import Dict, Generator 17 | 18 | import pytest 19 | 20 | from undetected_playwright.sync_api import ( 21 | Browser, 22 | BrowserContext, 23 | BrowserType, 24 | Page, 25 | Playwright, 26 | Selectors, 27 | sync_playwright, 28 | ) 29 | 30 | from .utils import Utils 31 | from .utils import utils as utils_object 32 | 33 | 34 | @pytest.fixture 35 | def utils() -> Generator[Utils, None, None]: 36 | yield utils_object 37 | 38 | 39 | @pytest.fixture(scope="session") 40 | def playwright() -> Generator[Playwright, None, None]: 41 | with sync_playwright() as p: 42 | yield p 43 | 44 | 45 | @pytest.fixture(scope="session") 46 | def browser_type( 47 | playwright: Playwright, browser_name: str 48 | ) -> Generator[BrowserType, None, None]: 49 | browser_type = None 50 | if browser_name == "chromium": 51 | browser_type = playwright.chromium 52 | elif browser_name == "firefox": 53 | browser_type = playwright.firefox 54 | elif browser_name == "webkit": 55 | browser_type = playwright.webkit 56 | assert browser_type 57 | yield browser_type 58 | 59 | 60 | @pytest.fixture(scope="session") 61 | def browser( 62 | browser_type: BrowserType, launch_arguments: Dict 63 | ) -> Generator[Browser, None, None]: 64 | browser = browser_type.launch(**launch_arguments) 65 | yield browser 66 | browser.close() 67 | 68 | 69 | @pytest.fixture 70 | def context(browser: Browser) -> Generator[BrowserContext, None, None]: 71 | context = browser.new_context() 72 | yield context 73 | context.close() 74 | 75 | 76 | @pytest.fixture 77 | def page(context: BrowserContext) -> Generator[Page, None, None]: 78 | page = context.new_page() 79 | yield page 80 | page.close() 81 | 82 | 83 | @pytest.fixture(scope="session") 84 | def selectors(playwright: Playwright) -> Selectors: 85 | return playwright.selectors 86 | -------------------------------------------------------------------------------- /tests/sync/test_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.sync_api import Browser, BrowserType 16 | 17 | 18 | def test_should_return_browser_type( 19 | browser: Browser, browser_type: BrowserType 20 | ) -> None: 21 | assert browser.browser_type is browser_type 22 | -------------------------------------------------------------------------------- /tests/sync/test_browsercontext_service_worker_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from undetected_playwright.sync_api import Browser 15 | from tests.server import Server 16 | 17 | 18 | def test_should_allow_service_workers_by_default( 19 | browser: Browser, server: Server 20 | ) -> None: 21 | context = browser.new_context() 22 | page = context.new_page() 23 | page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 24 | page.evaluate("() => window.activationPromise") 25 | context.close() 26 | 27 | 28 | def test_block_blocks_service_worker_registration( 29 | browser: Browser, server: Server 30 | ) -> None: 31 | context = browser.new_context(service_workers="block") 32 | page = context.new_page() 33 | with page.expect_console_message( 34 | lambda m: "Service Worker registration blocked by Playwright" == m.text 35 | ): 36 | page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 37 | context.close() 38 | -------------------------------------------------------------------------------- /tests/sync/test_browsertype_connect_cdp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from undetected_playwright.sync_api import BrowserType 20 | from tests.server import find_free_port 21 | 22 | pytestmark = pytest.mark.only_browser("chromium") 23 | 24 | 25 | def test_connect_to_an_existing_cdp_session( 26 | launch_arguments: Dict, browser_type: BrowserType 27 | ) -> None: 28 | port = find_free_port() 29 | browser_server = browser_type.launch( 30 | **launch_arguments, args=[f"--remote-debugging-port={port}"] 31 | ) 32 | cdp_browser = browser_type.connect_over_cdp(f"http://127.0.0.1:{port}") 33 | assert len(cdp_browser.contexts) == 1 34 | cdp_browser.close() 35 | browser_server.close() 36 | -------------------------------------------------------------------------------- /tests/sync/test_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from undetected_playwright.sync_api import BrowserContext, BrowserType 20 | 21 | 22 | def test_context_managers(browser_type: BrowserType, launch_arguments: Dict) -> None: 23 | with browser_type.launch(**launch_arguments) as browser: 24 | with browser.new_context() as context: 25 | with context.new_page(): 26 | assert len(context.pages) == 1 27 | assert len(context.pages) == 0 28 | assert len(browser.contexts) == 1 29 | assert len(browser.contexts) == 0 30 | assert not browser.is_connected() 31 | 32 | 33 | def test_context_managers_not_hang(context: BrowserContext) -> None: 34 | with pytest.raises(Exception, match="Oops!"): 35 | with context.new_page(): 36 | raise Exception("Oops!") 37 | -------------------------------------------------------------------------------- /tests/sync/test_expect_misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from undetected_playwright.sync_api import Page, expect 18 | from tests.server import Server 19 | 20 | 21 | def test_to_be_in_viewport_should_work(page: Page) -> None: 22 | page.set_content( 23 | """ 24 |
25 |
foo
26 | """ 27 | ) 28 | expect(page.locator("#big")).to_be_in_viewport() 29 | expect(page.locator("#small")).not_to_be_in_viewport() 30 | page.locator("#small").scroll_into_view_if_needed() 31 | expect(page.locator("#small")).to_be_in_viewport() 32 | expect(page.locator("#small")).to_be_in_viewport(ratio=1) 33 | 34 | 35 | def test_to_be_in_viewport_should_respect_ratio_option( 36 | page: Page, server: Server 37 | ) -> None: 38 | page.set_content( 39 | """ 40 | 41 |
42 | """ 43 | ) 44 | expect(page.locator("div")).to_be_in_viewport() 45 | expect(page.locator("div")).to_be_in_viewport(ratio=0.1) 46 | expect(page.locator("div")).to_be_in_viewport(ratio=0.2) 47 | 48 | expect(page.locator("div")).to_be_in_viewport(ratio=0.25) 49 | # In this test, element's ratio is 0.25. 50 | expect(page.locator("div")).not_to_be_in_viewport(ratio=0.26) 51 | 52 | expect(page.locator("div")).not_to_be_in_viewport(ratio=0.3) 53 | expect(page.locator("div")).not_to_be_in_viewport(ratio=0.7) 54 | expect(page.locator("div")).not_to_be_in_viewport(ratio=0.8) 55 | 56 | 57 | def test_to_be_in_viewport_should_have_good_stack(page: Page, server: Server) -> None: 58 | with pytest.raises(AssertionError) as exc_info: 59 | expect(page.locator("body")).not_to_be_in_viewport(timeout=100) 60 | assert 'unexpected value "viewport ratio' in str(exc_info.value) 61 | 62 | 63 | def test_to_be_in_viewport_should_report_intersection_even_if_fully_covered_by_other_element( 64 | page: Page, server: Server 65 | ) -> None: 66 | page.set_content( 67 | """ 68 |

hello

69 |
None: 20 | page.goto(f"{server.PREFIX}/input/textarea.html") 21 | page.fill("textarea", "some value") 22 | assert page.evaluate("result") == "some value" 23 | 24 | 25 | def test_fill_input(page: Page, server: Server) -> None: 26 | page.goto(f"{server.PREFIX}/input/textarea.html") 27 | page.fill("input", "some value") 28 | assert page.evaluate("result") == "some value" 29 | -------------------------------------------------------------------------------- /tests/sync/test_input.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | from typing import Any 18 | 19 | from undetected_playwright.sync_api import Page 20 | 21 | 22 | def test_expect_file_chooser(page: Page) -> None: 23 | page.set_content("") 24 | with page.expect_file_chooser() as fc_info: 25 | page.click('input[type="file"]') 26 | fc = fc_info.value 27 | fc.set_files( 28 | {"name": "test.txt", "mimeType": "text/plain", "buffer": b"Hello World"} 29 | ) 30 | 31 | 32 | def test_set_input_files_should_preserve_last_modified_timestamp( 33 | page: Page, 34 | assetdir: Path, 35 | ) -> None: 36 | page.set_content("") 37 | input = page.locator("input") 38 | files: Any = ["file-to-upload.txt", "file-to-upload-2.txt"] 39 | input.set_input_files([assetdir / file for file in files]) 40 | assert input.evaluate("input => [...input.files].map(f => f.name)") == files 41 | timestamps = input.evaluate("input => [...input.files].map(f => f.lastModified)") 42 | expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] 43 | 44 | # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even 45 | # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. 46 | for i in range(len(timestamps)): 47 | assert abs(timestamps[i] - expected_timestamps[i]) < 1000 48 | -------------------------------------------------------------------------------- /tests/sync/test_listeners.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from undetected_playwright.sync_api import Page, Response 17 | from tests.server import Server 18 | 19 | 20 | def test_listeners(page: Page, server: Server) -> None: 21 | log = [] 22 | 23 | def print_response(response: Response) -> None: 24 | log.append(response) 25 | 26 | page.on("response", print_response) 27 | page.goto(f"{server.PREFIX}/input/textarea.html") 28 | assert len(log) > 0 29 | page.remove_listener("response", print_response) 30 | 31 | log = [] 32 | page.goto(f"{server.PREFIX}/input/textarea.html") 33 | assert len(log) == 0 34 | -------------------------------------------------------------------------------- /tests/sync/test_page_network_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | from twisted.web import http 17 | 18 | from undetected_playwright.sync_api import Error, Page 19 | from tests.server import Server 20 | 21 | 22 | def test_should_reject_response_finished_if_page_closes( 23 | page: Page, server: Server 24 | ) -> None: 25 | page.goto(server.EMPTY_PAGE) 26 | 27 | def handle_get(request: http.Request) -> None: 28 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 29 | # from server. 30 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 31 | request.write(b"hello ") 32 | 33 | server.set_route("/get", handle_get) 34 | # send request and wait for server response 35 | with page.expect_response("**/*") as response_info: 36 | page.evaluate("() => fetch('./get', { method: 'GET' })") 37 | page_response = response_info.value 38 | page.close() 39 | with pytest.raises(Error) as exc_info: 40 | page_response.finished() 41 | error = exc_info.value 42 | assert "closed" in error.message 43 | 44 | 45 | def test_should_reject_response_finished_if_context_closes( 46 | page: Page, server: Server 47 | ) -> None: 48 | page.goto(server.EMPTY_PAGE) 49 | 50 | def handle_get(request: http.Request) -> None: 51 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 52 | # from server. 53 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 54 | request.write(b"hello ") 55 | 56 | server.set_route("/get", handle_get) 57 | # send request and wait for server response 58 | with page.expect_response("**/*") as response_info: 59 | page.evaluate("() => fetch('./get', { method: 'GET' })") 60 | page_response = response_info.value 61 | 62 | page.context.close() 63 | with pytest.raises(Error) as exc_info: 64 | page_response.finished() 65 | error = exc_info.value 66 | assert "closed" in error.message 67 | -------------------------------------------------------------------------------- /tests/sync/test_page_request_intercept.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from undetected_playwright.sync_api import Error, Page, Route 18 | from tests.server import Server, TestServerRequest 19 | 20 | 21 | def test_should_support_timeout_option_in_route_fetch( 22 | server: Server, page: Page 23 | ) -> None: 24 | def _handle(request: TestServerRequest) -> None: 25 | request.responseHeaders.addRawHeader("Content-Length", "4096") 26 | request.responseHeaders.addRawHeader("Content-Type", "text/html") 27 | request.write(b"") 28 | 29 | server.set_route( 30 | "/slow", 31 | _handle, 32 | ) 33 | 34 | def handle(route: Route) -> None: 35 | with pytest.raises(Error) as error: 36 | route.fetch(timeout=1000) 37 | assert "Request timed out after 1000ms" in error.value.message 38 | 39 | page.route("**/*", lambda route: handle(route)) 40 | with pytest.raises(Error) as error: 41 | page.goto(server.PREFIX + "/slow", timeout=2000) 42 | assert "Timeout 2000ms exceeded" in error.value.message 43 | 44 | 45 | def test_should_intercept_with_url_override(server: Server, page: Page) -> None: 46 | def handle(route: Route) -> None: 47 | response = route.fetch(url=server.PREFIX + "/one-style.html") 48 | route.fulfill(response=response) 49 | 50 | page.route("**/*.html", lambda route: handle(route)) 51 | response = page.goto(server.PREFIX + "/empty.html") 52 | assert response 53 | assert response.status == 200 54 | assert "one-style.css" in response.body().decode("utf-8") 55 | -------------------------------------------------------------------------------- /tests/sync/test_pdf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | 18 | import pytest 19 | 20 | from undetected_playwright.sync_api import Page 21 | 22 | 23 | @pytest.mark.only_browser("chromium") 24 | def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: 25 | output_file = tmpdir / "foo.png" 26 | page.pdf(path=str(output_file)) 27 | assert os.path.getsize(output_file) > 0 28 | 29 | 30 | @pytest.mark.only_browser("chromium") 31 | def test_should_be_able_capture_pdf_without_path(page: Page) -> None: 32 | buffer = page.pdf() 33 | assert buffer 34 | -------------------------------------------------------------------------------- /tests/sync/test_request_fulfill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.sync_api import Page, Route 16 | from tests.server import Server 17 | 18 | 19 | def test_should_fetch_original_request_and_fulfill(page: Page, server: Server) -> None: 20 | def handle(route: Route) -> None: 21 | response = page.request.fetch(route.request) 22 | route.fulfill(response=response) 23 | 24 | page.route("**/*", handle) 25 | response = page.goto(server.PREFIX + "/title.html") 26 | assert response 27 | assert response.status == 200 28 | assert page.title() == "Woof-Woof" 29 | 30 | 31 | def test_should_fulfill_json(page: Page, server: Server) -> None: 32 | def handle(route: Route) -> None: 33 | route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"}) 34 | 35 | page.route("**/*", handle) 36 | 37 | response = page.goto(server.EMPTY_PAGE) 38 | assert response 39 | assert response.status == 201 40 | assert response.headers["content-type"] == "application/json" 41 | assert response.json() == {"bar": "baz"} 42 | 43 | 44 | def test_should_fulfill_json_overriding_existing_response( 45 | page: Page, server: Server 46 | ) -> None: 47 | server.set_route( 48 | "/tags", 49 | lambda request: ( 50 | request.setHeader("foo", "bar"), 51 | request.write('{"tags": ["a", "b"]}'.encode()), 52 | request.finish(), 53 | ), 54 | ) 55 | 56 | original = {} 57 | 58 | def handle(route: Route) -> None: 59 | response = route.fetch() 60 | json = response.json() 61 | original["tags"] = json["tags"] 62 | json["tags"] = ["c"] 63 | route.fulfill(response=response, json=json) 64 | 65 | page.route("**/*", handle) 66 | 67 | response = page.goto(server.PREFIX + "/tags") 68 | assert response 69 | assert response.status == 200 70 | assert response.headers["content-type"] == "application/json" 71 | assert response.headers["foo"] == "bar" 72 | assert original["tags"] == ["a", "b"] 73 | assert response.json() == {"tags": ["c"]} 74 | -------------------------------------------------------------------------------- /tests/sync/test_selectors_misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from undetected_playwright.sync_api import Page 16 | 17 | 18 | def test_should_work_with_internal_and(page: Page) -> None: 19 | page.set_content( 20 | """ 21 |
hello
world
22 | hello2world2 23 | """ 24 | ) 25 | assert ( 26 | page.eval_on_selector_all( 27 | 'div >> internal:and="span"', "els => els.map(e => e.textContent)" 28 | ) 29 | ) == [] 30 | assert ( 31 | page.eval_on_selector_all( 32 | 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" 33 | ) 34 | ) == ["hello"] 35 | assert ( 36 | page.eval_on_selector_all( 37 | 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" 38 | ) 39 | ) == ["world"] 40 | assert ( 41 | page.eval_on_selector_all( 42 | 'span >> internal:and="span"', "els => els.map(e => e.textContent)" 43 | ) 44 | ) == ["hello2", "world2"] 45 | assert ( 46 | page.eval_on_selector_all( 47 | '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" 48 | ) 49 | ) == ["hello"] 50 | assert ( 51 | page.eval_on_selector_all( 52 | '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" 53 | ) 54 | ) == ["world2"] 55 | -------------------------------------------------------------------------------- /tests/test_installation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import subprocess 18 | import sys 19 | from pathlib import Path 20 | from venv import EnvBuilder 21 | 22 | 23 | def test_install(tmp_path: Path, browser_name: str) -> None: 24 | env_dir = tmp_path / "env" 25 | env = EnvBuilder(with_pip=True) 26 | env.create(env_dir=env_dir) 27 | context = env.ensure_directories(env_dir) 28 | root = Path(__file__).parent.parent.resolve() 29 | if sys.platform == "win32": 30 | wheelpath = list((root / "dist").glob("undetected_playwright*win_amd64*.whl"))[0] 31 | elif sys.platform == "linux": 32 | wheelpath = list((root / "dist").glob("undetected_playwright*manylinux1*.whl"))[0] 33 | elif sys.platform == "darwin": 34 | wheelpath = list((root / "dist").glob("undetected_playwright*macosx_*.whl"))[0] 35 | subprocess.check_output( 36 | [ 37 | context.env_exe, 38 | "-m", 39 | "pip", 40 | "install", 41 | str(wheelpath), 42 | ] 43 | ) 44 | environ = os.environ.copy() 45 | environ["PLAYWRIGHT_BROWSERS_PATH"] = str(tmp_path) 46 | subprocess.check_output( 47 | [context.env_exe, "-m", "undetected_playwright", "install", browser_name], env=environ 48 | ) 49 | shutil.copyfile(root / "tests" / "assets" / "client.py", tmp_path / "main.py") 50 | subprocess.check_output( 51 | [context.env_exe, str(tmp_path / "main.py"), browser_name], env=environ 52 | ) 53 | assert (tmp_path / f"{browser_name}.png").exists() 54 | -------------------------------------------------------------------------------- /tests/test_reference_count_async.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import gc 16 | from collections import defaultdict 17 | from typing import Any 18 | 19 | import objgraph 20 | import pytest 21 | 22 | from undetected_playwright.async_api import async_playwright 23 | from tests.server import Server 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_memory_objects(server: Server, browser_name: str) -> None: 28 | async with async_playwright() as p: 29 | browser = await p[browser_name].launch() 30 | page = await browser.new_page() 31 | await page.goto(server.EMPTY_PAGE) 32 | 33 | page.on("dialog", lambda dialog: dialog.dismiss()) 34 | for _ in range(100): 35 | await page.evaluate("""async () => alert()""") 36 | 37 | await page.route("**/*", lambda route, _: route.fulfill(body="OK")) 38 | 39 | def handle_network_response_received(event: Any) -> None: 40 | event["__pw__is_last_network_response_received_event"] = True 41 | 42 | if browser_name == "chromium": 43 | # https://github.com/microsoft/playwright-python/issues/1602 44 | client = await page.context.new_cdp_session(page) 45 | await client.send("Network.enable") 46 | 47 | client.on( 48 | "Network.responseReceived", 49 | handle_network_response_received, 50 | ) 51 | 52 | for _ in range(100): 53 | response = await page.evaluate("""async () => (await fetch("/")).text()""") 54 | assert response == "OK" 55 | 56 | await browser.close() 57 | 58 | gc.collect() 59 | 60 | pw_objects: defaultdict = defaultdict(int) 61 | for o in objgraph.by_type("dict"): 62 | name = o.get("_type") 63 | # https://github.com/microsoft/playwright-python/issues/1602 64 | if o.get("__pw__is_last_network_response_received_event", False): 65 | assert False 66 | if not name: 67 | continue 68 | pw_objects[name] += 1 69 | 70 | assert "Dialog" not in pw_objects 71 | assert "Request" not in pw_objects 72 | assert "Route" not in pw_objects 73 | -------------------------------------------------------------------------------- /tests/testserver/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEsjCCApoCCQCIPLvQDgoZojANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9w 3 | dXBwZXRlZXItdGVzdHMwIBcNMTkwMjEzMTkwNzQzWhgPMzAxODA2MTYxOTA3NDNa 4 | MBoxGDAWBgNVBAMMD3B1cHBldGVlci10ZXN0czCCAiIwDQYJKoZIhvcNAQEBBQAD 5 | ggIPADCCAgoCggIBAJue1yqA4qn0SJR3rgTd6sCYVHMKqUouD0No09H7qf+5ZaIb 6 | 3yGpC5J9Bsf/ZbvD5xpgqbGEYkHj7Qh6Z/cPCSHA+ZpsUzDXVrLFXrdwwiK1FrIS 7 | rDI2RYsiP+e52XPC/acWC/7f+E54C62oMjYojaVaDn8gu06gyS1rXK2JITQ6CrKn 8 | b+PVSkjtPB4ku245u1qCKoblkNEZSkEmw8Csl+gw6ydGqOSQAoo8rsDte5zCMnPX 9 | 7XzL6EhRqpiVx7PCuQWnXhL7j9N214Pit7s7F8TeAA6yZR9oswW+h0dWO+XwocJ1 10 | rwkODXOngbCqO+GUxyuavIl2m0d2MP8n6Wa9RVqYetmPQzafKkR5hjiV4mgCFqNQ 11 | bHMTjI6udcR+h5pYoWKxN9/gJaWwyAAzck0AiMeGVrvKR3JKACqlTMzy/Y30obRF 12 | dddURoFf2wjKJvuTK9hHI7pwM5tlPEwu9bTCWNA6XXs2Bq1f6N2OAKhpKOcihNem 13 | aeGUPmygLPb66z9JO75yZXM+1yk1ScXaNHWZLmluVpEPk7maWULpSpxPAlaN3PmK 14 | 8lEihgfBBovampxZo8SvPEt+g5jGyPq9weNg8ic8476PuRVQdg7D8spVxl6whDlJ 15 | bcFojzgrX70t13jqZOtla4WK1vRnZAGplfoH0i5WvAVw+i5S/OVzsmNDtGFbAgMB 16 | AAEwDQYJKoZIhvcNAQELBQADggIBADUAjA/dH+b5UxDC5SL98w1hphw9PvD1cuGS 17 | sVnKPM236JoTiO3KVfm3NMBfSoBi1hPNkXzqr/R4xbyje4Kc4oYcdjGtpll3T5da 18 | wkx1+qumx6O2mEaOshxh76dfZfZne6SQphQKHw8PD10CfDb/NMnmdEbiOSENSqS4 19 | jGELuGviUl361oCBU45UEN7lfs7ANAhwSZyEO7deroyGdvsxfQUaqQrEQsG30jn3 20 | t0cCamYU6eK3bNR/yNXJrZFv3dzoquRY9H52YtVElRqdAIsNlnbxbqz0cm5xFKFt 21 | YTIrMSO1EvDTbB0PPwC5FJvONHhjwiWzgVXSnZrcs/05TsWWnSHH92S+wGCIBC+0 22 | 6fcSKnjdBn9ks5TrDX0TRY6N890KyDQWxPRhHYrMVpn833WY8y/SguxqiMgLFgMD 23 | WLy6yZzJloW7NgpLGAfMA0nMG1O92hfKmQw82Pyf3SVXGTDiXiEOXn0vN6bsPaV/ 24 | 3Ws2LJQECnVfHj3TsuxdtwcO+VGcFCarMOqlhE6IlQzfK8ykYdP6wCkVgXEtiVCR 25 | T1OWUWCFowoFpwBFLf1lA065qsAymddnkrUEOMiScZ/3OZhmd+FvgQ+O0iYuqpeI 26 | xauiQ68+Jb4KjVWnu5QBVq8n1vUJ5+gAzowNMN9G+1+A282Ox23T48dce22BTS6B 27 | 3Taaccm+ 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import zipfile 17 | from pathlib import Path 18 | from typing import Any, Dict, List, Optional, Tuple, TypeVar 19 | 20 | 21 | def parse_trace(path: Path) -> Tuple[Dict[str, bytes], List[Any]]: 22 | resources: Dict[str, bytes] = {} 23 | with zipfile.ZipFile(path, "r") as zip: 24 | for name in zip.namelist(): 25 | resources[name] = zip.read(name) 26 | action_map: Dict[str, Any] = {} 27 | events: List[Any] = [] 28 | for name in ["trace.trace", "trace.network"]: 29 | for line in resources[name].decode().splitlines(): 30 | if not line: 31 | continue 32 | event = json.loads(line) 33 | if event["type"] == "before": 34 | event["type"] = "action" 35 | action_map[event["callId"]] = event 36 | events.append(event) 37 | elif event["type"] == "input": 38 | pass 39 | elif event["type"] == "after": 40 | existing = action_map[event["callId"]] 41 | existing["error"] = event.get("error", None) 42 | else: 43 | events.append(event) 44 | return (resources, events) 45 | 46 | 47 | def get_trace_actions(events: List[Any]) -> List[str]: 48 | action_events = sorted( 49 | list( 50 | filter( 51 | lambda e: e["type"] == "action", 52 | events, 53 | ) 54 | ), 55 | key=lambda e: e["startTime"], 56 | ) 57 | return [e["apiName"] for e in action_events] 58 | 59 | 60 | TARGET_CLOSED_ERROR_MESSAGE = "Target page, context or browser has been closed" 61 | 62 | MustType = TypeVar("MustType") 63 | 64 | 65 | def must(value: Optional[MustType]) -> MustType: 66 | assert value 67 | return value 68 | -------------------------------------------------------------------------------- /undetected_playwright/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Python package `undetected_playwright` is a Python library to automate Chromium, 17 | Firefox and WebKit with a single API. Playwright is built to enable cross-browser 18 | web automation that is ever-green, capable, reliable and fast. 19 | """ 20 | __version__ = "1.40.0-1700587210000" 21 | -------------------------------------------------------------------------------- /undetected_playwright/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import subprocess 16 | import sys 17 | 18 | from undetected_playwright._impl._driver import compute_driver_executable, get_driver_env 19 | 20 | 21 | def main() -> None: 22 | driver_executable = compute_driver_executable() 23 | completed_process = subprocess.run( 24 | [str(driver_executable), *sys.argv[1:]], env=get_driver_env() 25 | ) 26 | sys.exit(completed_process.returncode) 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/undetected-playwright-python/4b15791d5263b886477c14a56d9bb54c1c375997/undetected_playwright/_impl/__init__.py -------------------------------------------------------------------------------- /undetected_playwright/_impl/__pyinstaller/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from typing import List 17 | 18 | 19 | def get_hook_dirs() -> List[str]: 20 | return [os.path.dirname(__file__)] 21 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/__pyinstaller/hook-playwright.async_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PyInstaller.utils.hooks import collect_data_files # type: ignore 16 | 17 | datas = collect_data_files("undetected_playwright") 18 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/__pyinstaller/hook-playwright.sync_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PyInstaller.utils.hooks import collect_data_files # type: ignore 16 | 17 | datas = collect_data_files("undetected_playwright") 18 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_accessibility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict, Optional 16 | 17 | from undetected_playwright._impl._connection import Channel 18 | from undetected_playwright._impl._element_handle import ElementHandle 19 | from undetected_playwright._impl._helper import locals_to_params 20 | 21 | 22 | def _ax_node_from_protocol(axNode: Dict) -> Dict: 23 | result = {**axNode} 24 | if "valueNumber" in axNode: 25 | result["value"] = axNode["valueNumber"] 26 | elif "valueString" in axNode: 27 | result["value"] = axNode["valueString"] 28 | 29 | if "checked" in axNode: 30 | result["checked"] = ( 31 | True 32 | if axNode.get("checked") == "checked" 33 | else ( 34 | False if axNode.get("checked") == "unchecked" else axNode.get("checked") 35 | ) 36 | ) 37 | 38 | if "pressed" in axNode: 39 | result["pressed"] = ( 40 | True 41 | if axNode.get("pressed") == "pressed" 42 | else ( 43 | False if axNode.get("pressed") == "released" else axNode.get("pressed") 44 | ) 45 | ) 46 | 47 | if axNode.get("children"): 48 | result["children"] = list(map(_ax_node_from_protocol, axNode["children"])) 49 | if "valueNumber" in result: 50 | del result["valueNumber"] 51 | if "valueString" in result: 52 | del result["valueString"] 53 | return result 54 | 55 | 56 | class Accessibility: 57 | def __init__(self, channel: Channel) -> None: 58 | self._channel = channel 59 | self._loop = channel._connection._loop 60 | self._dispatcher_fiber = channel._connection._dispatcher_fiber 61 | 62 | async def snapshot( 63 | self, interestingOnly: bool = None, root: ElementHandle = None 64 | ) -> Optional[Dict]: 65 | params = locals_to_params(locals()) 66 | if root: 67 | params["root"] = root._channel 68 | result = await self._channel.send("accessibilitySnapshot", params) 69 | return _ax_node_from_protocol(result) if result else None 70 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_artifact.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from pathlib import Path 17 | from typing import Dict, Optional, Union, cast 18 | 19 | from undetected_playwright._impl._connection import ChannelOwner, from_channel 20 | from undetected_playwright._impl._helper import Error, make_dirs_for_file, patch_error_message 21 | from undetected_playwright._impl._stream import Stream 22 | 23 | 24 | class Artifact(ChannelOwner): 25 | def __init__( 26 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 27 | ) -> None: 28 | super().__init__(parent, type, guid, initializer) 29 | self.absolute_path = initializer["absolutePath"] 30 | 31 | async def path_after_finished(self) -> pathlib.Path: 32 | if self._connection.is_remote: 33 | raise Error( 34 | "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." 35 | ) 36 | path = await self._channel.send("pathAfterFinished") 37 | return pathlib.Path(path) 38 | 39 | async def save_as(self, path: Union[str, Path]) -> None: 40 | stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) 41 | make_dirs_for_file(path) 42 | await stream.save_as(path) 43 | 44 | async def failure(self) -> Optional[str]: 45 | return patch_error_message(await self._channel.send("failure")) 46 | 47 | async def delete(self) -> None: 48 | await self._channel.send("delete") 49 | 50 | async def read_info_buffer(self) -> bytes: 51 | stream = cast(Stream, from_channel(await self._channel.send("stream"))) 52 | buffer = await stream.read_all() 53 | return buffer 54 | 55 | async def cancel(self) -> None: 56 | await self._channel.send("cancel") 57 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_cdp_session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Dict 16 | 17 | from undetected_playwright._impl._connection import ChannelOwner 18 | from undetected_playwright._impl._helper import locals_to_params 19 | 20 | 21 | class CDPSession(ChannelOwner): 22 | def __init__( 23 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 24 | ) -> None: 25 | super().__init__(parent, type, guid, initializer) 26 | self._channel.on("event", lambda params: self._on_event(params)) 27 | 28 | def _on_event(self, params: Any) -> None: 29 | self.emit(params["method"], params["params"]) 30 | 31 | async def send(self, method: str, params: Dict = None) -> Dict: 32 | return await self._channel.send("send", locals_to_params(locals())) 33 | 34 | async def detach(self) -> None: 35 | await self._channel.send("detach") 36 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_console_message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import AbstractEventLoop 16 | from typing import TYPE_CHECKING, Any, Dict, List, Optional 17 | 18 | from undetected_playwright._impl._api_structures import SourceLocation 19 | from undetected_playwright._impl._connection import from_channel, from_nullable_channel 20 | from undetected_playwright._impl._js_handle import JSHandle 21 | 22 | if TYPE_CHECKING: # pragma: no cover 23 | from undetected_playwright._impl._page import Page 24 | 25 | 26 | class ConsoleMessage: 27 | def __init__( 28 | self, event: Dict, loop: AbstractEventLoop, dispatcher_fiber: Any 29 | ) -> None: 30 | self._event = event 31 | self._loop = loop 32 | self._dispatcher_fiber = dispatcher_fiber 33 | self._page: Optional["Page"] = from_nullable_channel(event.get("page")) 34 | 35 | def __repr__(self) -> str: 36 | return f"" 37 | 38 | def __str__(self) -> str: 39 | return self.text 40 | 41 | @property 42 | def type(self) -> str: 43 | return self._event["type"] 44 | 45 | @property 46 | def text(self) -> str: 47 | return self._event["text"] 48 | 49 | @property 50 | def args(self) -> List[JSHandle]: 51 | return list(map(from_channel, self._event["args"])) 52 | 53 | @property 54 | def location(self) -> SourceLocation: 55 | return self._event["location"] 56 | 57 | @property 58 | def page(self) -> Optional["Page"]: 59 | return self._page 60 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import TYPE_CHECKING, Dict, Optional 16 | 17 | from undetected_playwright._impl._connection import ChannelOwner, from_nullable_channel 18 | from undetected_playwright._impl._helper import locals_to_params 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from undetected_playwright._impl._page import Page 22 | 23 | 24 | class Dialog(ChannelOwner): 25 | def __init__( 26 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 27 | ) -> None: 28 | super().__init__(parent, type, guid, initializer) 29 | self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) 30 | 31 | def __repr__(self) -> str: 32 | return f"" 33 | 34 | @property 35 | def type(self) -> str: 36 | return self._initializer["type"] 37 | 38 | @property 39 | def message(self) -> str: 40 | return self._initializer["message"] 41 | 42 | @property 43 | def default_value(self) -> str: 44 | return self._initializer["defaultValue"] 45 | 46 | @property 47 | def page(self) -> Optional["Page"]: 48 | return self._page 49 | 50 | async def accept(self, promptText: str = None) -> None: 51 | await self._channel.send("accept", locals_to_params(locals())) 52 | 53 | async def dismiss(self) -> None: 54 | await self._channel.send("dismiss") 55 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_download.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from pathlib import Path 17 | from typing import TYPE_CHECKING, Optional, Union 18 | 19 | from undetected_playwright._impl._artifact import Artifact 20 | 21 | if TYPE_CHECKING: # pragma: no cover 22 | from undetected_playwright._impl._page import Page 23 | 24 | 25 | class Download: 26 | def __init__( 27 | self, page: "Page", url: str, suggested_filename: str, artifact: Artifact 28 | ) -> None: 29 | self._page = page 30 | self._loop = page._loop 31 | self._dispatcher_fiber = page._dispatcher_fiber 32 | self._url = url 33 | self._suggested_filename = suggested_filename 34 | self._artifact = artifact 35 | 36 | def __repr__(self) -> str: 37 | return f"" 38 | 39 | @property 40 | def page(self) -> "Page": 41 | return self._page 42 | 43 | @property 44 | def url(self) -> str: 45 | return self._url 46 | 47 | @property 48 | def suggested_filename(self) -> str: 49 | return self._suggested_filename 50 | 51 | async def delete(self) -> None: 52 | await self._artifact.delete() 53 | 54 | async def failure(self) -> Optional[str]: 55 | return await self._artifact.failure() 56 | 57 | async def path(self) -> pathlib.Path: 58 | return await self._artifact.path_after_finished() 59 | 60 | async def save_as(self, path: Union[str, Path]) -> None: 61 | await self._artifact.save_as(path) 62 | 63 | async def cancel(self) -> None: 64 | return await self._artifact.cancel() 65 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_driver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import inspect 16 | import os 17 | import sys 18 | from pathlib import Path 19 | 20 | import undetected_playwright 21 | # from undetected_playwright._repo_version import version 22 | 23 | 24 | def compute_driver_executable() -> Path: 25 | package_path = Path(inspect.getfile(undetected_playwright)).parent 26 | platform = sys.platform 27 | if platform == "win32": 28 | return package_path / "driver" / "playwright.cmd" 29 | return package_path / "driver" / "playwright.sh" 30 | 31 | 32 | def get_driver_env() -> dict: 33 | env = os.environ.copy() 34 | env["PW_LANG_NAME"] = "python" 35 | env["PW_LANG_NAME_VERSION"] = f"{sys.version_info.major}.{sys.version_info.minor}" 36 | # env["PW_CLI_DISPLAY_VERSION"] = version 37 | return env 38 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # These are types that we use in the API. They are public and are a part of the 16 | # stable API. 17 | 18 | 19 | from typing import Optional 20 | 21 | 22 | def is_target_closed_error(error: Exception) -> bool: 23 | return isinstance(error, TargetClosedError) 24 | 25 | 26 | class Error(Exception): 27 | def __init__(self, message: str) -> None: 28 | self._message = message 29 | self._name: Optional[str] = None 30 | self._stack: Optional[str] = None 31 | super().__init__(message) 32 | 33 | @property 34 | def message(self) -> str: 35 | return self._message 36 | 37 | @property 38 | def name(self) -> Optional[str]: 39 | return self._name 40 | 41 | @property 42 | def stack(self) -> Optional[str]: 43 | return self._stack 44 | 45 | 46 | class TimeoutError(Error): 47 | pass 48 | 49 | 50 | class TargetClosedError(Error): 51 | def __init__(self, message: str = None) -> None: 52 | super().__init__(message or "Target page, context or browser has been closed") 53 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_event_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from typing import Any, Generic, TypeVar 17 | 18 | T = TypeVar("T") 19 | 20 | 21 | class EventContextManagerImpl(Generic[T]): 22 | def __init__(self, future: asyncio.Future) -> None: 23 | self._future: asyncio.Future = future 24 | 25 | @property 26 | def future(self) -> asyncio.Future: 27 | return self._future 28 | 29 | async def __aenter__(self) -> asyncio.Future: 30 | return self._future 31 | 32 | async def __aexit__(self, *args: Any) -> None: 33 | await self._future 34 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_file_chooser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pathlib import Path 16 | from typing import TYPE_CHECKING, Sequence, Union 17 | 18 | from undetected_playwright._impl._api_structures import FilePayload 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from undetected_playwright._impl._element_handle import ElementHandle 22 | from undetected_playwright._impl._page import Page 23 | 24 | 25 | class FileChooser: 26 | def __init__( 27 | self, page: "Page", element_handle: "ElementHandle", is_multiple: bool 28 | ) -> None: 29 | self._page = page 30 | self._loop = page._loop 31 | self._dispatcher_fiber = page._dispatcher_fiber 32 | self._element_handle = element_handle 33 | self._is_multiple = is_multiple 34 | 35 | def __repr__(self) -> str: 36 | return f"" 37 | 38 | @property 39 | def page(self) -> "Page": 40 | return self._page 41 | 42 | @property 43 | def element(self) -> "ElementHandle": 44 | return self._element_handle 45 | 46 | def is_multiple(self) -> bool: 47 | return self._is_multiple 48 | 49 | async def set_files( 50 | self, 51 | files: Union[ 52 | str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] 53 | ], 54 | timeout: float = None, 55 | noWaitAfter: bool = None, 56 | ) -> None: 57 | await self._element_handle.set_input_files(files, timeout, noWaitAfter) 58 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_json_pipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from typing import Dict, cast 17 | 18 | from pyee.asyncio import AsyncIOEventEmitter 19 | 20 | from undetected_playwright._impl._connection import Channel 21 | from undetected_playwright._impl._helper import Error, ParsedMessagePayload 22 | from undetected_playwright._impl._transport import Transport 23 | 24 | 25 | class JsonPipeTransport(AsyncIOEventEmitter, Transport): 26 | def __init__( 27 | self, 28 | loop: asyncio.AbstractEventLoop, 29 | pipe_channel: Channel, 30 | ) -> None: 31 | super().__init__(loop) 32 | Transport.__init__(self, loop) 33 | self._stop_requested = False 34 | self._pipe_channel = pipe_channel 35 | self._loop: asyncio.AbstractEventLoop 36 | 37 | def request_stop(self) -> None: 38 | self._stop_requested = True 39 | self._pipe_channel.send_no_reply("close", {}) 40 | 41 | def dispose(self) -> None: 42 | self.on_error_future.cancel() 43 | self._stopped_future.cancel() 44 | 45 | async def wait_until_stopped(self) -> None: 46 | await self._stopped_future 47 | 48 | async def connect(self) -> None: 49 | self._stopped_future: asyncio.Future = asyncio.Future() 50 | 51 | def handle_message(message: Dict) -> None: 52 | if self._stop_requested: 53 | return 54 | self.on_message(cast(ParsedMessagePayload, message)) 55 | 56 | def handle_closed() -> None: 57 | self.emit("close") 58 | self._stopped_future.set_result(None) 59 | 60 | self._pipe_channel.on( 61 | "message", 62 | lambda params: handle_message(params["message"]), 63 | ) 64 | self._pipe_channel.on( 65 | "closed", 66 | lambda _: handle_closed(), 67 | ) 68 | 69 | async def run(self) -> None: 70 | await self._stopped_future 71 | 72 | def send(self, message: Dict) -> None: 73 | if self._stop_requested: 74 | raise Error("Playwright connection closed") 75 | self._pipe_channel.send_no_reply("send", {"message": message}) 76 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_map.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict, Generic, Tuple, TypeVar 15 | 16 | K = TypeVar("K") 17 | V = TypeVar("V") 18 | 19 | 20 | class Map(Generic[K, V]): 21 | def __init__(self) -> None: 22 | self._entries: Dict[int, Tuple[K, V]] = {} 23 | 24 | def __contains__(self, item: K) -> bool: 25 | return id(item) in self._entries 26 | 27 | def __setitem__(self, idx: K, value: V) -> None: 28 | self._entries[id(idx)] = (idx, value) 29 | 30 | def __getitem__(self, obj: K) -> V: 31 | return self._entries[id(obj)][1] 32 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_path_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import inspect 16 | from pathlib import Path 17 | 18 | 19 | def get_file_dirname() -> Path: 20 | """Returns the callee (`__file__`) directory name""" 21 | frame = inspect.stack()[1] 22 | module = inspect.getmodule(frame[0]) 23 | assert module 24 | assert module.__file__ 25 | return Path(module.__file__).parent.absolute() 26 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_playwright.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | from undetected_playwright._impl._browser_type import BrowserType 18 | from undetected_playwright._impl._connection import ChannelOwner, from_channel 19 | from undetected_playwright._impl._fetch import APIRequest 20 | from undetected_playwright._impl._selectors import Selectors, SelectorsOwner 21 | 22 | 23 | class Playwright(ChannelOwner): 24 | devices: Dict 25 | selectors: Selectors 26 | chromium: BrowserType 27 | firefox: BrowserType 28 | webkit: BrowserType 29 | request: APIRequest 30 | 31 | def __init__( 32 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 33 | ) -> None: 34 | super().__init__(parent, type, guid, initializer) 35 | self.request = APIRequest(self) 36 | self.chromium = from_channel(initializer["chromium"]) 37 | self.chromium._playwright = self 38 | self.firefox = from_channel(initializer["firefox"]) 39 | self.firefox._playwright = self 40 | self.webkit = from_channel(initializer["webkit"]) 41 | self.webkit._playwright = self 42 | 43 | self.selectors = Selectors(self._loop, self._dispatcher_fiber) 44 | selectors_owner: SelectorsOwner = from_channel(initializer["selectors"]) 45 | self.selectors._add_channel(selectors_owner) 46 | 47 | self._connection.on( 48 | "close", lambda: self.selectors._remove_channel(selectors_owner) 49 | ) 50 | self.devices = self._connection.local_utils.devices 51 | 52 | def __getitem__(self, value: str) -> "BrowserType": 53 | if value == "chromium": 54 | return self.chromium 55 | elif value == "firefox": 56 | return self.firefox 57 | elif value == "webkit": 58 | return self.webkit 59 | raise ValueError("Invalid browser " + value) 60 | 61 | def _set_selectors(self, selectors: Selectors) -> None: 62 | selectors_owner = from_channel(self._initializer["selectors"]) 63 | self.selectors._remove_channel(selectors_owner) 64 | self.selectors = selectors 65 | self.selectors._add_channel(selectors_owner) 66 | 67 | async def stop(self) -> None: 68 | pass 69 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_str_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import re 17 | from typing import Pattern, Union 18 | 19 | 20 | def escape_regex_flags(pattern: Pattern) -> str: 21 | flags = "" 22 | if pattern.flags != 0: 23 | flags = "" 24 | if (pattern.flags & int(re.IGNORECASE)) != 0: 25 | flags += "i" 26 | if (pattern.flags & int(re.DOTALL)) != 0: 27 | flags += "s" 28 | if (pattern.flags & int(re.MULTILINE)) != 0: 29 | flags += "m" 30 | assert ( 31 | pattern.flags 32 | & ~(int(re.MULTILINE) | int(re.IGNORECASE) | int(re.DOTALL) | int(re.UNICODE)) 33 | == 0 34 | ), "Unexpected re.Pattern flag, only MULTILINE, IGNORECASE and DOTALL are supported." 35 | return flags 36 | 37 | 38 | def escape_for_regex(text: str) -> str: 39 | return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text) 40 | 41 | 42 | def escape_regex_for_selector(text: Pattern) -> str: 43 | # Even number of backslashes followed by the quote -> insert a backslash. 44 | return ( 45 | "/" 46 | + re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace( 47 | ">>", "\\>\\>" 48 | ) 49 | + "/" 50 | + escape_regex_flags(text) 51 | ) 52 | 53 | 54 | def escape_for_text_selector( 55 | text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None 56 | ) -> str: 57 | if isinstance(text, Pattern): 58 | return escape_regex_for_selector(text) 59 | return json.dumps(text) + ("s" if exact else "i") 60 | 61 | 62 | def escape_for_attribute_selector( 63 | value: Union[str, Pattern], exact: bool = None 64 | ) -> str: 65 | if isinstance(value, Pattern): 66 | return escape_regex_for_selector(value) 67 | # TODO: this should actually be 68 | # cssEscape(value).replace(/\\ /g, ' ') 69 | # However, our attribute selectors do not conform to CSS parsing spec, 70 | # so we escape them differently. 71 | return ( 72 | '"' 73 | + value.replace("\\", "\\\\").replace('"', '\\"') 74 | + '"' 75 | + ("s" if exact else "i") 76 | ) 77 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_stream.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | from pathlib import Path 17 | from typing import Dict, Union 18 | 19 | from undetected_playwright._impl._connection import ChannelOwner 20 | 21 | 22 | class Stream(ChannelOwner): 23 | def __init__( 24 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 25 | ) -> None: 26 | super().__init__(parent, type, guid, initializer) 27 | 28 | async def save_as(self, path: Union[str, Path]) -> None: 29 | file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) 30 | while True: 31 | binary = await self._channel.send("read", {"size": 1024 * 1024}) 32 | if not binary: 33 | break 34 | await self._loop.run_in_executor( 35 | None, lambda: file.write(base64.b64decode(binary)) 36 | ) 37 | await self._loop.run_in_executor(None, lambda: file.close()) 38 | 39 | async def read_all(self) -> bytes: 40 | binary = b"" 41 | while True: 42 | chunk = await self._channel.send("read", {"size": 1024 * 1024}) 43 | if not chunk: 44 | break 45 | binary += base64.b64decode(chunk) 46 | return binary 47 | -------------------------------------------------------------------------------- /undetected_playwright/_impl/_video.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from typing import TYPE_CHECKING, Union 17 | 18 | from undetected_playwright._impl._artifact import Artifact 19 | from undetected_playwright._impl._helper import Error 20 | 21 | if TYPE_CHECKING: # pragma: no cover 22 | from undetected_playwright._impl._page import Page 23 | 24 | 25 | class Video: 26 | def __init__(self, page: "Page") -> None: 27 | self._loop = page._loop 28 | self._dispatcher_fiber = page._dispatcher_fiber 29 | self._page = page 30 | self._artifact_future = page._loop.create_future() 31 | if page.is_closed(): 32 | self._page_closed() 33 | else: 34 | page.on("close", lambda page: self._page_closed()) 35 | 36 | def __repr__(self) -> str: 37 | return f"