Repository: pyppeteer/pyppeteer2 Branch: dev Commit: 7dc91ee5173d Files: 143 Total size: 588.3 KB Directory structure: gitextract_a3lak9ot/ ├── .circleci/ │ └── config.yml ├── .coveragerc ├── .gitignore ├── .noserc ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── custom.css │ ├── _templates/ │ │ └── layout.html │ ├── changes.md │ ├── conf.py │ ├── index.md │ ├── make.bat │ ├── reference.md │ └── server.py ├── pyppeteer/ │ ├── __init__.py │ ├── browser.py │ ├── chromium_downloader.py │ ├── command.py │ ├── connection.py │ ├── coverage.py │ ├── dialog.py │ ├── element_handle.py │ ├── emulation_manager.py │ ├── errors.py │ ├── execution_context.py │ ├── frame_manager.py │ ├── helper.py │ ├── input.py │ ├── launcher.py │ ├── multimap.py │ ├── navigator_watcher.py │ ├── network_manager.py │ ├── options.py │ ├── page.py │ ├── target.py │ ├── tracing.py │ ├── us_keyboard_layout.py │ ├── util.py │ └── worker.py ├── pyproject.toml ├── spell.txt ├── tests/ │ ├── __init__.py │ ├── base.py │ ├── closeme.py │ ├── dumpio.py │ ├── file-to-upload.txt │ ├── frame_utils.py │ ├── server.py │ ├── static/ │ │ ├── beforeunload.html │ │ ├── button.html │ │ ├── cached/ │ │ │ ├── one-style.css │ │ │ └── one-style.html │ │ ├── checkbox.html │ │ ├── csp.html │ │ ├── csscoverage/ │ │ │ ├── involved.html │ │ │ ├── media.html │ │ │ ├── multiple.html │ │ │ ├── simple.html │ │ │ ├── sourceurl.html │ │ │ ├── stylesheet1.css │ │ │ ├── stylesheet2.css │ │ │ └── unused.html │ │ ├── detect-touch.html │ │ ├── error.html │ │ ├── es6/ │ │ │ ├── es6import.js │ │ │ ├── es6module.js │ │ │ └── es6pathimport.js │ │ ├── fileupload.html │ │ ├── frame-204.html │ │ ├── frame.html │ │ ├── grid.html │ │ ├── historyapi.html │ │ ├── huge-page.html │ │ ├── injectedfile.js │ │ ├── injectedstyle.css │ │ ├── jscoverage/ │ │ │ ├── eval.html │ │ │ ├── involved.html │ │ │ ├── multiple.html │ │ │ ├── ranges.html │ │ │ ├── script1.js │ │ │ ├── script2.js │ │ │ ├── simple.html │ │ │ ├── sourceurl.html │ │ │ └── unused.html │ │ ├── keyboard.html │ │ ├── mobile.html │ │ ├── modernizr.js │ │ ├── mouse-helper.js │ │ ├── nested-frames.html │ │ ├── offscreenbuttons.html │ │ ├── one-frame.html │ │ ├── one-style.css │ │ ├── one-style.html │ │ ├── popup/ │ │ │ ├── popup.html │ │ │ └── window-open.html │ │ ├── resetcss.html │ │ ├── script.js │ │ ├── scrollable.html │ │ ├── select.html │ │ ├── self-request.html │ │ ├── serviceworkers/ │ │ │ ├── empty/ │ │ │ │ ├── sw.html │ │ │ │ └── sw.js │ │ │ └── fetch/ │ │ │ ├── style.css │ │ │ ├── sw.html │ │ │ └── sw.js │ │ ├── shadow.html │ │ ├── simple-extension/ │ │ │ ├── index.js │ │ │ └── manifest.json │ │ ├── simple.json │ │ ├── style.css │ │ ├── sw.js │ │ ├── temperable.html │ │ ├── textarea.html │ │ ├── touches.html │ │ ├── two-frames.html │ │ ├── worker/ │ │ │ ├── worker.html │ │ │ └── worker.js │ │ └── wrappedlink.html │ ├── test_abnormal_crash.py │ ├── test_browser.py │ ├── test_browser_context.py │ ├── test_connection.py │ ├── test_coverage.py │ ├── test_dialog.py │ ├── test_element_handle.py │ ├── test_execution_context.py │ ├── test_frame.py │ ├── test_input.py │ ├── test_launcher.py │ ├── test_misc.py │ ├── test_network.py │ ├── test_page.py │ ├── test_pyppeteer.py │ ├── test_screenshot.py │ ├── test_target.py │ ├── test_tracing.py │ ├── test_worker.py │ └── utils.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 orbs: codecov: codecov/codecov@1.0.5 workflows: main: jobs: - lint - mypy - test_36 - test_37 - test_38 - test_39 - test_310 jobs: test_36: docker: - image: circleci/python:3.6 environment: TOXENV: py36 PYTEST_ADDOPTS: -n 8 --junitxml=/tmp/tests/pytest/results.xml --cov=./ steps: &step_template - checkout - restore_cache: keys: - poetry_deps_{{checksum "poetry.lock"}} - run: name: Install headless Chrome dependencies # chrome headless libs, see # https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix command: | sudo apt install -yq \ ca-certificates fonts-liberation libasound2 libatk1.0-0 \ libcairo2 libcups2 libdbus-1-3 libgdk-pixbuf2.0-0 \ libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 \ libpangocairo-1.0-0 libx11-xcb1 libxcomposite1 libxcursor1 \ libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ lsb-release xdg-utils wget - run: name: Install tox command: pip install tox - run: name: Run tests command: tox - save_cache: key: poetry_deps_{{checksum "poetry.lock"}} paths: ~/.cache/pypoetry/ - store_test_results: path: /tmp/tests/ # this step will simply fail for other jobs - codecov/upload: file: ./pytest-cov.pth test_37: docker: - image: circleci/python:3.7 environment: TOXENV: py37 PYTEST_ADDOPTS: &pytest_default -n 8 --junitxml=/tmp/tests/pytest/results.xml steps: *step_template test_38: docker: - image: circleci/python:3.8 environment: TOXENV: py38 PYTEST_ADDOPTS: *pytest_default steps: *step_template test_39: docker: - image: circleci/python:3.9 environment: TOXENV: py39 PYTEST_ADDOPTS: *pytest_default steps: *step_template test_310: docker: - image: circleci/python:3.10-rc environment: TOXENV: py310 PYTEST_ADDOPTS: *pytest_default steps: *step_template mypy: docker: - image: circleci/python:3.6 environment: TOXENV: mypy MYPY_JUNIT_XML_PATH: /tmp/tests/mypy/results.xml steps: - checkout - run: name: Install tox command: pip install tox - run: name: Check typing command: tox - store_test_results: path: /tmp/tests lint: docker: - image: circleci/python:3.6 environment: TOXENV: flake8 steps: - checkout - run: name: Install tox command: pip install tox - run: name: Check code style command: tox ================================================ FILE: .coveragerc ================================================ [run] omit=setup.py source=pyppeteer,tests ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Virtualenv env/ venv/ bin/ include/ lib/ lib64 lib64/ man/ pyvenv.cfg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache .doit.db.* .mypy_cache nosetests.xml coverage.xml *,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv python configuration file .python-version # pycharm file .idea/ ###### direnv ###### .direnv .envrc ###### zsh-autoenv ###### .autoenv.zsh .autoenv_leave.zsh # test files trace.json ================================================ FILE: .noserc ================================================ [nosetests] logging-level=INFO # no-path-adjustment=true # with-coverage=true # cover-package=pyppeteer ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-toml - id: check-builtin-literals - id: debug-statements - id: check-added-large-files - repo: https://github.com/asottile/seed-isort-config rev: v2.1.1 hooks: - id: seed-isort-config # if we don't specify these the seeder will intermittently include # these as 'known third parties' which messes with our diffs args: ['--application-directories', './pyppeteer:./tests'] - repo: https://github.com/timothycrosley/isort rev: 4.3.21 hooks: - id: isort additional_dependencies: [toml] - repo: https://github.com/psf/black rev: stable hooks: - id: black language_version: python3 ================================================ FILE: CHANGELOG.md ================================================ History ======= ## Version 2.0.0 * Bump pyee version, which removes support for Python 3.7 * Bumped included browser version to revision 1181205. It may not match the base p*u*ppeteer version, but at least it runs * Fix invalid escape sequence (#453) * Fix deprecated asyncio wait in page.py (#451) * Remove version_info. `version` is still present and can be parsed if necessary ## Version 1.0.2 * Fix circular import as result of 1.0.1 (#344) ## Version 1.0.1 * don't configure logging ourselves (#343) * make logging better for chromium downloader ## Version 1.0.0 * hotfix: websockets 10 for python 3.10 (#321, #327) * removes support for python 3.6./ * remove `Page.craete` ## Version 0.2.6 * Change build backend to poetry-core (allows for faster PEP517 package building) #262 @fabaff * Chromium download fixes (file not found) #245 @mborsetti * Do not try to set an exception on finished futures #216 @polyfloyd * Add HTTPException to caught exceptions in launch #293 @raymondguo-db * Fix encoding error #226 @aleksei140888 * support websockets 9.0 #252 @mborsetti * Fix tqdm exception when NO_PROGRESS_BAR is True #224 @mborsetti * fix(browser): Clean up coroutine Browser._targetCreated() #271 @H--o-I ## Version 0.2.5 * Match package version and \_\_version__ (🤦‍♂️) * Use `importlib_metadata` so this isn't a problem in the future ## Version 0.2.4 * Update `pyee` dependency breaking build failures on NixOS + Fedora packaging systems (#207) ## Version 0.2.3 * Hotfix: random freezes from sending stdout to PIPE instead of DEVNULL * Fix `tests` package being installed for no reason ## Version 0.0.26 * Add `$PYPPETEER_NO_PROGRESS_BAR` environment variable * `pyppeteer.defaultArgs` now accepts that help infer chromium command-line flags. * `pyppeteer.launch()` argument `ignoreDefaultArgs` now accepts a list of flags to ignore. * `Page.type()` now supports typing emoji * `Page.pdf()` accepts a new argument `preferCSSPageSize` * Add new option `defaultViewport` to `launch()` and `connect()` * Add `BrowserContext.pages()` method ## Version 0.0.25 (2018-09-27) * Fix miss-spelled methods and functions * Change `Browser.isIncognite` to `Browser.isIncognito` * Change `Browser.createIncogniteBrowserContext` to `Browser.createIncognitoBrowserContext` * Change `chromium_excutable` to `chromium_executable` * Remove `craete` function in `page.py` ## Version 0.0.24 (2018-09-12) Catch up puppeteer v1.6.0 * Add `ElementHandle.isIntersectingViewport()` * Add `reportAnonymousScript` option to `Coverage.startJSCoverage()` * Add `Page.waitForRequest` and `Page.waitForResponse` methods * Now possible to attach to extension background pages with `Target.page()` * Improved reliability of clicking with `Page.click()` and `ElementHandle.click()` ## Version 0.0.23 (2018-09-10) Catch up puppeteer v1.5.0 * Add `BrowserContext` class * Add `Worker` class * Change `CDPSession.send` to a normal function which returns awaitable value * Add `Page.isClosed` method * Add `ElementHandle.querySelectorAllEval` and `ElementHandle.JJeval` * Add `Target.opener` * Add `Request.isNavigationRequest` ## Version 0.0.22 (2018-09-06) Catch up puppeteer v1.4.0 * Add `pyppeteer.DEBUG` variable * Add `Page.browser` * Add `Target.browser` * Add `ElementHandle.querySelectorEval` and `ElementHandle.Jeval` * Add `runBeforeUnload` option to `Page.close` method * Change `Page.querySelectorEval` to raise `ElementHandleError` when element which matches `selector` is not found * Report 'Log' domain entries as 'console' events * Fix `Page.goto` to return response when page pushes new state * (OS X) Suppress long log when extracting chromium ## Version 0.0.21 (2018-08-21) Catch up puppeteer v1.3.0 * Add `pyppeteer-install` command * Add `autoClose` option to `launch` function * Add `loop` option to `launch` function (experimental) * Add `Page.setBypassCSP` method * `Page.tracing.stop` returns result data * Rename `documentloaded` to `domcontentloaded` on `waitUntil` option * Fix `slowMo` option * Fix anchor navigation * Fix to return response via redirects * Continue to find WS URL while process is alive ## Version 0.0.20 (2018-08-11) * Run on msys/cygwin, anyway * Raise error correctly when connection failed (PR#91) * Change browser download location and temporary user data directory to: * If `$PYPPETEER_HOME` environment variable is defined, use this location * Otherwise, use platform dependent locations, based on [appdirs](https://pypi.org/project/appdirs/): * `'C:\Users\\AppData\Local\pyppeteer'` (Windows) * `'/Users//Library/Application Support/pyppeteer'` (OS X) * `'/home//.local/share/pyppeteer'` (Linux) * or in `'$XDG_DATA_HOME/pyppeteer'` if `$XDG_DATA_HOME` is defined * Introduce `$PYPPETEER_CHROMIUM_REVISION` * Introduce `$PYPPETEER_HOME` * Add `logLevel` option to `launch` and `connect` functions * Add page `close` event * Add `ElementHandle.boxModel` method * Add an option to disable timeout for `waitFor` functions ## Version 0.0.19 (2018-07-05) Catch up puppeteer v1.2.0 * Add `ElementHandle.contentFrame` method * Add `Request.redirectChain` method * `Page.addScriptTag` accepts a new option `type` ## Version 0.0.18 (2018-07-04) Catch up puppeteer v1.1.1 * Add `Page.waitForXPath` and `Frame.waitForXPath` * `Page.waitFor` accepts xpath string which starts with `//` * Add `Response.fromCache` and `Response.fromServiceWorker` * Add `SecurityDetails` class and `response.securityDetails` * Add `Page.setCacheEnabled` method * Add `ExecutionContext.frame` * Add `dumpio` option to `launch` function * Add `slowMo` option to `connect` function * `launcher.connect` can be access from package top * `from pyppeteer import connect` is now valid * Add `Frame.evaluateHandle` * Add `Page.Events.DOMContentLoaded` ## Version 0.0.17 (2018-04-02) * Mark as alpha * Gracefully terminate browser process * `Request.method` and `Request.postData` return `None` if no data * Change `Target.url` and `Target.type` to properties * Change `Dialog.message` and `Dialog.defaultValue` to properties * Fix: properly emit `Browser.targetChanged` events * Fix: properly emit `Browser.targetDestroyed` events ## Version 0.0.16 (2018-03-23) * BugFix: Skip SIGHUP option on windows (windows does not support this signal) ## Version 0.0.15 (2018-03-22) Catch up puppeteer v1.0.0 * Support `raf` and `mutation` polling for `waitFor*` methods * Add `Page.coverage` to support JS and CSS coverage * Add XPath support with `Page.xpath`, `Frame.xpath`, and `ElementHandle.xpath` * Add `Target.createCDPSession` to work with raw Devtools Protocol * Change `Frame.executionContext` from property to coroutine * Add `ignoreDefaultArgs` option to `pyppeteer.launch` * Add `handleSIGINT`/`handleSIGTERM`/`handleSIGHUP` options to `pyppeteer.launch` * Add `Page.setDefaultNavigationTimeout` method * `Page.waitFor*` methods accept `JSHandle` as argument * Implement `Frame.content` and `Frame.setContent` methods * `page.tracing.start` accepts custom tracing categories option * Add `Browser.process` property * Add `Request.frame` property ## Version 0.0.14 (2018-03-14) * Read WS endpoint from web interface instead of stdout * Pass environment variables of python process to chrome by default * Do not limit size of websocket frames * BugFix: * `Keyboard.type` * `Page.Events.Metrics` ## Version 0.0.13 (2018-03-10) Catch up puppeteer v0.13.0 * `pyppeteer.launch()` is now **coroutine** * Implement `connect` function * `PYPPETEER_DOWNLOAD_HOST` env variable specifies host part of URL to download chromium * Rename `setRequestInterceptionEnable` to `setRequestInterception` * Rename `Page.getMetrics` to `Page.metrics` * Implement `Browser.pages` to access all pages * Add `Target` class and some new method on Browser * Add `ElementHandle.querySelector` and `ElementHandle.querySelectorAll` * Refactor NavigatorWatcher * add `documentloaded`, `networkidle0`, and `networkidle2` options * `Request.abort` accepts error code * `addScriptTag` and `addStyleTag` return `ElementHandle` * Add `force_expr` option to `evaluate` method * `Page.select` returns selected values * Add `pyppeteer.version` and `pyppeteer.version_info` * BugFix: * Do not change original options dictionary * `Page.frames` * `Page.queryObjects` * `Page.exposeFunction` * Request interception * Console API * websocket error on closing browser (#24) ## Version 0.0.12 (2018-03-01) * BugFix (#33) ## Version 0.0.11 (2018-03-01) Catch up puppeteer v0.12.0 * Remove `ElementHandle.evaluate` * Remove `ElementHandle.attribute` * Deprecate `Page.plainText` * Deprecate `Page.injectFile` * Add `Page.querySelectorAllEval` * Add `Page.select` and `Page.type` * Add `ElementHandle.boundingBox` and `ElementHandle.screenshot` * Add `ElementHandle.focus`, `ElementHandle.type`, and `ElementHandle.press` * Add `getMetrics` method * Add `offlineMode` ## Version 0.0.10 (2018-02-27) * Enable to import `launch` from package root * Change `browser.close` to coroutine function * Catch up puppeteer v0.11.0 ### Version 0.0.9 (2017-09-09) * Delete temporary user data directory when browser closed * Fix bug to fail extracting zip on mac ### Version 0.0.8 (2017-09-03) * Change chromium revision * Support steps option of `Mouse.move()` * Experimentally supports python 3.5 by py-backwards ### Version 0.0.7 (2017-09-03) * Catch up puppeteer v0.10.2 * Add `Page.querySelectorEval` (`Page.$eval` in puppeteer) * Deprecate `ElementHandle.attribute` * Add `Touchscreen` class and implement `Page.tap` and `ElementHandle.tap` ### Version 0.0.6 (2017-09-02) * Accept keyword arguments for options * Faster polling on `waitFor*` functions * Fix bugs ### Version 0.0.5 (2017-08-30) * Implement pdf printing * Implement `waitFor*` functions ### Version 0.0.4 (2017-08-30) * Register PyPI ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution guidelines Contributions are welcome as long as they follow core rule of the project: The API of pyppeteer should [__match the API of puppeteer__](https://github.com/puppeteer/puppeteer) as closely as possible without sacrificing python too much. ie keep public API keywords such as method names, arguments, class names etc. as they are in puppeteer version. Other than that the contributions should remain as pythonic as possible and pass linting and code tests. Changes worthy of a changelog entry should get one - simply follow the existing format in CHANGELOG.md ## Maintainers - creating a release - Make sure all relevant changes have been recorded in the changelog - Ensure that code is properly tested - Bump the version in `pyproject.toml`, then tag the release in git - ex: `git tag -a 2.0.0rc1 -m "pypi release"` - Run `poetry build` - Run `poetry publish` ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017, Hiroyuki Takagi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This software includes the work that is distributed in the Apache License 2.0. ================================================ FILE: README.md ================================================ ### Attention: This repo is unmaintained and has been outside of minor changes for a long time. Please consider [playwright-python](https://github.com/microsoft/playwright-python) as an alternative. If you are interested in maintaining this, please contact [me](https://github.com/Mattwmaster58) pyppeteer ========== [![PyPI](https://img.shields.io/pypi/v/pyppeteer.svg)](https://pypi.python.org/pypi/pyppeteer) [![PyPI version](https://img.shields.io/pypi/pyversions/pyppeteer.svg)](https://pypi.python.org/pypi/pyppeteer) [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://pyppeteer.github.io/pyppeteer/) [![CircleCI](https://circleci.com/gh/pyppeteer/pyppeteer.svg?style=shield)](https://circleci.com/gh/pyppeteer/pyppeteer) [![codecov](https://codecov.io/gh/pyppeteer/pyppeteer/branch/dev/graph/badge.svg)](https://codecov.io/gh/pyppeteer/pyppeteer) _Note: this is a continuation of the [pyppeteer project](https://github.com/miyakogi/pyppeteer)_ Unofficial Python port of [puppeteer](https://github.com/GoogleChrome/puppeteer) JavaScript (headless) chrome/chromium browser automation library. * Free software: MIT license (including the work distributed under the Apache 2.0 license) * Documentation: https://pyppeteer.github.io/pyppeteer/ ## Installation pyppeteer requires Python >= 3.8 Install with `pip` from PyPI: ``` pip install pyppeteer ``` Or install the latest version from [this github repo](https://github.com/pyppeteer/pyppeteer/): ``` pip install -U git+https://github.com/pyppeteer/pyppeteer@dev ``` ## Usage > **Note**: When you run pyppeteer for the first time, it downloads the latest version of Chromium (~150MB) if it is not found on your system. If you don't prefer this behavior, ensure that a suitable Chrome binary is installed. One way to do this is to run `pyppeteer-install` command before prior to using this library. Full documentation can be found [here](https://pyppeteer.github.io/pyppeteer/reference.html). [Puppeteer's documentation](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#) and [its troubleshooting guide](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md) are also great resources for pyppeteer users. ### Examples Open web page and take a screenshot: ```py import asyncio from pyppeteer import launch async def main(): browser = await launch() page = await browser.newPage() await page.goto('https://example.com') await page.screenshot({'path': 'example.png'}) await browser.close() asyncio.get_event_loop().run_until_complete(main()) ``` Evaluate javascript on a page: ```py import asyncio from pyppeteer import launch async def main(): browser = await launch() page = await browser.newPage() await page.goto('https://example.com') await page.screenshot({'path': 'example.png'}) dimensions = await page.evaluate('''() => { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio, } }''') print(dimensions) # >>> {'width': 800, 'height': 600, 'deviceScaleFactor': 1} await browser.close() asyncio.get_event_loop().run_until_complete(main()) ``` ## Differences between puppeteer and pyppeteer pyppeteer strives to replicate the puppeteer API as close as possible, however, fundamental differences between Javascript and Python make this difficult to do precisely. More information on specifics can be found in the [documentation](https://pyppeteer.github.io/pyppeteer/reference.html). ### Keyword arguments for options puppeteer uses an object for passing options to functions/methods. pyppeteer methods/functions accept both dictionary (python equivalent to JavaScript's objects) and keyword arguments for options. Dictionary style options (similar to puppeteer): ```python browser = await launch({'headless': True}) ``` Keyword argument style options (more pythonic, isn't it?): ```python browser = await launch(headless=True) ``` ### Element selector method names In python, `$` is not a valid identifier. The equivalent methods to Puppeteer's `$`, `$$`, and `$x` methods are listed below, along with some shorthand methods for your convenience: | puppeteer | pyppeteer | pyppeteer shorthand | |-----------|-------------------------|----------------------| | Page.$() | Page.querySelector() | Page.J() | | Page.$$() | Page.querySelectorAll() | Page.JJ() | | Page.$x() | Page.xpath() | Page.Jx() | ### Arguments of `Page.evaluate()` and `Page.querySelectorEval()` puppeteer's version of `evaluate()` takes a JavaScript function or a string representation of a JavaScript expression. pyppeteer takes string representation of JavaScript expression or function. pyppeteer will try to automatically detect if the string is function or expression, but it will fail sometimes. If an expression is erroneously treated as function and an error is raised, try setting `force_expr` to `True`, to force pyppeteer to treat the string as expression. ### Examples: Get a page's `textContent`: ```python content = await page.evaluate('document.body.textContent', force_expr=True) ``` Get an element's `textContent`: ```python element = await page.querySelector('h1') title = await page.evaluate('(element) => element.textContent', element) ``` ## Roadmap See [projects](https://github.com/pyppeteer/pyppeteer/projects) ## Credits ###### This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) project template. ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyppeteer.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyppeteer.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pyppeteer" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyppeteer" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/_static/custom.css ================================================ h1.logo { font-family: "Raleway"; font-weight: 500; } a.headerlink { color: rgba(0, 0, 0, 0.1); } div.sphinxsidebarwrapper p.blurb { font-family: Lato, sans-serif; } div.sphinxsidebar li.toctree-l1 { font-family: Lato, sans-serif; } body { background-color: #fafafa } .search-btn { padding: 0 1em; font-family: Lato, sans-serif; font-weight: normal; line-height: normal; align-self: stretch; } ================================================ FILE: docs/_templates/layout.html ================================================ {% extends 'alabaster/layout.html' %} {% block extrahead %} {{ super() }} {% endblock %} ================================================ FILE: docs/changes.md ================================================ .. mdinclude:: ../CHANGES.md ================================================ FILE: docs/conf.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # pyppeteer documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() project_root = os.path.dirname(cwd) # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. sys.path.insert(0, project_root) import pyppeteer # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinx.ext.viewcode', # 'sphinx_autodoc_typehints', 'sphinxcontrib.asyncio', 'm2r', ] primary_domain = 'py' default_role = 'py:obj' # autodoc_member_order = 'bysource' # include class' and __init__'s docstring # autoclass_content = 'both' # autodoc_docstring_signature = False autodoc_default_flags = ['show-inheritance'] suppress_warnings = ['image.nonlocal_uri'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: source_suffix = ['.rst', '.md'] # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Pyppeteer' copyright = "2017, Hiroyuki Takagi" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The short X.Y version. version = pyppeteer.__version__ # The full version, including alpha/beta/rc tags. release = pyppeteer.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to # some non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built # documents. # keep_warnings = False # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. html_theme_options = { 'description': ('Headless chrome/chromium automation library ' '(unofficial port of puppeteer)'), 'github_user': 'miyakogi', 'github_repo': 'pyppeteer', 'github_banner': True, 'github_type': 'mark', 'github_count': False, 'font_family': '"Charis SIL", "Noto Serif", serif', 'head_font_family': 'Lato, sans-serif', 'code_font_family': '"Code new roman", "Ubuntu Mono", monospace', 'code_font_size': '1rem', } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as # html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the # top of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon # of the docs. This file should be a Windows icon file (.ico) being # 16x16 or 32x32 pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', 'searchbox.html', ] } # Additional templates that should be rendered to pages, maps page names # to template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. # Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. # Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages # will contain a tag referring to it. The value of this option # must be the base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'pyppeteerdoc' # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'pyppeteer.tex', 'pyppeteer Documentation', 'Hiroyuki Takagi', 'manual'), ] # The name of an image file (relative to this directory) to place at # the top of the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings # are parts, not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pyppeteer', 'pyppeteer Documentation', ['Hiroyuki Takagi'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'pyppeteer', 'pyppeteer Documentation', 'Hiroyuki Takagi', 'pyppeteer', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False ================================================ FILE: docs/index.md ================================================ Pyppeteer's documentation ========================= .. mdinclude:: ../README.md Contents -------- .. toctree:: :maxdepth: 2 reference changes Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyppeteer.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyppeteer.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: docs/reference.md ================================================ API Reference ============= Commands -------- * ``pyppeteer-install``: Download and install chromium for pyppeteer. Environment Variables --------------------- * ``$PYPPETEER_HOME``: Specify the directory to be used by pyppeteer. Pyppeteer uses this directory for extracting downloaded Chromium, and for making temporary user data directory. Default location depends on platform: * Windows: `C:\Users\\AppData\Local\pyppeteer` * OS X: `/Users//Library/Application Support/pyppeteer` * Linux: `/home//.local/share/pyppeteer` * or in `$XDG_DATA_HOME/pyppeteer` if `$XDG_DATA_HOME` is defined. Details see [appdirs](https://pypi.org/project/appdirs/)'s `user_data_dir`. * ``$PYPPETEER_DOWNLOAD_HOST``: Overwrite host part of URL that is used to download Chromium. Defaults to ``https://storage.googleapis.com``. * ``$PYPPETEER_CHROMIUM_REVISION``: Specify a certain version of chromium you'd like pyppeteer to use. Default value can be checked by ``pyppeteer.__chromium_revision__``. * ``$PYPPETEER_NO_PROGRESS_BAR``: Suppress showing progress bar in chromium download process. Acceptable values are ``1`` or ``true`` (case-insensitive). Pyppeteer Main Module --------------------- .. currentmodule:: pyppeteer .. autofunction:: launch .. autofunction:: connect .. autofunction:: defaultArgs .. autofunction:: executablePath Browser Class ------------- .. currentmodule:: pyppeteer.browser .. autoclass:: pyppeteer.browser.Browser :members: :exclude-members: create BrowserContext Class -------------------- .. currentmodule:: pyppeteer.browser .. autoclass:: pyppeteer.browser.BrowserContext :members: Page Class ---------- .. currentmodule:: pyppeteer.page .. autoclass:: pyppeteer.page.Page :members: :exclude-members: create Worker Class ------------ .. currentmodule:: pyppeteer.worker .. autoclass:: pyppeteer.worker.Worker :members: Keyboard Class -------------- .. currentmodule:: pyppeteer.input .. autoclass:: pyppeteer.input.Keyboard :members: Mouse Class ----------- .. currentmodule:: pyppeteer.input .. autoclass:: pyppeteer.input.Mouse :members: Tracing Class ------------- .. currentmodule:: pyppeteer.tracing .. autoclass:: pyppeteer.tracing.Tracing :members: Dialog Class ------------ .. currentmodule:: pyppeteer.dialog .. autoclass:: pyppeteer.dialog.Dialog :members: ConsoleMessage Class -------------------- .. currentmodule:: pyppeteer.page .. autoclass:: pyppeteer.page.ConsoleMessage :members: Frame Class ----------- .. currentmodule:: pyppeteer.frame .. autoclass:: pyppeteer.frame_manager.Frame :members: ExecutionContext Class ---------------------- .. currentmodule:: pyppeteer.execution_context .. autoclass:: pyppeteer.execution_context.ExecutionContext :members: JSHandle Class -------------- .. autoclass:: pyppeteer.execution_context.JSHandle :members: ElementHandle Class ------------------- .. currentmodule:: pyppeteer.element_handle .. autoclass:: pyppeteer.element_handle.ElementHandle :members: Request Class ------------- .. currentmodule:: pyppeteer.network_manager .. autoclass:: pyppeteer.network_manager.Request :members: Response Class -------------- .. currentmodule:: pyppeteer.network_manager .. autoclass:: pyppeteer.network_manager.Response :members: Target Class ------------ .. currentmodule:: pyppeteer.target .. autoclass:: pyppeteer.target.Target :members: CDPSession Class ---------------- .. currentmodule:: pyppeteer.connection .. autoclass:: pyppeteer.connection.CDPSession :members: Coverage Class -------------- .. currentmodule:: pyppeteer.coverage .. autoclass:: pyppeteer.coverage.Coverage :members: Debugging --------- For debugging, you can set `logLevel` option to `logging.DEBUG` for :func:`pyppeteer.launcher.launch` and :func:`pyppeteer.launcher.connect` functions. However, this option prints too many logs including SEND/RECV messages of pyppeteer. In order to only show suppressed error messages, you should set ``pyppeteer.DEBUG`` to ``True``. Example: ```python import asyncio import pyppeteer from pyppeteer import launch pyppeteer.DEBUG = True # print suppressed errors as error log async def main(): browser = await launch() ... # do something asyncio.get_event_loop().run_until_complete(main()) ``` ================================================ FILE: docs/server.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from os import path import subprocess from livereload import Server from livereload import watcher watcher.pyinotify = None # disable pyinotify docsdir = path.dirname(path.abspath(__file__)) builddir = path.join(docsdir, '_build') build_cmd = [ 'sphinx-build', '-q', '-j', 'auto', '-b', 'html', '-d', path.join(builddir, 'doctrees'), docsdir, path.join(builddir, 'html'), ] def cmd() -> None: print('=== Sphinx Build Start ===') subprocess.run(build_cmd, cwd=docsdir) print('=== Sphinx Build done ===') # subprocess.run(['make', 'clean'], cwd=docsdir) cmd() server = Server() def docs(p: str) -> str: return path.join(docsdir, p) # Watch documents server.watch(docs('*.py'), cmd, delay=1) server.watch(docs('*.md'), cmd, delay=1) server.watch(docs('../*.md'), cmd, delay=1) server.watch(docs('*.md'), cmd, delay=1) server.watch(docs('*/*.md'), cmd, delay=1) server.watch(docs('*/*/*.md'), cmd, delay=1) # Watch template/style server.watch(docs('_templates/*.html'), cmd, delay=1) server.watch(docs('_static/*.css'), cmd, delay=1) server.watch(docs('_static/*.js'), cmd, delay=1) # Watch package server.watch(docs('../pyppeteer/*.py'), cmd, delay=1) server.watch(docs('../pyppeteer/*/*.py'), cmd, delay=1) server.watch(docs('../pyppeteer/*/*/*.py'), cmd, delay=1) server.serve(port=8889, root=docs('_build/html'), debug=True, restart_delay=1) ================================================ FILE: pyppeteer/__init__.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Meta data for pyppeteer.""" import logging import os from appdirs import AppDirs from importlib.metadata import version try: __version__ = version(__name__) except Exception: __version__ = None # old chrome version panic upon launching - this one may not match the base puppeteer version, but at least it launches __chromium_revision__ = '1181205' __base_puppeteer_version__ = 'v1.6.0' __pyppeteer_home__ = os.environ.get('PYPPETEER_HOME', AppDirs('pyppeteer').user_data_dir) # type: str DEBUG = False from pyppeteer.launcher import connect, executablePath, launch, defaultArgs # noqa: E402; noqa: E402 version = __version__ __all__ = [ 'connect', 'launch', 'executablePath', 'defaultArgs', 'version', ] ================================================ FILE: pyppeteer/browser.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Browser module.""" import logging from subprocess import Popen from types import SimpleNamespace from typing import Any, Awaitable, Callable, Dict, List, Optional from pyee import EventEmitter from pyppeteer.connection import Connection from pyppeteer.errors import BrowserError from pyppeteer.page import Page from pyppeteer.target import Target logger = logging.getLogger(__name__) class Browser(EventEmitter): """Browser class. A Browser object is created when pyppeteer connects to chrome, either through :func:`~pyppeteer.launcher.launch` or :func:`~pyppeteer.launcher.connect`. """ Events = SimpleNamespace( TargetCreated='targetcreated', TargetDestroyed='targetdestroyed', TargetChanged='targetchanged', Disconnected='disconnected', ) def __init__(self, connection: Connection, contextIds: List[str], ignoreHTTPSErrors: bool, defaultViewport: Optional[Dict], process: Optional[Popen] = None, closeCallback: Callable[[], Awaitable[None]] = None, **kwargs: Any) -> None: super().__init__() self._ignoreHTTPSErrors = ignoreHTTPSErrors self._defaultViewport = defaultViewport self._process = process self._screenshotTaskQueue: List = [] self._connection = connection loop = self._connection._loop def _dummy_callback() -> Awaitable[None]: fut = loop.create_future() fut.set_result(None) return fut if closeCallback: self._closeCallback = closeCallback else: self._closeCallback = _dummy_callback self._defaultContext = BrowserContext(self, None) self._contexts: Dict[str, BrowserContext] = dict() for contextId in contextIds: self._contexts[contextId] = BrowserContext(self, contextId) self._targets: Dict[str, Target] = dict() self._connection.setClosedCallback( lambda: self.emit(Browser.Events.Disconnected) ) self._connection.on( 'Target.targetCreated', lambda event: loop.create_task(self._targetCreated(event)), ) self._connection.on( 'Target.targetDestroyed', lambda event: loop.create_task(self._targetDestroyed(event)), ) self._connection.on( 'Target.targetInfoChanged', lambda event: loop.create_task(self._targetInfoChanged(event)), ) @property def process(self) -> Optional[Popen]: """Return process of this browser. If browser instance is created by :func:`pyppeteer.launcher.connect`, return ``None``. """ return self._process async def createIncogniteBrowserContext(self) -> 'BrowserContext': """[Deprecated] Miss spelled method. Use :meth:`createIncognitoBrowserContext` method instead. """ logger.warning( 'createIncogniteBrowserContext is deprecated. ' 'Use createIncognitoBrowserContext instead.' ) return await self.createIncognitoBrowserContext() async def createIncognitoBrowserContext(self) -> 'BrowserContext': """Create a new incognito browser context. This won't share cookies/cache with other browser contexts. .. code:: browser = await launch() # Create a new incognito browser context. context = await browser.createIncognitoBrowserContext() # Create a new page in a pristine context. page = await context.newPage() # Do stuff await page.goto('https://example.com') ... """ obj = await self._connection.send('Target.createBrowserContext') browserContextId = obj['browserContextId'] context = BrowserContext(self, browserContextId) # noqa: E501 self._contexts[browserContextId] = context return context @property def browserContexts(self) -> List['BrowserContext']: """Return a list of all open browser contexts. In a newly created browser, this will return a single instance of ``[BrowserContext]`` """ return [self._defaultContext] + [context for context in self._contexts.values()] # noqa: E501 async def _disposeContext(self, contextId: str) -> None: await self._connection.send('Target.disposeBrowserContext', { 'browserContextId': contextId, }) self._contexts.pop(contextId, None) @staticmethod async def create(connection: Connection, contextIds: List[str], ignoreHTTPSErrors: bool, defaultViewport: Optional[Dict], process: Optional[Popen] = None, closeCallback: Callable[[], Awaitable[None]] = None, **kwargs: Any) -> 'Browser': """Create browser object.""" browser = Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) await connection.send('Target.setDiscoverTargets', {'discover': True}) return browser async def _targetCreated(self, event: Dict) -> None: targetInfo = event['targetInfo'] browserContextId = targetInfo.get('browserContextId') if browserContextId and browserContextId in self._contexts: context = self._contexts[browserContextId] else: context = self._defaultContext target = Target( targetInfo, context, lambda: self._connection.createSession(targetInfo), self._ignoreHTTPSErrors, self._defaultViewport, self._screenshotTaskQueue, self._connection._loop, ) if targetInfo['targetId'] in self._targets: raise BrowserError('target should not exist before create.') self._targets[targetInfo['targetId']] = target if await target._initializedPromise: self.emit(Browser.Events.TargetCreated, target) context.emit(BrowserContext.Events.TargetCreated, target) async def _targetDestroyed(self, event: Dict) -> None: target = self._targets[event['targetId']] del self._targets[event['targetId']] target._closedCallback() if await target._initializedPromise: self.emit(Browser.Events.TargetDestroyed, target) target.browserContext.emit(BrowserContext.Events.TargetDestroyed, target) # noqa: E501 target._initializedCallback(False) async def _targetInfoChanged(self, event: Dict) -> None: target = self._targets.get(event['targetInfo']['targetId']) if not target: raise BrowserError('target should exist before targetInfoChanged') previousURL = target.url wasInitialized = target._isInitialized target._targetInfoChanged(event['targetInfo']) if wasInitialized and previousURL != target.url: self.emit(Browser.Events.TargetChanged, target) target.browserContext.emit(BrowserContext.Events.TargetChanged, target) # noqa: E501 @property def wsEndpoint(self) -> str: """Return websocket end point url.""" return self._connection.url async def newPage(self) -> Page: """Make new page on this browser and return its object.""" return await self._defaultContext.newPage() async def _createPageInContext(self, contextId: Optional[str]) -> Page: options = {'url': 'about:blank'} if contextId: options['browserContextId'] = contextId targetId = (await self._connection.send( 'Target.createTarget', options)).get('targetId') target = self._targets.get(targetId) if target is None: raise BrowserError('Failed to create target for page.') if not await target._initializedPromise: raise BrowserError('Failed to create target for page.') page = await target.page() if page is None: raise BrowserError('Failed to create page.') return page def targets(self) -> List[Target]: """Get a list of all active targets inside the browser. In case of multiple browser contexts, the method will return a list with all the targets in all browser contexts. """ return [target for target in self._targets.values() if target._isInitialized] async def pages(self) -> List[Page]: """Get all pages of this browser. Non visible pages, such as ``"background_page"``, will not be listed here. You can find then using :meth:`pyppeteer.target.Target.page`. In case of multiple browser contexts, this method will return a list with all the pages in all browser contexts. """ # Using asyncio.gather is better for performance pages: List[Page] = list() for context in self.browserContexts: pages.extend(await context.pages()) return pages async def version(self) -> str: """Get version of the browser.""" version = await self._getVersion() return version['product'] async def userAgent(self) -> str: """Return browser's original user agent. .. note:: Pages can override browser user agent with :meth:`pyppeteer.page.Page.setUserAgent`. """ version = await self._getVersion() return version.get('userAgent', '') async def close(self) -> None: """Close connections and terminate browser process.""" await self._closeCallback() # Launcher.killChrome() async def disconnect(self) -> None: """Disconnect browser.""" await self._connection.dispose() for target in self._targets.values(): if not target._isInitialized: target._initializedCallback(False) def _getVersion(self) -> Awaitable: return self._connection.send('Browser.getVersion') class BrowserContext(EventEmitter): """BrowserContext provides multiple independent browser sessions. When a browser is launched, it has a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context. If a page opens another page, e.g. with a ``window.open`` call, the popup will belong to the parent page's browser context. Pyppeteer allows creation of "incognito" browser context with ``browser.createIncognitoBrowserContext()`` method. "incognito" browser contexts don't write any browser data to disk. .. code:: # Create new incognito browser context context = await browser.createIncognitoBrowserContext() # Create a new page inside context page = await context.newPage() # ... do stuff with page ... await page.goto('https://example.com') # Dispose context once it's no longer needed await context.close() """ Events = SimpleNamespace( TargetCreated='targetcreated', TargetDestroyed='targetdestroyed', TargetChanged='targetchanged', ) def __init__(self, browser: Browser, contextId: Optional[str]) -> None: super().__init__() self._browser = browser self._id = contextId def targets(self) -> List[Target]: """Return a list of all active targets inside the browser context.""" targets = [] for target in self._browser.targets(): if target.browserContext == self: targets.append(target) return targets async def pages(self) -> List[Page]: """Return list of all open pages. Non-visible pages, such as ``"background_page"``, will not be listed here. You can find them using :meth:`pyppeteer.target.Target.page`. """ # Using asyncio.gather is better for performance pages = [] for target in self.targets(): if target.type == 'page': page = await target.page() if page: pages.append(page) return pages def isIncognite(self) -> bool: """[Deprecated] Miss spelled method. Use :meth:`isIncognito` method instead. """ logger.warning( 'isIncognite is deprecated. ' 'Use isIncognito instead.' ) return self.isIncognito() def isIncognito(self) -> bool: """Return whether BrowserContext is incognito. The default browser context is the only non-incognito browser context. .. note:: The default browser context cannot be closed. """ return bool(self._id) async def newPage(self) -> Page: """Create a new page in the browser context.""" return await self._browser._createPageInContext(self._id) @property def browser(self) -> Browser: """Return the browser this browser context belongs to.""" return self._browser async def close(self) -> None: """Close the browser context. All the targets that belongs to the browser context will be closed. .. note:: Only incognito browser context can be closed. """ if self._id is None: raise BrowserError('Non-incognito profile cannot be closed') await self._browser._disposeContext(self._id) ================================================ FILE: pyppeteer/chromium_downloader.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Chromium download module.""" import logging import os import stat import sys from io import BytesIO from pathlib import Path from zipfile import ZipFile import certifi import urllib3 from pyppeteer import __chromium_revision__, __pyppeteer_home__ from tqdm import tqdm logger = logging.getLogger(__name__) # add our own stream handler - we want some output here handler = logging.StreamHandler() handler.setFormatter(fmt=logging.Formatter(fmt="[{levelname}] {msg}", style="{")) handler.setLevel(logging.INFO) logger.setLevel(logging.INFO) logger.addHandler(handler) DOWNLOADS_FOLDER = Path(__pyppeteer_home__) / 'local-chromium' DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com' DOWNLOAD_HOST = os.environ.get('PYPPETEER_DOWNLOAD_HOST', DEFAULT_DOWNLOAD_HOST) BASE_URL = f'{DOWNLOAD_HOST}/chromium-browser-snapshots' REVISION = os.environ.get('PYPPETEER_CHROMIUM_REVISION', __chromium_revision__) NO_PROGRESS_BAR = os.environ.get('PYPPETEER_NO_PROGRESS_BAR', '') if NO_PROGRESS_BAR.lower() in ('1', 'true'): NO_PROGRESS_BAR = True # type: ignore windowsArchive = 'chrome-win' downloadURLs = { 'linux': f'{BASE_URL}/Linux_x64/{REVISION}/chrome-linux.zip', 'mac': f'{BASE_URL}/Mac/{REVISION}/chrome-mac.zip', 'win32': f'{BASE_URL}/Win/{REVISION}/{windowsArchive}.zip', 'win64': f'{BASE_URL}/Win_x64/{REVISION}/{windowsArchive}.zip', } chromiumExecutable = { 'linux': DOWNLOADS_FOLDER / REVISION / 'chrome-linux' / 'chrome', 'mac': (DOWNLOADS_FOLDER / REVISION / 'chrome-mac' / 'Chromium.app' / 'Contents' / 'MacOS' / 'Chromium'), 'win32': DOWNLOADS_FOLDER / REVISION / windowsArchive / 'chrome.exe', 'win64': DOWNLOADS_FOLDER / REVISION / windowsArchive / 'chrome.exe', } def current_platform() -> str: """Get current platform name by short string.""" if sys.platform.startswith('linux'): return 'linux' elif sys.platform.startswith('darwin'): return 'mac' elif sys.platform.startswith('win') or sys.platform.startswith('msys') or sys.platform.startswith('cyg'): if sys.maxsize > 2 ** 31 - 1: return 'win64' return 'win32' raise OSError('Unsupported platform: ' + sys.platform) def get_url() -> str: """Get chromium download url.""" return downloadURLs[current_platform()] def download_zip(url: str) -> BytesIO: """Download data from url.""" logger.info('Starting Chromium download.') with urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) as http: # Get data from url. # set preload_content=False means using stream later. r = http.request('GET', url, preload_content=False) if r.status >= 400: raise OSError(f'Chromium downloadable not found at {url}: ' f'Received {r.data.decode()}.\n') # 10 * 1024 _data = BytesIO() if NO_PROGRESS_BAR: for chunk in r.stream(10240): _data.write(chunk) else: try: total_length = int(r.headers['content-length']) except (KeyError, ValueError, AttributeError): total_length = 0 process_bar = tqdm(total=total_length, unit_scale=True, unit='b') for chunk in r.stream(10240): _data.write(chunk) process_bar.update(len(chunk)) process_bar.close() return _data def extract_zip(data: BytesIO, path: Path) -> None: """Extract zipped data to path.""" # On mac zipfile module cannot extract correctly, so use unzip instead. logger.info('Beginning extraction') if current_platform() == 'mac': import subprocess import shutil zip_path = path / 'chrome.zip' if not path.exists(): path.mkdir(parents=True) with zip_path.open('wb') as f: f.write(data.getvalue()) if not shutil.which('unzip'): raise OSError('Failed to automatically extract chromium.' f'Please unzip {zip_path} manually.') proc = subprocess.run( ['unzip', str(zip_path)], cwd=str(path), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) if proc.returncode != 0: logger.error(proc.stdout.decode()) raise OSError(f'Failed to unzip {zip_path}.') if chromium_executable().exists() and zip_path.exists(): zip_path.unlink() else: with ZipFile(data) as zf: zf.extractall(str(path)) exec_path = chromium_executable() if not exec_path.exists(): raise IOError('Failed to extract chromium.') exec_path.chmod(exec_path.stat().st_mode | stat.S_IXOTH | stat.S_IXGRP | stat.S_IXUSR) logger.info(f'Chromium extracted to: {path}') def download_chromium() -> None: """Download and extract chromium.""" extract_zip(download_zip(get_url()), DOWNLOADS_FOLDER / REVISION) def chromium_executable() -> Path: """Get path of the chromium executable.""" return chromiumExecutable[current_platform()] def check_chromium() -> bool: """Check if chromium is placed at correct path.""" return chromium_executable().exists() ================================================ FILE: pyppeteer/command.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Commands for Pyppeteer.""" import logging from pyppeteer.chromium_downloader import check_chromium, download_chromium def install() -> None: """Download chromium if not install.""" if not check_chromium(): download_chromium() else: logging.getLogger(__name__).warning('chromium is already installed.') ================================================ FILE: pyppeteer/connection.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Connection/Session management module.""" import asyncio import json import logging from typing import Awaitable, Callable, Dict, Union, TYPE_CHECKING from pyee import EventEmitter import websockets from websockets.legacy.client import connect as ws_connect from pyppeteer.errors import NetworkError if TYPE_CHECKING: from typing import Optional # noqa: F401 logger = logging.getLogger(__name__) logger_connection = logging.getLogger(__name__ + '.Connection') logger_session = logging.getLogger(__name__ + '.CDPSession') class Connection(EventEmitter): """Connection management class.""" def __init__(self, url: str, loop: asyncio.AbstractEventLoop, delay: int = 0) -> None: """Make connection. :arg str url: WebSocket url to connect devtool. :arg int delay: delay to wait before processing received messages. """ super().__init__() self._url = url self._lastId = 0 self._callbacks: Dict[int, asyncio.Future] = dict() self._delay = delay / 1000 self._loop = loop self._sessions: Dict[str, CDPSession] = dict() self.connection: CDPSession self._connected = False self._ws = ws_connect(self._url, max_size=None, loop=self._loop, ping_interval=None, ping_timeout=None) self._recv_fut = self._loop.create_task(self._recv_loop()) self._closeCallback: Optional[Callable[[], None]] = None @property def url(self) -> str: """Get connected WebSocket url.""" return self._url async def _recv_loop(self) -> None: async with self._ws as connection: self._connected = True self.connection = connection while self._connected: try: resp = await self.connection.recv() if resp: await self._on_message(resp) except (websockets.ConnectionClosed, ConnectionResetError): logger.info('connection closed') break await asyncio.sleep(0) if self._connected: self._loop.create_task(self.dispose()) async def _async_send(self, msg: str, callback_id: int) -> None: while not self._connected: await asyncio.sleep(self._delay) try: await self.connection.send(msg) except websockets.ConnectionClosed: logger.error('connection unexpectedly closed') callback = self._callbacks.get(callback_id, None) if callback and not callback.done(): callback.set_result(None) await self.dispose() def send(self, method: str, params: dict = None) -> Awaitable: """Send message via the connection.""" # Detect connection availability from the second transmission if self._lastId and not self._connected: raise ConnectionError('Connection is closed') if params is None: params = dict() self._lastId += 1 _id = self._lastId msg = json.dumps(dict( id=_id, method=method, params=params, )) logger_connection.debug(f'SEND: {msg}') self._loop.create_task(self._async_send(msg, _id)) callback = self._loop.create_future() self._callbacks[_id] = callback callback.error: Exception = NetworkError() # type: ignore callback.method: str = method # type: ignore return callback def _on_response(self, msg: dict) -> None: callback = self._callbacks.pop(msg.get('id', -1)) if msg.get('error'): callback.set_exception( _createProtocolError( callback.error, # type: ignore callback.method, # type: ignore msg ) ) else: callback.set_result(msg.get('result')) def _on_query(self, msg: dict) -> None: params = msg.get('params', {}) method = msg.get('method', '') sessionId = params.get('sessionId') if method == 'Target.receivedMessageFromTarget': session = self._sessions.get(sessionId) if session: session._on_message(params.get('message')) elif method == 'Target.detachedFromTarget': session = self._sessions.get(sessionId) if session: session._on_closed() del self._sessions[sessionId] else: self.emit(method, params) def setClosedCallback(self, callback: Callable[[], None]) -> None: """Set closed callback.""" self._closeCallback = callback async def _on_message(self, message: str) -> None: await asyncio.sleep(self._delay) logger_connection.debug(f'RECV: {message}') msg = json.loads(message) if msg.get('id') in self._callbacks: self._on_response(msg) else: self._on_query(msg) async def _on_close(self) -> None: if self._closeCallback: self._closeCallback() self._closeCallback = None for cb in self._callbacks.values(): cb.set_exception(_rewriteError( cb.error, # type: ignore f'Protocol error {cb.method}: Target closed.', # type: ignore )) self._callbacks.clear() for session in self._sessions.values(): session._on_closed() self._sessions.clear() # close connection if hasattr(self, 'connection'): # may not have connection await self.connection.close() if not self._recv_fut.done(): self._recv_fut.cancel() async def dispose(self) -> None: """Close all connection.""" self._connected = False await self._on_close() async def createSession(self, targetInfo: Dict) -> 'CDPSession': """Create new session.""" resp = await self.send( 'Target.attachToTarget', {'targetId': targetInfo['targetId']} ) sessionId = resp.get('sessionId') session = CDPSession(self, targetInfo['type'], sessionId, self._loop) self._sessions[sessionId] = session return session class CDPSession(EventEmitter): """Chrome Devtools Protocol Session. The :class:`CDPSession` instances are used to talk raw Chrome Devtools Protocol: * protocol methods can be called with :meth:`send` method. * protocol events can be subscribed to with :meth:`on` method. Documentation on DevTools Protocol can be found `here `__. """ def __init__(self, connection: Union[Connection, 'CDPSession'], targetType: str, sessionId: str, loop: asyncio.AbstractEventLoop) -> None: """Make new session.""" super().__init__() self._lastId = 0 self._callbacks: Dict[int, asyncio.Future] = {} self._connection: Optional[Connection] = connection self._targetType = targetType self._sessionId = sessionId self._sessions: Dict[str, CDPSession] = dict() self._loop = loop def send(self, method: str, params: dict = None) -> Awaitable: """Send message to the connected session. :arg str method: Protocol method name. :arg dict params: Optional method parameters. """ if not self._connection: raise NetworkError( f'Protocol Error ({method}): Session closed. Most likely the ' f'{self._targetType} has been closed.' ) self._lastId += 1 _id = self._lastId msg = json.dumps(dict(id=_id, method=method, params=params)) logger_session.debug(f'SEND: {msg}') callback = self._loop.create_future() self._callbacks[_id] = callback callback.error: Exception = NetworkError() # type: ignore callback.method: str = method # type: ignore try: self._connection.send('Target.sendMessageToTarget', { 'sessionId': self._sessionId, 'message': msg, }) except Exception as e: # The response from target might have been already dispatched if _id in self._callbacks: _callback = self._callbacks[_id] del self._callbacks[_id] _callback.set_exception(_rewriteError( _callback.error, # type: ignore e.args[0], )) return callback def _on_message(self, msg: str) -> None: # noqa: C901 logger_session.debug(f'RECV: {msg}') obj = json.loads(msg) _id = obj.get('id') if _id: callback = self._callbacks.get(_id) if callback: del self._callbacks[_id] if obj.get('error'): callback.set_exception(_createProtocolError( callback.error, # type: ignore callback.method, # type: ignore obj, )) else: result = obj.get('result') if callback and not callback.done(): callback.set_result(result) else: params = obj.get('params', {}) if obj.get('method') == 'Target.receivedMessageFromTarget': session = self._sessions.get(params.get('sessionId')) if session: session._on_message(params.get('message')) elif obj.get('method') == 'Target.detachFromTarget': sessionId = params.get('sessionId') session = self._sessions.get(sessionId) if session: session._on_closed() del self._sessions[sessionId] self.emit(obj.get('method'), obj.get('params')) async def detach(self) -> None: """Detach session from target. Once detached, session won't emit any events and can't be used to send messages. """ if not self._connection: raise NetworkError('Connection already closed.') await self._connection.send('Target.detachFromTarget', {'sessionId': self._sessionId}) def _on_closed(self) -> None: for cb in self._callbacks.values(): cb.set_exception(_rewriteError( cb.error, # type: ignore f'Protocol error {cb.method}: Target closed.', # type: ignore )) self._callbacks.clear() self._connection = None def _createSession(self, targetType: str, sessionId: str) -> 'CDPSession': session = CDPSession(self, targetType, sessionId, self._loop) self._sessions[sessionId] = session return session def _createProtocolError(error: Exception, method: str, obj: Dict ) -> Exception: message = f'Protocol error ({method}): {obj["error"]["message"]}' if 'data' in obj['error']: message += f' {obj["error"]["data"]}' return _rewriteError(error, message) def _rewriteError(error: Exception, message: str) -> Exception: error.args = (message, ) return error ================================================ FILE: pyppeteer/coverage.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Coverage module.""" from functools import cmp_to_key import logging from typing import Any, Dict, List from pyppeteer import helper from pyppeteer.connection import CDPSession from pyppeteer.errors import PageError from pyppeteer.execution_context import EVALUATION_SCRIPT_URL from pyppeteer.helper import debugError from pyppeteer.util import merge_dict logger = logging.getLogger(__name__) class Coverage(object): """Coverage class. Coverage gathers information about parts of JavaScript and CSS that were used by the page. An example of using JavaScript and CSS coverage to get percentage of initially executed code:: # Enable both JavaScript and CSS coverage await page.coverage.startJSCoverage() await page.coverage.startCSSCoverage() # Navigate to page await page.goto('https://example.com') # Disable JS and CSS coverage and get results jsCoverage = await page.coverage.stopJSCoverage() cssCoverage = await page.coverage.stopCSSCoverage() totalBytes = 0 usedBytes = 0 coverage = jsCoverage + cssCoverage for entry in coverage: totalBytes += len(entry['text']) for range in entry['ranges']: usedBytes += range['end'] - range['start'] - 1 print('Bytes used: {}%'.format(usedBytes / totalBytes * 100)) """ def __init__(self, client: CDPSession) -> None: self._jsCoverage = JSCoverage(client) self._cssCoverage = CSSCoverage(client) async def startJSCoverage(self, options: Dict = None, **kwargs: Any ) -> None: """Start JS coverage measurement. Available options are: * ``resetOnNavigation`` (bool): Whether to reset coverage on every navigation. Defaults to ``True``. * ``reportAnonymousScript`` (bool): Whether anonymous script generated by the page should be reported. Defaults to ``False``. .. note:: Anonymous scripts are ones that don't have an associated url. These are scripts that are dynamically created on the page using ``eval`` of ``new Function``. If ``reportAnonymousScript`` is set to ``True``, anonymous scripts will have ``__pyppeteer_evaluation_script__`` as their url. """ options = merge_dict(options, kwargs) await self._jsCoverage.start(options) async def stopJSCoverage(self) -> List: """Stop JS coverage measurement and get result. Return list of coverage reports for all scripts. Each report includes: * ``url`` (str): Script url. * ``text`` (str): Script content. * ``ranges`` (List[Dict]): Script ranges that were executed. Ranges are sorted and non-overlapping. * ``start`` (int): A start offset in text, inclusive. * ``end`` (int): An end offset in text, exclusive. .. note:: JavaScript coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. """ return await self._jsCoverage.stop() async def startCSSCoverage(self, options: Dict = None, **kwargs: Any ) -> None: """Start CSS coverage measurement. Available options are: * ``resetOnNavigation`` (bool): Whether to reset coverage on every navigation. Defaults to ``True``. """ options = merge_dict(options, kwargs) await self._cssCoverage.start(options) async def stopCSSCoverage(self) -> List: """Stop CSS coverage measurement and get result. Return list of coverage reports for all non-anonymous scripts. Each report includes: * ``url`` (str): StyleSheet url. * ``text`` (str): StyleSheet content. * ``ranges`` (List[Dict]): StyleSheet ranges that were executed. Ranges are sorted and non-overlapping. * ``start`` (int): A start offset in text, inclusive. * ``end`` (int): An end offset in text, exclusive. .. note:: CSS coverage doesn't include dynamically injected style tags without sourceURLs (but currently includes... to be fixed). """ return await self._cssCoverage.stop() class JSCoverage(object): """JavaScript Coverage class.""" def __init__(self, client: CDPSession) -> None: self._client = client self._enabled = False self._scriptURLs: Dict = dict() self._scriptSources: Dict = dict() self._eventListeners: List = list() self._resetOnNavigation = False async def start(self, options: Dict = None, **kwargs: Any) -> None: """Start coverage measurement.""" options = merge_dict(options, kwargs) if self._enabled: raise PageError('JSCoverage is always enabled.') self._resetOnNavigation = (True if 'resetOnNavigation' not in options else bool(options['resetOnNavigation'])) self._reportAnonymousScript = bool(options.get('reportAnonymousScript')) # noqa: E501 self._enabled = True self._scriptURLs.clear() self._scriptSources.clear() self._eventListeners = [ helper.addEventListener( self._client, 'Debugger.scriptParsed', lambda e: self._client._loop.create_task( self._onScriptParsed(e))), helper.addEventListener( self._client, 'Runtime.executionContextsCleared', self._onExecutionContextsCleared), ] await self._client.send('Profiler.enable') await self._client.send('Profiler.startPreciseCoverage', {'callCount': False, 'detailed': True}) await self._client.send('Debugger.enable') await self._client.send('Debugger.setSkipAllPauses', {'skip': True}) def _onExecutionContextsCleared(self, event: Dict) -> None: if not self._resetOnNavigation: return self._scriptURLs.clear() self._scriptSources.clear() async def _onScriptParsed(self, event: Dict) -> None: # Ignore pyppeteer-injected scripts if event.get('url') == EVALUATION_SCRIPT_URL: return # Ignore other anonymous scripts unless the reportAnonymousScript # option is True if not event.get('url') and not self._reportAnonymousScript: return scriptId = event.get('scriptId') url = event.get('url') if not url and self._reportAnonymousScript: url = f'debugger://VM{scriptId}' try: response = await self._client.send( 'Debugger.getScriptSource', {'scriptId': scriptId} ) self._scriptURLs[scriptId] = url self._scriptSources[scriptId] = response.get('scriptSource') except Exception as e: # This might happen if the page has already navigated away. debugError(logger, e) async def stop(self) -> List: """Stop coverage measurement and return results.""" if not self._enabled: raise PageError('JSCoverage is not enabled.') self._enabled = False result = await self._client.send('Profiler.takePreciseCoverage') await self._client.send('Profiler.stopPreciseCoverage') await self._client.send('Profiler.disable') await self._client.send('Debugger.disable') helper.removeEventListeners(self._eventListeners) coverage: List = [] for entry in result.get('result', []): url = self._scriptURLs.get(entry.get('scriptId')) text = self._scriptSources.get(entry.get('scriptId')) if text is None or url is None: continue flattenRanges: List = [] for func in entry.get('functions', []): flattenRanges.extend(func.get('ranges', [])) ranges = convertToDisjointRanges(flattenRanges) coverage.append({'url': url, 'ranges': ranges, 'text': text}) return coverage class CSSCoverage(object): """CSS Coverage class.""" def __init__(self, client: CDPSession) -> None: self._client = client self._enabled = False self._stylesheetURLs: Dict = dict() self._stylesheetSources: Dict = dict() self._eventListeners: List = [] self._resetOnNavigation = False async def start(self, options: Dict = None, **kwargs: Any) -> None: """Start coverage measurement.""" options = merge_dict(options, kwargs) if self._enabled: raise PageError('CSSCoverage is already enabled.') self._resetOnNavigation = (True if 'resetOnNavigation' not in options else bool(options['resetOnNavigation'])) self._enabled = True self._stylesheetURLs.clear() self._stylesheetSources.clear() self._eventListeners = [ helper.addEventListener( self._client, 'CSS.styleSheetAdded', lambda e: self._client._loop.create_task( self._onStyleSheet(e))), helper.addEventListener( self._client, 'Runtime.executionContextsCleared', self._onExecutionContextsCleared), ] await self._client.send('DOM.enable') await self._client.send('CSS.enable') await self._client.send('CSS.startRuleUsageTracking') def _onExecutionContextsCleared(self, event: Dict) -> None: if not self._resetOnNavigation: return self._stylesheetURLs.clear() self._stylesheetSources.clear() async def _onStyleSheet(self, event: Dict) -> None: header = event.get('header', {}) # Ignore anonymous scripts if not header.get('sourceURL'): return try: response = await self._client.send( 'CSS.getStyleSheetText', {'styleSheetId': header['styleSheetId']} ) self._stylesheetURLs[header['styleSheetId']] = header['sourceURL'] self._stylesheetSources[header['styleSheetId']] = response['text'] except Exception as e: # This might happen if the page has already navigated away. debugError(logger, e) async def stop(self) -> List: """Stop coverage measurement and return results.""" if not self._enabled: raise PageError('CSSCoverage is not enabled.') self._enabled = False result = await self._client.send('CSS.stopRuleUsageTracking') await self._client.send('CSS.disable') await self._client.send('DOM.disable') helper.removeEventListeners(self._eventListeners) # aggregate by styleSheetId styleSheetIdToCoverage: Dict = {} for entry in result['ruleUsage']: ranges = styleSheetIdToCoverage.get(entry['styleSheetId']) if not ranges: ranges = [] styleSheetIdToCoverage[entry['styleSheetId']] = ranges ranges.append({ 'startOffset': entry['startOffset'], 'endOffset': entry['endOffset'], 'count': 1 if entry['used'] else 0 }) coverage = [] for styleSheetId in self._stylesheetURLs: url = self._stylesheetURLs.get(styleSheetId) text = self._stylesheetSources.get(styleSheetId) ranges = convertToDisjointRanges( styleSheetIdToCoverage.get(styleSheetId, []) ) coverage.append({'url': url, 'ranges': ranges, 'text': text}) return coverage def convertToDisjointRanges(nestedRanges: List[Any] # noqa: C901 ) -> List[Any]: """Convert ranges.""" points: List = [] for nested_range in nestedRanges: points.append({'offset': nested_range['startOffset'], 'type': 0, 'range': nested_range}) points.append({'offset': nested_range['endOffset'], 'type': 1, 'range': nested_range}) # Sort points to form a valid parenthesis sequence. def _sort_func(a: Dict, b: Dict) -> int: # Sort with increasing offsets. if a['offset'] != b['offset']: return a['offset'] - b['offset'] # All "end" points should go before "start" points. if a['type'] != b['type']: return b['type'] - a['type'] aLength = a['range']['endOffset'] - a['range']['startOffset'] bLength = b['range']['endOffset'] - b['range']['startOffset'] # For two "start" points, the one with longer range goes first. if a['type'] == 0: return bLength - aLength # For two "end" points, the one with shorter range goes first. return aLength - bLength points.sort(key=cmp_to_key(_sort_func)) hitCountStack: List[int] = [] results: List[Dict] = [] lastOffset = 0 # Run scanning line to intersect all ranges. for point in points: if (hitCountStack and lastOffset < point['offset'] and hitCountStack[len(hitCountStack) - 1] > 0): lastResult = results[-1] if results else None if lastResult and lastResult['end'] == lastOffset: lastResult['end'] = point['offset'] else: results.append({'start': lastOffset, 'end': point['offset']}) lastOffset = point['offset'] if point['type'] == 0: hitCountStack.append(point['range']['count']) else: hitCountStack.pop() # Filter out empty ranges. return [range for range in results if range['end'] - range['start'] > 1] ================================================ FILE: pyppeteer/dialog.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Dialog module.""" from types import SimpleNamespace from pyppeteer.connection import CDPSession class Dialog(object): """Dialog class. Dialog objects are dispatched by page via the ``dialog`` event. An example of using ``Dialog`` class: .. code:: browser = await launch() page = await browser.newPage() async def close_dialog(dialog): print(dialog.message) await dialog.dismiss() await browser.close() page.on( 'dialog', lambda dialog: asyncio.ensure_future(close_dialog(dialog)) ) await page.evaluate('() => alert("1")') """ Type = SimpleNamespace( Alert='alert', BeforeUnload='beforeunload', Confirm='confirm', Prompt='prompt', ) def __init__(self, client: CDPSession, type: str, message: str, defaultValue: str = '') -> None: self._client = client self._type = type self._message = message self._handled = False self._defaultValue = defaultValue @property def type(self) -> str: """Get dialog type. One of ``alert``, ``beforeunload``, ``confirm``, or ``prompt``. """ return self._type @property def message(self) -> str: """Get dialog message.""" return self._message @property def defaultValue(self) -> str: """If dialog is prompt, get default prompt value. If dialog is not prompt, return empty string (``''``). """ return self._defaultValue async def accept(self, promptText: str = '') -> None: """Accept the dialog. * ``promptText`` (str): A text to enter in prompt. If the dialog's type is not prompt, this does not cause any effect. """ self._handled = True await self._client.send('Page.handleJavaScriptDialog', { 'accept': True, 'promptText': promptText, }) async def dismiss(self) -> None: """Dismiss the dialog.""" self._handled = True await self._client.send('Page.handleJavaScriptDialog', { 'accept': False, }) ================================================ FILE: pyppeteer/element_handle.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Element handle module.""" import copy import logging import math import os.path from typing import Any, Dict, List, Optional, TYPE_CHECKING from pyppeteer.connection import CDPSession from pyppeteer.execution_context import ExecutionContext, JSHandle from pyppeteer.errors import ElementHandleError, NetworkError from pyppeteer.helper import debugError from pyppeteer.util import merge_dict if TYPE_CHECKING: from pyppeteer.frame_manager import Frame, FrameManager # noqa: F401 logger = logging.getLogger(__name__) class ElementHandle(JSHandle): """ElementHandle class. This class represents an in-page DOM element. ElementHandle can be created by the :meth:`pyppeteer.page.Page.querySelector` method. ElementHandle prevents DOM element from garbage collection unless the handle is disposed. ElementHandles are automatically disposed when their origin frame gets navigated. ElementHandle isinstance can be used as arguments in :meth:`pyppeteer.page.Page.querySelectorEval` and :meth:`pyppeteer.page.Page.evaluate` methods. """ def __init__(self, context: ExecutionContext, client: CDPSession, remoteObject: dict, page: Any, frameManager: 'FrameManager') -> None: super().__init__(context, client, remoteObject) self._client = client self._remoteObject = remoteObject self._page = page self._frameManager = frameManager self._disposed = False def asElement(self) -> 'ElementHandle': """Return this ElementHandle.""" return self async def contentFrame(self) -> Optional['Frame']: """Return the content frame for the element handle. Return ``None`` if this handle is not referencing iframe. """ nodeInfo = await self._client.send('DOM.describeNode', { 'objectId': self._remoteObject.get('objectId'), }) node_obj = nodeInfo.get('node', {}) if not isinstance(node_obj.get('frameId'), str): return None return self._frameManager.frame(node_obj['frameId']) async def _scrollIntoViewIfNeeded(self) -> None: error = await self.executionContext.evaluate(''' async (element, pageJavascriptEnabled) => { if (!element.isConnected) return 'Node is detached from document'; if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement'; // force-scroll if page's javascript is disabled. if (!pageJavascriptEnabled) { element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant', }); return false; } const visibleRatio = await new Promise(resolve => { const observer = new IntersectionObserver(entries => { resolve(entries[0].intersectionRatio); observer.disconnect(); }); observer.observe(element); }); if (visibleRatio !== 1.0) element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant', }); return false; }''', self, self._page._javascriptEnabled) if error: raise ElementHandleError(error) async def _clickablePoint(self) -> Dict[str, float]: # noqa: C901 result = None try: result = await self._client.send('DOM.getContentQuads', { 'objectId': self._remoteObject.get('objectId'), }) except Exception as e: debugError(logger, e) if not result or not result.get('quads'): raise ElementHandleError( 'Node is either not visible or not an HTMLElement') quads = [] for _quad in result.get('quads'): _q = self._fromProtocolQuad(_quad) if _computeQuadArea(_q) > 1: quads.append(_q) if not quads: raise ElementHandleError( 'Node is either not visible or not an HTMLElement') quad = quads[0] x = 0 y = 0 for point in quad: x += point['x'] y += point['y'] return {'x': x / 4, 'y': y / 4} async def _getBoxModel(self) -> Optional[Dict]: try: result: Optional[Dict] = await self._client.send( 'DOM.getBoxModel', {'objectId': self._remoteObject.get('objectId')}, ) except NetworkError as e: debugError(logger, e) result = None return result def _fromProtocolQuad(self, quad: List[int]) -> List[Dict[str, int]]: return [ {'x': quad[0], 'y': quad[1]}, {'x': quad[2], 'y': quad[3]}, {'x': quad[4], 'y': quad[5]}, {'x': quad[6], 'y': quad[7]}, ] async def hover(self) -> None: """Move mouse over to center of this element. If needed, this method scrolls element into view. If this element is detached from DOM tree, the method raises an ``ElementHandleError``. """ await self._scrollIntoViewIfNeeded() obj = await self._clickablePoint() x = obj.get('x', 0) y = obj.get('y', 0) await self._page.mouse.move(x, y) async def click(self, options: dict = None, **kwargs: Any) -> None: """Click the center of this element. If needed, this method scrolls element into view. If the element is detached from DOM, the method raises ``ElementHandleError``. ``options`` can contain the following fields: * ``button`` (str): ``left``, ``right``, of ``middle``, defaults to ``left``. * ``clickCount`` (int): Defaults to 1. * ``delay`` (int|float): Time to wait between ``mousedown`` and ``mouseup`` in milliseconds. Defaults to 0. """ options = merge_dict(options, kwargs) await self._scrollIntoViewIfNeeded() obj = await self._clickablePoint() x = obj.get('x', 0) y = obj.get('y', 0) await self._page.mouse.click(x, y, options) async def uploadFile(self, *filePaths: str) -> dict: """Upload files.""" files = [os.path.abspath(p) for p in filePaths] objectId = self._remoteObject.get('objectId') return await self._client.send( 'DOM.setFileInputFiles', {'objectId': objectId, 'files': files} ) async def tap(self) -> None: """Tap the center of this element. If needed, this method scrolls element into view. If the element is detached from DOM, the method raises ``ElementHandleError``. """ await self._scrollIntoViewIfNeeded() center = await self._clickablePoint() x = center.get('x', 0) y = center.get('y', 0) await self._page.touchscreen.tap(x, y) async def focus(self) -> None: """Focus on this element.""" await self.executionContext.evaluate( 'element => element.focus()', self) async def type(self, text: str, options: Dict = None, **kwargs: Any ) -> None: """Focus the element and then type text. Details see :meth:`pyppeteer.input.Keyboard.type` method. """ options = merge_dict(options, kwargs) await self.focus() await self._page.keyboard.type(text, options) async def press(self, key: str, options: Dict = None, **kwargs: Any ) -> None: """Press ``key`` onto the element. This method focuses the element, and then uses :meth:`pyppeteer.input.keyboard.down` and :meth:`pyppeteer.input.keyboard.up`. :arg str key: Name of key to press, such as ``ArrowLeft``. This method accepts the following options: * ``text`` (str): If specified, generates an input event with this text. * ``delay`` (int|float): Time to wait between ``keydown`` and ``keyup``. Defaults to 0. """ options = merge_dict(options, kwargs) await self.focus() await self._page.keyboard.press(key, options) async def boundingBox(self) -> Optional[Dict[str, float]]: """Return bounding box of this element. If the element is not visible, return ``None``. This method returns dictionary of bounding box, which contains: * ``x`` (int): The X coordinate of the element in pixels. * ``y`` (int): The Y coordinate of the element in pixels. * ``width`` (int): The width of the element in pixels. * ``height`` (int): The height of the element in pixels. """ result = await self._getBoxModel() if not result: return None quad = result['model']['border'] x = min(quad[0], quad[2], quad[4], quad[6]) y = min(quad[1], quad[3], quad[5], quad[7]) width = max(quad[0], quad[2], quad[4], quad[6]) - x height = max(quad[1], quad[3], quad[5], quad[7]) - y return {'x': x, 'y': y, 'width': width, 'height': height} async def boxModel(self) -> Optional[Dict]: """Return boxes of element. Return ``None`` if element is not visible. Boxes are represented as an list of points; each Point is a dictionary ``{x, y}``. Box points are sorted clock-wise. Returned value is a dictionary with the following fields: * ``content`` (List[Dict]): Content box. * ``padding`` (List[Dict]): Padding box. * ``border`` (List[Dict]): Border box. * ``margin`` (List[Dict]): Margin box. * ``width`` (int): Element's width. * ``height`` (int): Element's height. """ result = await self._getBoxModel() if not result: return None model = result.get('model', {}) return { 'content': self._fromProtocolQuad(model.get('content')), 'padding': self._fromProtocolQuad(model.get('padding')), 'border': self._fromProtocolQuad(model.get('border')), 'margin': self._fromProtocolQuad(model.get('margin')), 'width': model.get('width'), 'height': model.get('height'), } async def screenshot(self, options: Dict = None, **kwargs: Any) -> bytes: """Take a screenshot of this element. If the element is detached from DOM, this method raises an ``ElementHandleError``. Available options are same as :meth:`pyppeteer.page.Page.screenshot`. """ options = merge_dict(options, kwargs) needsViewportReset = False boundingBox = await self.boundingBox() if not boundingBox: raise ElementHandleError( 'Node is either not visible or not an HTMLElement') original_viewport = copy.deepcopy(self._page.viewport) if (boundingBox['width'] > original_viewport['width'] or boundingBox['height'] > original_viewport['height']): newViewport = { 'width': max( original_viewport['width'], math.ceil(boundingBox['width']) ), 'height': max( original_viewport['height'], math.ceil(boundingBox['height']) ), } new_viewport = copy.deepcopy(original_viewport) new_viewport.update(newViewport) await self._page.setViewport(new_viewport) needsViewportReset = True await self._scrollIntoViewIfNeeded() boundingBox = await self.boundingBox() if not boundingBox: raise ElementHandleError( 'Node is either not visible or not an HTMLElement') _obj = await self._client.send('Page.getLayoutMetrics') pageX = _obj['layoutViewport']['pageX'] pageY = _obj['layoutViewport']['pageY'] clip = {} clip.update(boundingBox) clip['x'] = clip['x'] + pageX clip['y'] = clip['y'] + pageY opt = {'clip': clip} opt.update(options) imageData = await self._page.screenshot(opt) if needsViewportReset: await self._page.setViewport(original_viewport) return imageData async def querySelector(self, selector: str) -> Optional['ElementHandle']: """Return first element which matches ``selector`` under this element. If no element matches the ``selector``, returns ``None``. """ handle = await self.executionContext.evaluateHandle( '(element, selector) => element.querySelector(selector)', self, selector, ) element = handle.asElement() if element: return element await handle.dispose() return None async def querySelectorAll(self, selector: str) -> List['ElementHandle']: """Return all elements which match ``selector`` under this element. If no element matches the ``selector``, returns empty list (``[]``). """ arrayHandle = await self.executionContext.evaluateHandle( '(element, selector) => element.querySelectorAll(selector)', self, selector, ) properties = await arrayHandle.getProperties() await arrayHandle.dispose() result = [] for prop in properties.values(): elementHandle = prop.asElement() if elementHandle: result.append(elementHandle) return result # type: ignore async def querySelectorEval(self, selector: str, pageFunction: str, *args: Any) -> Any: """Run ``Page.querySelectorEval`` within the element. This method runs ``document.querySelector`` within the element and passes it as the first argument to ``pageFunction``. If there is no element matching ``selector``, the method raises ``ElementHandleError``. If ``pageFunction`` returns a promise, then wait for the promise to resolve and return its value. ``ElementHandle.Jeval`` is a shortcut of this method. Example: .. code:: python tweetHandle = await page.querySelector('.tweet') assert (await tweetHandle.querySelectorEval('.like', 'node => node.innerText')) == 100 assert (await tweetHandle.Jeval('.retweets', 'node => node.innerText')) == 10 """ # noqa: E501 elementHandle = await self.querySelector(selector) if not elementHandle: raise ElementHandleError( f'Error: failed to find element matching selector "{selector}"' ) result = await self.executionContext.evaluate( pageFunction, elementHandle, *args) await elementHandle.dispose() return result async def querySelectorAllEval(self, selector: str, pageFunction: str, *args: Any) -> Any: """Run ``Page.querySelectorAllEval`` within the element. This method runs ``Array.from(document.querySelectorAll)`` within the element and passes it as the first argument to ``pageFunction``. If there is no element matching ``selector``, the method raises ``ElementHandleError``. If ``pageFunction`` returns a promise, then wait for the promise to resolve and return its value. Example: .. code:: html
Hello!
Hi!
.. code:: python feedHandle = await page.J('.feed') assert (await feedHandle.JJeval('.tweet', '(nodes => nodes.map(n => n.innerText))')) == ['Hello!', 'Hi!'] """ # noqa: E501 arrayHandle = await self.executionContext.evaluateHandle( '(element, selector) => Array.from(element.querySelectorAll(selector))', # noqa: E501 self, selector ) result = await self.executionContext.evaluate( pageFunction, arrayHandle, *args) await arrayHandle.dispose() return result #: alias to :meth:`querySelector` J = querySelector #: alias to :meth:`querySelectorAll` JJ = querySelectorAll #: alias to :meth:`querySelectorEval` Jeval = querySelectorEval #: alias to :meth:`querySelectorAllEval` JJeval = querySelectorAllEval async def xpath(self, expression: str) -> List['ElementHandle']: """Evaluate the XPath expression relative to this elementHandle. If there are no such elements, return an empty list. :arg str expression: XPath string to be evaluated. """ arrayHandle = await self.executionContext.evaluateHandle( '''(element, expression) => { const document = element.ownerDocument || element; const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); const array = []; let item; while ((item = iterator.iterateNext())) array.push(item); return array; }''', self, expression) properties = await arrayHandle.getProperties() await arrayHandle.dispose() result = [] for property in properties.values(): elementHandle = property.asElement() if elementHandle: result.append(elementHandle) return result #: alias to :meth:`xpath` Jx = xpath async def isIntersectingViewport(self) -> bool: """Return ``True`` if the element is visible in the viewport.""" return await self.executionContext.evaluate('''async element => { const visibleRatio = await new Promise(resolve => { const observer = new IntersectionObserver(entries => { resolve(entries[0].intersectionRatio); observer.disconnect(); }); observer.observe(element); }); return visibleRatio > 0; }''', self) def _computeQuadArea(quad: List[Dict]) -> float: area = 0 for i, _ in enumerate(quad): p1 = quad[i] p2 = quad[(i + 1) % len(quad)] area += (p1['x'] * p2['y'] - p2['x'] * p1['y']) / 2 return area ================================================ FILE: pyppeteer/emulation_manager.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Emulation Manager module.""" from pyppeteer import helper from pyppeteer.connection import CDPSession class EmulationManager(object): """EmulationManager class.""" def __init__(self, client: CDPSession) -> None: """Make new emulation manager.""" self._client = client self._emulatingMobile = False self._hasTouch = False async def emulateViewport(self, viewport: dict) -> bool: """Evaluate viewport.""" options = dict() mobile = viewport.get('isMobile', False) options['mobile'] = mobile if 'width' in viewport: options['width'] = helper.get_positive_int(viewport, 'width') if 'height' in viewport: options['height'] = helper.get_positive_int(viewport, 'height') options['deviceScaleFactor'] = viewport.get('deviceScaleFactor', 1) if viewport.get('isLandscape'): options['screenOrientation'] = {'angle': 90, 'type': 'landscapePrimary'} else: options['screenOrientation'] = {'angle': 0, 'type': 'portraitPrimary'} hasTouch = viewport.get('hasTouch', False) await self._client.send('Emulation.setDeviceMetricsOverride', options) await self._client.send('Emulation.setTouchEmulationEnabled', { 'enabled': hasTouch, 'configuration': 'mobile' if mobile else 'desktop' }) reloadNeeded = (self._emulatingMobile != mobile or self._hasTouch != hasTouch) self._emulatingMobile = mobile self._hasTouch = hasTouch return reloadNeeded ================================================ FILE: pyppeteer/errors.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Exceptions for pyppeteer package.""" import asyncio class PyppeteerError(Exception): # noqa: D204 """Base exception for pyppeteer.""" pass class BrowserError(PyppeteerError): # noqa: D204 """Exception raised from browser.""" pass class ElementHandleError(PyppeteerError): # noqa: D204 """ElementHandle related exception.""" pass class NetworkError(PyppeteerError): # noqa: D204 """Network/Protocol related exception.""" pass class PageError(PyppeteerError): # noqa: D204 """Page/Frame related exception.""" pass class TimeoutError(asyncio.TimeoutError): # noqa: D204 """Timeout Error class.""" pass ================================================ FILE: pyppeteer/execution_context.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Execution Context Module.""" import logging import math import re from typing import Any, Dict, Optional, TYPE_CHECKING from pyppeteer import helper from pyppeteer.connection import CDPSession from pyppeteer.errors import ElementHandleError, NetworkError from pyppeteer.helper import debugError if TYPE_CHECKING: from pyppeteer.element_handle import ElementHandle # noqa: F401 from pyppeteer.frame_manager import Frame # noqa: F401 logger = logging.getLogger(__name__) EVALUATION_SCRIPT_URL = '__pyppeteer_evaluation_script__' SOURCE_URL_REGEX = re.compile( r'^[\040\t]*//[@#] sourceURL=\s*(\S*?)\s*$', re.MULTILINE, ) class ExecutionContext(object): """Execution Context class.""" def __init__(self, client: CDPSession, contextPayload: Dict, objectHandleFactory: Any, frame: 'Frame' = None) -> None: self._client = client self._frame = frame self._contextId = contextPayload.get('id') auxData = contextPayload.get('auxData', {'isDefault': False}) self._isDefault = bool(auxData.get('isDefault')) self._objectHandleFactory = objectHandleFactory @property def frame(self) -> Optional['Frame']: """Return frame associated with this execution context.""" return self._frame async def evaluate(self, pageFunction: str, *args: Any, force_expr: bool = False) -> Any: """Execute ``pageFunction`` on this context. Details see :meth:`pyppeteer.page.Page.evaluate`. """ handle = await self.evaluateHandle( pageFunction, *args, force_expr=force_expr) try: result = await handle.jsonValue() except NetworkError as e: if 'Object reference chain is too long' in e.args[0]: return if 'Object couldn\'t be returned by value' in e.args[0]: return raise await handle.dispose() return result async def evaluateHandle(self, pageFunction: str, *args: Any, # noqa: C901 force_expr: bool = False) -> 'JSHandle': """Execute ``pageFunction`` on this context. Details see :meth:`pyppeteer.page.Page.evaluateHandle`. """ suffix = f'//# sourceURL={EVALUATION_SCRIPT_URL}' if force_expr or (not args and not helper.is_jsfunc(pageFunction)): try: if SOURCE_URL_REGEX.match(pageFunction): expressionWithSourceUrl = pageFunction else: expressionWithSourceUrl = f'{pageFunction}\n{suffix}' _obj = await self._client.send('Runtime.evaluate', { 'expression': expressionWithSourceUrl, 'contextId': self._contextId, 'returnByValue': False, 'awaitPromise': True, 'userGesture': True, }) except Exception as e: _rewriteError(e) exceptionDetails = _obj.get('exceptionDetails') if exceptionDetails: raise ElementHandleError( 'Evaluation failed: {}'.format( helper.getExceptionMessage(exceptionDetails))) remoteObject = _obj.get('result') return self._objectHandleFactory(remoteObject) try: _obj = await self._client.send('Runtime.callFunctionOn', { 'functionDeclaration': f'{pageFunction}\n{suffix}\n', 'executionContextId': self._contextId, 'arguments': [self._convertArgument(arg) for arg in args], 'returnByValue': False, 'awaitPromise': True, 'userGesture': True, }) except Exception as e: _rewriteError(e) exceptionDetails = _obj.get('exceptionDetails') if exceptionDetails: raise ElementHandleError('Evaluation failed: {}'.format( helper.getExceptionMessage(exceptionDetails))) remoteObject = _obj.get('result') return self._objectHandleFactory(remoteObject) def _convertArgument(self, arg: Any) -> Dict: # noqa: C901 if arg == math.inf: return {'unserializableValue': 'Infinity'} if arg == -math.inf: return {'unserializableValue': '-Infinity'} objectHandle = arg if isinstance(arg, JSHandle) else None if objectHandle: if objectHandle._context != self: raise ElementHandleError('JSHandles can be evaluated only in the context they were created!') # noqa: E501 if objectHandle._disposed: raise ElementHandleError('JSHandle is disposed!') if objectHandle._remoteObject.get('unserializableValue'): return {'unserializableValue': objectHandle._remoteObject.get('unserializableValue')} # noqa: E501 if not objectHandle._remoteObject.get('objectId'): return {'value': objectHandle._remoteObject.get('value')} return {'objectId': objectHandle._remoteObject.get('objectId')} return {'value': arg} async def queryObjects(self, prototypeHandle: 'JSHandle') -> 'JSHandle': """Send query. Details see :meth:`pyppeteer.page.Page.queryObjects`. """ if prototypeHandle._disposed: raise ElementHandleError('Prototype JSHandle is disposed!') if not prototypeHandle._remoteObject.get('objectId'): raise ElementHandleError( 'Prototype JSHandle must not be referencing primitive value') response = await self._client.send('Runtime.queryObjects', { 'prototypeObjectId': prototypeHandle._remoteObject['objectId'], }) return self._objectHandleFactory(response.get('objects')) class JSHandle(object): """JSHandle class. JSHandle represents an in-page JavaScript object. JSHandle can be created with the :meth:`~pyppeteer.page.Page.evaluateHandle` method. """ def __init__(self, context: ExecutionContext, client: CDPSession, remoteObject: Dict) -> None: self._context = context self._client = client self._remoteObject = remoteObject self._disposed = False @property def executionContext(self) -> ExecutionContext: """Get execution context of this handle.""" return self._context async def getProperty(self, propertyName: str) -> 'JSHandle': """Get property value of ``propertyName``.""" objectHandle = await self._context.evaluateHandle( '''(object, propertyName) => { const result = {__proto__: null}; result[propertyName] = object[propertyName]; return result; }''', self, propertyName) properties = await objectHandle.getProperties() result = properties[propertyName] await objectHandle.dispose() return result async def getProperties(self) -> Dict[str, 'JSHandle']: """Get all properties of this handle.""" response = await self._client.send('Runtime.getProperties', { 'objectId': self._remoteObject.get('objectId', ''), 'ownProperties': True, }) result = dict() for prop in response['result']: if not prop.get('enumerable'): continue result[prop.get('name')] = self._context._objectHandleFactory( prop.get('value')) return result async def jsonValue(self) -> Dict: """Get Jsonized value of this object.""" objectId = self._remoteObject.get('objectId') if objectId: response = await self._client.send('Runtime.callFunctionOn', { 'functionDeclaration': 'function() { return this; }', 'objectId': objectId, 'returnByValue': True, 'awaitPromise': True, }) return helper.valueFromRemoteObject(response['result']) return helper.valueFromRemoteObject(self._remoteObject) def asElement(self) -> Optional['ElementHandle']: """Return either null or the object handle itself.""" return None async def dispose(self) -> None: """Stop referencing the handle.""" if self._disposed: return self._disposed = True try: await helper.releaseObject(self._client, self._remoteObject) except Exception as e: debugError(logger, e) def toString(self) -> str: """Get string representation.""" if self._remoteObject.get('objectId'): _type = (self._remoteObject.get('subtype') or self._remoteObject.get('type')) return f'JSHandle@{_type}' return 'JSHandle:{}'.format( helper.valueFromRemoteObject(self._remoteObject)) def _rewriteError(error: Exception) -> None: if error.args[0].endswith('Cannot find context with specified id'): msg = 'Execution context was destroyed, most likely because of a navigation.' # noqa: E501 raise type(error)(msg) raise error ================================================ FILE: pyppeteer/frame_manager.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Frame Manager module.""" import asyncio from collections import OrderedDict import logging from types import SimpleNamespace from typing import Any, Awaitable, Dict, Generator, List, Optional, Set, Union from pyee import EventEmitter from pyppeteer import helper from pyppeteer.connection import CDPSession from pyppeteer.element_handle import ElementHandle from pyppeteer.errors import NetworkError from pyppeteer.execution_context import ExecutionContext, JSHandle from pyppeteer.errors import ElementHandleError, PageError, TimeoutError from pyppeteer.util import merge_dict logger = logging.getLogger(__name__) class FrameManager(EventEmitter): """FrameManager class.""" Events = SimpleNamespace( FrameAttached='frameattached', FrameNavigated='framenavigated', FrameDetached='framedetached', LifecycleEvent='lifecycleevent', FrameNavigatedWithinDocument='framenavigatedwithindocument', ) def __init__(self, client: CDPSession, frameTree: Dict, page: Any) -> None: """Make new frame manager.""" super().__init__() self._client = client self._page = page self._frames: OrderedDict[str, Frame] = OrderedDict() self._mainFrame: Optional[Frame] = None self._contextIdToContext: Dict[str, ExecutionContext] = dict() client.on('Page.frameAttached', lambda event: self._onFrameAttached( event.get('frameId', ''), event.get('parentFrameId', '')) ) client.on('Page.frameNavigated', lambda event: self._onFrameNavigated(event.get('frame'))) client.on('Page.navigatedWithinDocument', lambda event: self._onFrameNavigatedWithinDocument( event.get('frameId'), event.get('url') )) client.on('Page.frameDetached', lambda event: self._onFrameDetached(event.get('frameId'))) client.on('Page.frameStoppedLoading', lambda event: self._onFrameStoppedLoading( event.get('frameId') )) client.on('Runtime.executionContextCreated', lambda event: self._onExecutionContextCreated( event.get('context'))) client.on('Runtime.executionContextDestroyed', lambda event: self._onExecutionContextDestroyed( event.get('executionContextId'))) client.on('Runtime.executionContextsCleared', lambda event: self._onExecutionContextsCleared()) client.on('Page.lifecycleEvent', lambda event: self._onLifecycleEvent(event)) self._handleFrameTree(frameTree) def _onLifecycleEvent(self, event: Dict) -> None: frame = self._frames.get(event['frameId']) if not frame: return frame._onLifecycleEvent(event['loaderId'], event['name']) self.emit(FrameManager.Events.LifecycleEvent, frame) def _onFrameStoppedLoading(self, frameId: str) -> None: frame = self._frames.get(frameId) if not frame: return frame._onLoadingStopped() self.emit(FrameManager.Events.LifecycleEvent, frame) def _handleFrameTree(self, frameTree: Dict) -> None: frame = frameTree['frame'] if 'parentId' in frame: self._onFrameAttached( frame['id'], frame['parentId'], ) self._onFrameNavigated(frame) if 'childFrames' not in frameTree: return for child in frameTree['childFrames']: self._handleFrameTree(child) @property def mainFrame(self) -> Optional['Frame']: """Return main frame.""" return self._mainFrame def frames(self) -> List['Frame']: """Return all frames.""" return list(self._frames.values()) def frame(self, frameId: str) -> Optional['Frame']: """Return :class:`Frame` of ``frameId``.""" return self._frames.get(frameId) def _onFrameAttached(self, frameId: str, parentFrameId: str) -> None: if frameId in self._frames: return parentFrame = self._frames.get(parentFrameId) frame = Frame(self._client, parentFrame, frameId) self._frames[frameId] = frame self.emit(FrameManager.Events.FrameAttached, frame) def _onFrameNavigated(self, framePayload: dict) -> None: isMainFrame = not framePayload.get('parentId') if isMainFrame: frame = self._mainFrame else: frame = self._frames.get(framePayload.get('id', '')) if not (isMainFrame or frame): raise PageError('We either navigate top level or have old version ' 'of the navigated frame') # Detach all child frames first. if frame: for child in frame.childFrames: self._removeFramesRecursively(child) # Update or create main frame. _id = framePayload.get('id', '') if isMainFrame: if frame: # Update frame id to retain frame identity on cross-process navigation. # noqa: E501 self._frames.pop(frame._id, None) frame._id = _id else: # Initial main frame navigation. frame = Frame(self._client, None, _id) self._frames[_id] = frame self._mainFrame = frame # Update frame payload. frame._navigated(framePayload) # type: ignore self.emit(FrameManager.Events.FrameNavigated, frame) def _onFrameNavigatedWithinDocument(self, frameId: str, url: str) -> None: frame = self._frames.get(frameId) if not frame: return frame._navigatedWithinDocument(url) self.emit(FrameManager.Events.FrameNavigatedWithinDocument, frame) self.emit(FrameManager.Events.FrameNavigated, frame) def _onFrameDetached(self, frameId: str) -> None: frame = self._frames.get(frameId) if frame: self._removeFramesRecursively(frame) def _onExecutionContextCreated(self, contextPayload: Dict) -> None: if (contextPayload.get('auxData') and contextPayload['auxData'].get('frameId')): frameId = contextPayload['auxData']['frameId'] else: frameId = None frame = self._frames.get(frameId) def _createJSHandle(obj: Dict) -> JSHandle: context = self.executionContextById(contextPayload['id']) return self.createJSHandle(context, obj) context = ExecutionContext( self._client, contextPayload, _createJSHandle, frame, ) self._contextIdToContext[contextPayload['id']] = context if frame: frame._addExecutionContext(context) def _onExecutionContextDestroyed(self, executionContextId: str) -> None: context = self._contextIdToContext.get(executionContextId) if not context: return del self._contextIdToContext[executionContextId] frame = context.frame if frame: frame._removeExecutionContext(context) def _onExecutionContextsCleared(self) -> None: for context in self._contextIdToContext.values(): frame = context.frame if frame: frame._removeExecutionContext(context) self._contextIdToContext.clear() def executionContextById(self, contextId: str) -> ExecutionContext: """Get stored ``ExecutionContext`` by ``id``.""" context = self._contextIdToContext.get(contextId) if not context: raise ElementHandleError( f'INTERNAL ERROR: missing context with id = {contextId}' ) return context def createJSHandle(self, context: ExecutionContext, remoteObject: Dict = None) -> JSHandle: """Create JS handle associated to the context id and remote object.""" if remoteObject is None: remoteObject = dict() if remoteObject.get('subtype') == 'node': return ElementHandle(context, self._client, remoteObject, self._page, self) return JSHandle(context, self._client, remoteObject) def _removeFramesRecursively(self, frame: 'Frame') -> None: for child in frame.childFrames: self._removeFramesRecursively(child) frame._detach() self._frames.pop(frame._id, None) self.emit(FrameManager.Events.FrameDetached, frame) class Frame(object): """Frame class. Frame objects can be obtained via :attr:`pyppeteer.page.Page.mainFrame`. """ def __init__(self, client: CDPSession, parentFrame: Optional['Frame'], frameId: str) -> None: self._client = client self._parentFrame = parentFrame self._url = '' self._detached = False self._id = frameId self._documentPromise: Optional[ElementHandle] = None self._contextResolveCallback = lambda _: None self._setDefaultContext(None) self._waitTasks: Set[WaitTask] = set() # maybe list self._loaderId = '' self._lifecycleEvents: Set[str] = set() self._childFrames: Set[Frame] = set() # maybe list if self._parentFrame: self._parentFrame._childFrames.add(self) def _addExecutionContext(self, context: ExecutionContext) -> None: if context._isDefault: self._setDefaultContext(context) def _removeExecutionContext(self, context: ExecutionContext) -> None: if context._isDefault: self._setDefaultContext(None) def _setDefaultContext(self, context: Optional[ExecutionContext]) -> None: if context is not None: self._contextResolveCallback(context) # type: ignore self._contextResolveCallback = lambda _: None for waitTask in self._waitTasks: self._client._loop.create_task(waitTask.rerun()) else: self._documentPromise = None self._contextPromise = self._client._loop.create_future() self._contextResolveCallback = ( lambda _context: self._contextPromise.set_result(_context) ) async def executionContext(self) -> Optional[ExecutionContext]: """Return execution context of this frame. Return :class:`~pyppeteer.execution_context.ExecutionContext` associated to this frame. """ return await self._contextPromise async def evaluateHandle(self, pageFunction: str, *args: Any) -> JSHandle: """Execute function on this frame. Details see :meth:`pyppeteer.page.Page.evaluateHandle`. """ context = await self.executionContext() if context is None: raise PageError('this frame has no context.') return await context.evaluateHandle(pageFunction, *args) async def evaluate(self, pageFunction: str, *args: Any, force_expr: bool = False) -> Any: """Evaluate pageFunction on this frame. Details see :meth:`pyppeteer.page.Page.evaluate`. """ context = await self.executionContext() if context is None: raise ElementHandleError('ExecutionContext is None.') return await context.evaluate( pageFunction, *args, force_expr=force_expr) async def querySelector(self, selector: str) -> Optional[ElementHandle]: """Get element which matches `selector` string. Details see :meth:`pyppeteer.page.Page.querySelector`. """ document = await self._document() value = await document.querySelector(selector) return value async def _document(self) -> ElementHandle: if self._documentPromise: return self._documentPromise context = await self.executionContext() if context is None: raise PageError('No context exists.') document = (await context.evaluateHandle('document')).asElement() self._documentPromise = document if document is None: raise PageError('Could not find `document`.') return document async def xpath(self, expression: str) -> List[ElementHandle]: """Evaluate the XPath expression. If there are no such elements in this frame, return an empty list. :arg str expression: XPath string to be evaluated. """ document = await self._document() value = await document.xpath(expression) return value async def querySelectorEval(self, selector: str, pageFunction: str, *args: Any) -> Any: """Execute function on element which matches selector. Details see :meth:`pyppeteer.page.Page.querySelectorEval`. """ document = await self._document() return await document.querySelectorEval(selector, pageFunction, *args) async def querySelectorAllEval(self, selector: str, pageFunction: str, *args: Any) -> Optional[Dict]: """Execute function on all elements which matches selector. Details see :meth:`pyppeteer.page.Page.querySelectorAllEval`. """ document = await self._document() value = await document.JJeval(selector, pageFunction, *args) return value async def querySelectorAll(self, selector: str) -> List[ElementHandle]: """Get all elements which matches `selector`. Details see :meth:`pyppeteer.page.Page.querySelectorAll`. """ document = await self._document() value = await document.querySelectorAll(selector) return value #: Alias to :meth:`querySelector` J = querySelector #: Alias to :meth:`xpath` Jx = xpath #: Alias to :meth:`querySelectorEval` Jeval = querySelectorEval #: Alias to :meth:`querySelectorAll` JJ = querySelectorAll #: Alias to :meth:`querySelectorAllEval` JJeval = querySelectorAllEval async def content(self) -> str: """Get the whole HTML contents of the page.""" return await self.evaluate(''' () => { let retVal = ''; if (document.doctype) retVal = new XMLSerializer().serializeToString(document.doctype); if (document.documentElement) retVal += document.documentElement.outerHTML; return retVal; } '''.strip()) async def setContent(self, html: str) -> None: """Set content to this page.""" func = ''' function(html) { document.open(); document.write(html); document.close(); } ''' await self.evaluate(func, html) @property def name(self) -> str: """Get frame name.""" return self.__dict__.get('_name', '') @property def url(self) -> str: """Get url of the frame.""" return self._url @property def parentFrame(self) -> Optional['Frame']: """Get parent frame. If this frame is main frame or detached frame, return ``None``. """ return self._parentFrame @property def childFrames(self) -> List['Frame']: """Get child frames.""" return list(self._childFrames) def isDetached(self) -> bool: """Return ``True`` if this frame is detached. Otherwise return ``False``. """ return self._detached async def injectFile(self, filePath: str) -> str: """[Deprecated] Inject file to the frame.""" logger.warning('`injectFile` method is deprecated.' ' Use `addScriptTag` method instead.') with open(filePath) as f: contents = f.read() contents += '/* # sourceURL= {} */'.format(filePath.replace('\n', '')) return await self.evaluate(contents) async def addScriptTag(self, options: Dict) -> ElementHandle: # noqa: C901 """Add script tag to this frame. Details see :meth:`pyppeteer.page.Page.addScriptTag`. """ context = await self.executionContext() if context is None: raise ElementHandleError('ExecutionContext is None.') addScriptUrl = ''' async function addScriptUrl(url, type) { const script = document.createElement('script'); script.src = url; if (type) script.type = type; const promise = new Promise((res, rej) => { script.onload = res; script.onerror = rej; }); document.head.appendChild(script); await promise; return script; }''' addScriptContent = ''' function addScriptContent(content, type = 'text/javascript') { const script = document.createElement('script'); script.type = type; script.text = content; let error = null; script.onerror = e => error = e; document.head.appendChild(script); if (error) throw error; return script; }''' if isinstance(options.get('url'), str): url = options['url'] args = [addScriptUrl, url] if 'type' in options: args.append(options['type']) try: return (await context.evaluateHandle(*args) # type: ignore ).asElement() except ElementHandleError as e: raise PageError(f'Loading script from {url} failed') from e if isinstance(options.get('path'), str): with open(options['path']) as f: contents = f.read() contents = contents + '//# sourceURL={}'.format( options['path'].replace('\n', '')) args = [addScriptContent, contents] if 'type' in options: args.append(options['type']) return (await context.evaluateHandle(*args) # type: ignore ).asElement() if isinstance(options.get('content'), str): args = [addScriptContent, options['content']] if 'type' in options: args.append(options['type']) return (await context.evaluateHandle(*args) # type: ignore ).asElement() raise ValueError( 'Provide an object with a `url`, `path` or `content` property') async def addStyleTag(self, options: Dict) -> ElementHandle: """Add style tag to this frame. Details see :meth:`pyppeteer.page.Page.addStyleTag`. """ context = await self.executionContext() if context is None: raise ElementHandleError('ExecutionContext is None.') addStyleUrl = ''' async function (url) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; const promise = new Promise((res, rej) => { link.onload = res; link.onerror = rej; }); document.head.appendChild(link); await promise; return link; }''' addStyleContent = ''' async function (content) { const style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(content)); const promise = new Promise((res, rej) => { style.onload = res; style.onerror = rej; }); document.head.appendChild(style); await promise; return style; }''' if isinstance(options.get('url'), str): url = options['url'] try: return (await context.evaluateHandle( # type: ignore addStyleUrl, url)).asElement() except ElementHandleError as e: raise PageError(f'Loading style from {url} failed') from e if isinstance(options.get('path'), str): with open(options['path']) as f: contents = f.read() contents = contents + '/*# sourceURL={}*/'.format( options['path'].replace('\n', '')) return (await context.evaluateHandle( # type: ignore addStyleContent, contents)).asElement() if isinstance(options.get('content'), str): return (await context.evaluateHandle( # type: ignore addStyleContent, options['content'])).asElement() raise ValueError( 'Provide an object with a `url`, `path` or `content` property') async def click(self, selector: str, options: dict = None, **kwargs: Any ) -> None: """Click element which matches ``selector``. Details see :meth:`pyppeteer.page.Page.click`. """ options = merge_dict(options, kwargs) handle = await self.J(selector) if not handle: raise PageError('No node found for selector: ' + selector) await handle.click(options) await handle.dispose() async def focus(self, selector: str) -> None: """Focus element which matches ``selector``. Details see :meth:`pyppeteer.page.Page.focus`. """ handle = await self.J(selector) if not handle: raise PageError('No node found for selector: ' + selector) await self.evaluate('element => element.focus()', handle) await handle.dispose() async def hover(self, selector: str) -> None: """Mouse hover the element which matches ``selector``. Details see :meth:`pyppeteer.page.Page.hover`. """ handle = await self.J(selector) if not handle: raise PageError('No node found for selector: ' + selector) await handle.hover() await handle.dispose() async def select(self, selector: str, *values: str) -> List[str]: """Select options and return selected values. Details see :meth:`pyppeteer.page.Page.select`. """ for value in values: if not isinstance(value, str): raise TypeError( 'Values must be string. ' f'Found {value} of type {type(value)}' ) return await self.querySelectorEval( # type: ignore selector, ''' (element, values) => { if (element.nodeName.toLowerCase() !== 'select') throw new Error('Element is not a ================================================ FILE: tests/static/csp.html ================================================ ================================================ FILE: tests/static/csscoverage/involved.html ================================================
woof!
fancy text ================================================ FILE: tests/static/csscoverage/media.html ================================================
hello, world
================================================ FILE: tests/static/csscoverage/multiple.html ================================================ ================================================ FILE: tests/static/csscoverage/simple.html ================================================
hello, world
================================================ FILE: tests/static/csscoverage/sourceurl.html ================================================ ================================================ FILE: tests/static/csscoverage/stylesheet1.css ================================================ body { color: red; } ================================================ FILE: tests/static/csscoverage/stylesheet2.css ================================================ html { margin: 0; padding: 0; } ================================================ FILE: tests/static/csscoverage/unused.html ================================================ ================================================ FILE: tests/static/detect-touch.html ================================================ Detect Touch Test ================================================ FILE: tests/static/error.html ================================================ ================================================ FILE: tests/static/es6/es6import.js ================================================ import num from './es6module.js'; window.__es6injected = num; ================================================ FILE: tests/static/es6/es6module.js ================================================ export default 42; ================================================ FILE: tests/static/es6/es6pathimport.js ================================================ import num from '/static/es6/es6module.js'; window.__es6injected = num; ================================================ FILE: tests/static/fileupload.html ================================================ File upload test ================================================ FILE: tests/static/frame-204.html ================================================ ================================================ FILE: tests/static/frame.html ================================================
Hi, I'm frame
================================================ FILE: tests/static/grid.html ================================================ ================================================ FILE: tests/static/historyapi.html ================================================ ================================================ FILE: tests/static/huge-page.html ================================================ ================================================ FILE: tests/static/injectedfile.js ================================================ window.__injected = 42; window.__injectedError = new Error('hi'); ================================================ FILE: tests/static/injectedstyle.css ================================================ body { background-color: red; } ================================================ FILE: tests/static/jscoverage/eval.html ================================================ ================================================ FILE: tests/static/jscoverage/involved.html ================================================ ================================================ FILE: tests/static/jscoverage/multiple.html ================================================ ================================================ FILE: tests/static/jscoverage/ranges.html ================================================ ================================================ FILE: tests/static/jscoverage/script1.js ================================================ console.log(3); ================================================ FILE: tests/static/jscoverage/script2.js ================================================ console.log(3); ================================================ FILE: tests/static/jscoverage/simple.html ================================================ ================================================ FILE: tests/static/jscoverage/sourceurl.html ================================================ ================================================ FILE: tests/static/jscoverage/unused.html ================================================ ================================================ FILE: tests/static/keyboard.html ================================================ Keyboard test ================================================ FILE: tests/static/mobile.html ================================================ ================================================ FILE: tests/static/modernizr.js ================================================ /*! modernizr 3.5.0 (Custom Build) | MIT * * https://modernizr.com/download/?-touchevents-setclasses !*/ !function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t { box.style.left = event.pageX + 'px'; box.style.top = event.pageY + 'px'; updateButtons(event.buttons); }, true); document.addEventListener('mousedown', event => { updateButtons(event.buttons); box.classList.add('button-' + event.which); }, true); document.addEventListener('mouseup', event => { updateButtons(event.buttons); box.classList.remove('button-' + event.which); }, true); function updateButtons(buttons) { for (let i = 0; i < 5; i++) box.classList.toggle('button-' + i, buttons & (1 << i)); } })(); ================================================ FILE: tests/static/nested-frames.html ================================================ ================================================ FILE: tests/static/offscreenbuttons.html ================================================ ================================================ FILE: tests/static/one-frame.html ================================================ > ================================================ FILE: tests/static/one-style.css ================================================ body { background-color: pink; } ================================================ FILE: tests/static/one-style.html ================================================
hello, world!
================================================ FILE: tests/static/popup/popup.html ================================================ Popup I am a popup ================================================ FILE: tests/static/popup/window-open.html ================================================ Popup test ================================================ FILE: tests/static/resetcss.html ================================================ ================================================ FILE: tests/static/script.js ================================================ console.log('Cheers!'); ================================================ FILE: tests/static/scrollable.html ================================================ Scrollable test ================================================ FILE: tests/static/select.html ================================================ Selection Test ================================================ FILE: tests/static/self-request.html ================================================ ================================================ FILE: tests/static/serviceworkers/empty/sw.html ================================================ ================================================ FILE: tests/static/serviceworkers/empty/sw.js ================================================ ================================================ FILE: tests/static/serviceworkers/fetch/style.css ================================================ body { background-color: pink; } ================================================ FILE: tests/static/serviceworkers/fetch/sw.html ================================================ ================================================ FILE: tests/static/serviceworkers/fetch/sw.js ================================================ self.addEventListener('fetch', event => { event.respondWith(fetch(event.request)); }); self.addEventListener('activate', event => { event.waitUntil(clients.claim()); }); ================================================ FILE: tests/static/shadow.html ================================================ ================================================ FILE: tests/static/simple-extension/index.js ================================================ // Mock script for background extension ================================================ FILE: tests/static/simple-extension/manifest.json ================================================ { "name": "Simple extension", "version": "0.1", "app": { "background": { "scripts": ["index.js"] } }, "permissions": ["background"], "manifest_version": 2 } ================================================ FILE: tests/static/simple.json ================================================ {"foo": "bar"} ================================================ FILE: tests/static/style.css ================================================ div { color: blue; } ================================================ FILE: tests/static/sw.js ================================================ ================================================ FILE: tests/static/temperable.html ================================================ ================================================ FILE: tests/static/textarea.html ================================================ Textarea test ================================================ FILE: tests/static/touches.html ================================================ Touch test ================================================ FILE: tests/static/two-frames.html ================================================ ================================================ FILE: tests/static/worker/worker.html ================================================ Worker test ================================================ FILE: tests/static/worker/worker.js ================================================ console.log('hello from the worker'); function workerFunction() { return 'worker function result'; } self.addEventListener('message', event => { console.log('got this data: ' + event.data); }); (async function() { while (true) { self.postMessage(workerFunction.toString()); await new Promise(x => setTimeout(x, 100)); } })(); ================================================ FILE: tests/static/wrappedlink.html ================================================ ================================================ FILE: tests/test_abnormal_crash.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio import logging import unittest from syncer import sync from pyppeteer import launch from pyppeteer.chromium_downloader import current_platform from pyppeteer.errors import NetworkError class TestBrowserCrash(unittest.TestCase): @sync async def test_browser_crash_send(self): browser = await launch(args=['--no-sandbox']) page = await browser.newPage() await page.goto('about:blank') await page.querySelector("title") browser.process.terminate() browser.process.wait() if current_platform().startswith('win'): # wait for terminating browser process await asyncio.sleep(1) with self.assertRaises(NetworkError): await page.querySelector("title") with self.assertRaises(NetworkError): with self.assertLogs('pyppeteer', logging.ERROR): await page.querySelector("title") with self.assertRaises(ConnectionError): await browser.newPage() ================================================ FILE: tests/test_browser.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from copy import deepcopy import os from pathlib import Path import unittest from syncer import sync from pyppeteer import connect, launch from .base import BaseTestCase, DEFAULT_OPTIONS from .utils import waitEvent class TestBrowser(unittest.TestCase): extensionPath = Path(__file__).parent / 'static' / 'simple-extension' extensionOptions = { 'headless': False, 'args': [ '--no-sandbox', '--disable-extensions-except={}'.format(extensionPath), '--load-extensions={}'.format(extensionPath), ] } def waitForBackgroundPageTarget(self, browser): promise = asyncio.get_event_loop().create_future() for target in browser.targets(): if target.type == 'background_page': promise.set_result(target) return promise def _listener(target) -> None: if target.type != 'background_page': return browser.removeListener(_listener) promise.set_result(target) browser.on('targetcreated', _listener) return promise @sync async def test_browser_process(self): browser = await launch(DEFAULT_OPTIONS) process = browser.process self.assertGreater(process.pid, 0) wsEndpoint = browser.wsEndpoint browser2 = await connect({'browserWSEndpoint': wsEndpoint}) self.assertIsNone(browser2.process) await browser.close() @sync async def test_version(self): browser = await launch(DEFAULT_OPTIONS) version = await browser.version() self.assertTrue(len(version) > 0) self.assertTrue(version.startswith('Headless')) await browser.close() @sync async def test_user_agent(self): browser = await launch(DEFAULT_OPTIONS) userAgent = await browser.userAgent() self.assertGreater(len(userAgent), 0) self.assertIn('WebKit', userAgent) await browser.close() @sync async def test_disconnect(self): browser = await launch(DEFAULT_OPTIONS) endpoint = browser.wsEndpoint browser1 = await connect(browserWSEndpoint=endpoint) browser2 = await connect(browserWSEndpoint=endpoint) discon = [] discon1 = [] discon2 = [] browser.on('disconnected', lambda: discon.append(1)) browser1.on('disconnected', lambda: discon1.append(1)) browser2.on('disconnected', lambda: discon2.append(1)) await asyncio.wait([ browser2.disconnect(), waitEvent(browser2, 'disconnected'), ]) self.assertEqual(len(discon), 0) self.assertEqual(len(discon1), 0) self.assertEqual(len(discon2), 1) await asyncio.wait([ waitEvent(browser1, 'disconnected'), waitEvent(browser, 'disconnected'), browser.close(), ]) self.assertEqual(len(discon), 1) self.assertEqual(len(discon1), 1) self.assertEqual(len(discon2), 1) @sync async def test_crash(self): browser = await launch(DEFAULT_OPTIONS) page = await browser.newPage() errors = [] page.on('error', lambda e: errors.append(e)) asyncio.ensure_future(page.goto('chrome://crash')) for i in range(100): await asyncio.sleep(0.01) if errors: break await browser.close() self.assertTrue(errors) @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') @sync async def test_background_target_type(self): browser = await launch(self.extensionOptions) page = await browser.newPage() backgroundPageTarget = await self.waitForBackgroundPageTarget(browser) await page.close() await browser.close() self.assertTrue(backgroundPageTarget) @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') @sync async def test_OOPIF(self): options = deepcopy(DEFAULT_OPTIONS) options['headless'] = False browser = await launch(options) page = await browser.newPage() example_page = 'http://example.com/' await page.goto(example_page) await page.setRequestInterception(True) async def intercept(req): await req.respond({'body': 'YO, GOOGLE.COM'}) page.on('request', lambda req: asyncio.ensure_future(intercept(req))) await page.evaluate('''() => { const frame = document.createElement('iframe'); frame.setAttribute('src', 'https://google.com/'); document.body.appendChild(frame); return new Promise(x => frame.onload = x); }''') await page.waitForSelector('iframe[src="https://google.com/"]') urls = [] for frame in page.frames: urls.append(frame.url) urls.sort() self.assertEqual(urls, [example_page, 'https://google.com/']) await browser.close() @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') @sync async def test_background_page(self): browserWithExtension = await launch(self.extensionOptions) backgroundPageTarget = await self.waitForBackgroundPageTarget(browserWithExtension) # noqa: E501 self.assertIsNotNone(backgroundPageTarget) page = await backgroundPageTarget.page() self.assertEqual(await page.evaluate('2 * 3'), 6) await browserWithExtension.close() class TestPageClose(BaseTestCase): @sync async def test_not_visible_in_browser_pages(self): newPage = await self.context.newPage() self.assertIn(newPage, await self.browser.pages()) await newPage.close() self.assertNotIn(newPage, await self.browser.pages()) @sync async def test_before_unload(self): newPage = await self.context.newPage() await newPage.goto(self.url + 'static/beforeunload.html') await newPage.click('body') asyncio.ensure_future(newPage.close(runBeforeUnload=True)) dialog = await waitEvent(newPage, 'dialog') self.assertEqual(dialog.type, 'beforeunload') self.assertEqual(dialog.defaultValue, '') self.assertEqual(dialog.message, '') asyncio.ensure_future(dialog.accept()) await waitEvent(newPage, 'close') @sync async def test_page_close_state(self): newPage = await self.context.newPage() self.assertFalse(newPage.isClosed()) await newPage.close() self.assertTrue(newPage.isClosed()) ================================================ FILE: tests/test_browser_context.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio import unittest from pyppeteer import connect from pyppeteer.errors import BrowserError from syncer import sync from .base import BaseTestCase from .utils import waitEvent class BrowserBaseTestCase(BaseTestCase): def setUp(self): pass def tearDown(self): pass class TestBrowserContext(BrowserBaseTestCase): @sync async def test_default_context(self): self.assertEqual(len(self.browser.browserContexts), 1) defaultContext = self.browser.browserContexts[0] self.assertFalse(defaultContext.isIncognito()) with self.assertRaises(BrowserError) as cm: await defaultContext.close() self.assertIn('cannot be closed', cm.exception.args[0]) @unittest.skip('this test not pass in some environment') @sync async def test_incognito_context(self): self.assertEqual(len(self.browser.browserContexts), 1) context = await self.browser.createIncognitoBrowserContext() self.assertTrue(context.isIncognito()) self.assertEqual(len(self.browser.browserContexts), 2) self.assertIn(context, self.browser.browserContexts) await context.close() self.assertEqual(len(self.browser.browserContexts), 1) @sync async def test_close_all_targets_once(self): self.assertEqual(len(await self.browser.pages()), 1) context = await self.browser.createIncognitoBrowserContext() await context.newPage() self.assertEqual(len(await self.browser.pages()), 2) self.assertEqual(len(await context.pages()), 1) await context.close() self.assertEqual(len(await self.browser.pages()), 1) @sync async def test_window_open_use_parent_tab_context(self): context = await self.browser.createIncognitoBrowserContext() page = await context.newPage() await page.goto(self.url + 'empty') asyncio.ensure_future( page.evaluate('url => window.open(url)', self.url + 'empty')) popupTarget = await waitEvent(self.browser, 'targetcreated') self.assertEqual(popupTarget.browserContext, context) await context.close() @sync async def test_fire_target_event(self): context = await self.browser.createIncognitoBrowserContext() events = [] context.on('targetcreated', lambda t: events.append('CREATED: ' + t.url)) # noqa: E501 context.on('targetchanged', lambda t: events.append('CHANGED: ' + t.url)) # noqa: E501 context.on('targetdestroyed', lambda t: events.append('DESTROYED: ' + t.url)) # noqa: E501 page = await context.newPage() await page.goto(self.url + 'empty') await page.close() self.assertEqual(events, [ 'CREATED: about:blank', 'CHANGED: ' + self.url + 'empty', 'DESTROYED: ' + self.url + 'empty', ]) @unittest.skip('this test not pass in some environment') @sync async def test_isolate_local_storage_and_cookie(self): context1 = await self.browser.createIncognitoBrowserContext() context2 = await self.browser.createIncognitoBrowserContext() self.assertEqual(len(context1.targets()), 0) self.assertEqual(len(context2.targets()), 0) # create a page in the first incognito context page1 = await context1.newPage() await page1.goto(self.url + 'empty') await page1.evaluate('''() => { localStorage.setItem('name', 'page1'); document.cookie = 'name=page1'; }''') self.assertEqual(len(context1.targets()), 1) self.assertEqual(len(context2.targets()), 0) # create a page in the second incognito context page2 = await context2.newPage() await page2.goto(self.url + 'empty') await page2.evaluate('''() => { localStorage.setItem('name', 'page2'); document.cookie = 'name=page2'; }''') self.assertEqual(len(context1.targets()), 1) self.assertEqual(context1.targets()[0], page1.target) self.assertEqual(len(context2.targets()), 1) self.assertEqual(context2.targets()[0], page2.target) # make sure pages don't share local storage and cookie self.assertEqual(await page1.evaluate('localStorage.getItem("name")'), 'page1') # noqa: E501 self.assertEqual(await page1.evaluate('document.cookie'), 'name=page1') self.assertEqual(await page2.evaluate('localStorage.getItem("name")'), 'page2') # noqa: E501 self.assertEqual(await page2.evaluate('document.cookie'), 'name=page2') await context1.close() await context2.close() self.assertEqual(len(self.browser.browserContexts), 1) @sync async def test_across_session(self): self.assertEqual(len(self.browser.browserContexts), 1) context = await self.browser.createIncognitoBrowserContext() self.assertEqual(len(self.browser.browserContexts), 2) remoteBrowser = await connect( browserWSEndpoint=self.browser.wsEndpoint) contexts = remoteBrowser.browserContexts self.assertEqual(len(contexts), 2) await remoteBrowser.disconnect() await context.close() ================================================ FILE: tests/test_connection.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from syncer import sync from pyppeteer.errors import NetworkError from .base import BaseTestCase class TestConnection(BaseTestCase): @sync async def test_error_msg(self): with self.assertRaises(NetworkError) as cm: await self.page._client.send('ThisCommand.DoesNotExists') self.assertIn('ThisCommand.DoesNotExists', cm.exception.args[0]) class TestCDPSession(BaseTestCase): @sync async def test_create_session(self): client = await self.page.target.createCDPSession() await client.send('Runtime.enable') await client.send('Runtime.evaluate', {'expression': 'window.foo = "bar"'}) foo = await self.page.evaluate('window.foo') self.assertEqual(foo, 'bar') @sync async def test_send_event(self): client = await self.page.target.createCDPSession() await client.send('Network.enable') events = [] client.on('Network.requestWillBeSent', lambda e: events.append(e)) await self.page.goto(self.url + 'empty') self.assertEqual(len(events), 1) @sync async def test_enable_disable_domain(self): client = await self.page.target.createCDPSession() await client.send('Runtime.enable') await client.send('Debugger.enable') await self.page.coverage.startJSCoverage() await self.page.coverage.stopJSCoverage() @sync async def test_detach(self): client = await self.page.target.createCDPSession() await client.send('Runtime.enable') evalResponse = await client.send( 'Runtime.evaluate', {'expression': '1 + 2', 'returnByValue': True}) self.assertEqual(evalResponse['result']['value'], 3) await client.detach() with self.assertRaises(NetworkError): await client.send( 'Runtime.evaluate', {'expression': '1 + 3', 'returnByValue': True} ) ================================================ FILE: tests/test_coverage.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from syncer import sync from .base import BaseTestCase class TestJSCoverage(BaseTestCase): @sync async def test_js_coverage(self): await self.page.coverage.startJSCoverage() await self.page.goto( self.url + 'static/jscoverage/simple.html', waitUntil='networkidle0', ) coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 1) self.assertIn('/jscoverage/simple.html', coverage[0]['url']) self.assertEqual(coverage[0]['ranges'], [ {'start': 0, 'end': 17}, {'start': 35, 'end': 61}, ]) @sync async def test_js_coverage_source_url(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/sourceurl.html') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 1) self.assertEqual(coverage[0]['url'], 'nicename.js') @sync async def test_js_coverage_ignore_empty(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'empty') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(coverage, []) @sync async def test_ignore_eval_script_by_default(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/eval.html') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 1) @sync async def test_not_ignore_eval_script_with_reportAnonymousScript(self): await self.page.coverage.startJSCoverage(reportAnonymousScript=True) await self.page.goto(self.url + 'static/jscoverage/eval.html') coverage = await self.page.coverage.stopJSCoverage() self.assertTrue(any(entry for entry in coverage if entry['url'].startswith('debugger://'))) self.assertEqual(len(coverage), 2) @sync async def test_ignore_injected_script(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'empty') await self.page.evaluate('console.log("foo")') await self.page.evaluate('() => console.log("bar")') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 0) @sync async def test_ignore_injected_script_with_reportAnonymousScript(self): await self.page.coverage.startJSCoverage(reportAnonymousScript=True) await self.page.goto(self.url + 'empty') await self.page.evaluate('console.log("foo")') await self.page.evaluate('() => console.log("bar")') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 0) @sync async def test_js_coverage_multiple_script(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/multiple.html') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 2) coverage.sort(key=lambda cov: cov['url']) self.assertIn('/jscoverage/script1.js', coverage[0]['url']) self.assertIn('/jscoverage/script2.js', coverage[1]['url']) @sync async def test_js_coverage_ranges(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/ranges.html') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 1) entry = coverage[0] self.assertEqual(len(entry['ranges']), 1) range = entry['ranges'][0] self.assertEqual( entry['text'][range['start']:range['end']], 'console.log(\'used!\');', ) @sync async def test_no_coverage(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/unused.html') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 1) entry = coverage[0] self.assertIn('static/jscoverage/unused.html', entry['url']) self.assertEqual(len(entry['ranges']), 0) @sync async def test_js_coverage_condition(self): await self.page.coverage.startJSCoverage() await self.page.goto(self.url + 'static/jscoverage/involved.html') coverage = await self.page.coverage.stopJSCoverage() expected_range = [ {'start': 0, 'end': 35}, {'start': 50, 'end': 100}, {'start': 107, 'end': 141}, {'start': 148, 'end': 160}, {'start': 168, 'end': 207}, ] self.assertEqual(coverage[0]['ranges'], expected_range) @sync async def test_js_coverage_no_reset_navigation(self): await self.page.coverage.startJSCoverage(resetOnNavigation=False) await self.page.goto(self.url + 'static/jscoverage/multiple.html') await self.page.goto(self.url + 'empty') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 2) @sync async def test_js_coverage_reset_navigation(self): await self.page.coverage.startJSCoverage() # enabled by default await self.page.goto(self.url + 'static/jscoverage/multiple.html') await self.page.goto(self.url + 'empty') coverage = await self.page.coverage.stopJSCoverage() self.assertEqual(len(coverage), 0) class TestCSSCoverage(BaseTestCase): @sync async def test_css_coverage(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/simple.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 1) self.assertIn('/csscoverage/simple.html', coverage[0]['url']) self.assertEqual(coverage[0]['ranges'], [{'start': 1, 'end': 22}]) range = coverage[0]['ranges'][0] self.assertEqual( coverage[0]['text'][range['start']:range['end']], 'div { color: green; }', ) @sync async def test_css_coverage_url(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/sourceurl.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 1) self.assertEqual(coverage[0]['url'], 'nicename.css') @sync async def test_css_coverage_multiple(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/multiple.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 2) coverage.sort(key=lambda cov: cov['url']) self.assertIn('/csscoverage/stylesheet1.css', coverage[0]['url']) self.assertIn('/csscoverage/stylesheet2.css', coverage[1]['url']) @sync async def test_css_coverage_no_coverage(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/unused.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 1) self.assertEqual(coverage[0]['url'], 'unused.css') self.assertEqual(coverage[0]['ranges'], []) @sync async def test_css_coverage_media(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/media.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 1) self.assertIn('/csscoverage/media.html', coverage[0]['url']) self.assertEqual(coverage[0]['ranges'], [{'start': 17, 'end': 38}]) @sync async def test_css_coverage_complicated(self): await self.page.coverage.startCSSCoverage() await self.page.goto(self.url + 'static/csscoverage/involved.html') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 1) range = coverage[0]['ranges'] self.assertEqual(range, [ {'start': 20, 'end': 168}, {'start': 198, 'end': 304}, ]) @sync async def test_css_ignore_injected_css(self): await self.page.goto(self.url + 'empty') await self.page.coverage.startCSSCoverage() await self.page.addStyleTag(content='body { margin: 10px; }') # trigger style recalc margin = await self.page.evaluate( '() => window.getComputedStyle(document.body).margin') self.assertEqual(margin, '10px') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(coverage, []) @sync async def test_css_coverage_no_reset_navigation(self): await self.page.coverage.startCSSCoverage(resetOnNavigation=False) await self.page.goto(self.url + 'static/csscoverage/multiple.html') await self.page.goto(self.url + 'empty') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 2) @sync async def test_css_coverage_reset_navigation(self): await self.page.coverage.startCSSCoverage() # enabled by default await self.page.goto(self.url + 'static/csscoverage/multiple.html') await self.page.goto(self.url + 'empty') coverage = await self.page.coverage.stopCSSCoverage() self.assertEqual(len(coverage), 0) ================================================ FILE: tests/test_dialog.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from syncer import sync from .base import BaseTestCase class TestDialog(BaseTestCase): @sync async def test_alert(self): def dialog_test(dialog): self.assertEqual(dialog.type, 'alert') self.assertEqual(dialog.defaultValue, '') self.assertEqual(dialog.message, 'yo') asyncio.ensure_future(dialog.accept()) self.page.on('dialog', dialog_test) await self.page.evaluate('() => alert("yo")') @sync async def test_prompt(self): def dialog_test(dialog): self.assertEqual(dialog.type, 'prompt') self.assertEqual(dialog.defaultValue, 'yes.') self.assertEqual(dialog.message, 'question?') asyncio.ensure_future(dialog.accept('answer!')) self.page.on('dialog', dialog_test) answer = await self.page.evaluate('() => prompt("question?", "yes.")') self.assertEqual(answer, 'answer!') @sync async def test_prompt_dismiss(self): def dismiss_test(dialog, *args): asyncio.ensure_future(dialog.dismiss()) self.page.on('dialog', dismiss_test) result = await self.page.evaluate('() => prompt("question?", "yes.")') self.assertIsNone(result) ================================================ FILE: tests/test_element_handle.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging import sys from syncer import sync import pyppeteer from pyppeteer.errors import ElementHandleError from .base import BaseTestCase from .frame_utils import attachFrame class TestBoundingBox(BaseTestCase): @sync async def test_bounding_box(self): await self.page.setViewport({'width': 500, 'height': 500}) await self.page.goto(self.url + 'static/grid.html') elementHandle = await self.page.J('.box:nth-of-type(13)') box = await elementHandle.boundingBox() self.assertEqual({'x': 100, 'y': 50, 'width': 50, 'height': 50}, box) @sync async def test_nested_frame(self): await self.page.setViewport({'width': 500, 'height': 500}) await self.page.goto(self.url + 'static/nested-frames.html') nestedFrame = self.page.frames[1].childFrames[1] elementHandle = await nestedFrame.J('div') box = await elementHandle.boundingBox() # Frame size is unstable # Frame order is unstable # self.assertIn(box, [ # {'x': 28, 'y': 28, 'width': 264, 'height': 16}, # {'x': 28, 'y': 260, 'width': 264, 'height': 16}, # ]) self.assertEqual(box['x'], 28) self.assertIn(box['y'], [28, 260]) self.assertEqual(box['width'], 264) @sync async def test_invisible_element(self): await self.page.setContent('
hi
') element = await self.page.J('div') self.assertIsNone(await element.boundingBox()) @sync async def test_force_layout(self): await self.page.setViewport({'width': 500, 'height': 500}) await self.page.setContent( '
hello
') elementHandle = await self.page.J('div') await self.page.evaluate( 'element => element.style.height = "200px"', elementHandle, ) box = await elementHandle.boundingBox() self.assertEqual(box, { 'x': 8, 'y': 8, 'width': 100, 'height': 200, }) @sync async def test_svg(self): await self.page.setContent(''' ''') # noqa: E501 element = await self.page.J('#therect') pptrBoundingBox = await element.boundingBox() webBoundingBox = await self.page.evaluate('''e => { const rect = e.getBoundingClientRect(); return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; }''', element) # noqa: E501 self.assertEqual(pptrBoundingBox, webBoundingBox) class TestBoxModel(BaseTestCase): def setUp(self): self._old_debug = pyppeteer.DEBUG super().setUp() def tearDown(self): super().tearDown() pyppeteer.DEBUG = self._old_debug @sync async def test_box_model(self): await self.page.goto(self.url + 'static/resetcss.html') # add frame and position it absolutely await attachFrame( self.page, 'frame1', self.url + 'static/resetcss.html') await self.page.evaluate('''() => { const frame = document.querySelector('#frame1'); frame.style = ` position: absolute; left: 1px; top: 2px; `; }''') # add div and position it absolutely inside frame frame = self.page.frames[1] divHandle = (await frame.evaluateHandle('''() => { const div = document.createElement('div'); document.body.appendChild(div); div.style = ` box-sizing: border-box; position: absolute; border-left: 1px solid black; padding-left: 2px; margin-left: 3px; left: 4px; top: 5px; width: 6px; height: 7px; ` return div }''')).asElement() # query div's boxModel and assert box values box = await divHandle.boxModel() self.assertEqual(box['width'], 6) self.assertEqual(box['height'], 7) self.assertEqual(box['margin'][0], { 'x': 1 + 4, 'y': 2 + 5, }) self.assertEqual(box['border'][0], { 'x': 1 + 4 + 3, 'y': 2 + 5, }) self.assertEqual(box['padding'][0], { 'x': 1 + 4 + 3 + 1, 'y': 2 + 5, }) self.assertEqual(box['content'][0], { 'x': 1 + 4 + 3 + 1 + 2, 'y': 2 + 5, }) @sync async def test_box_model_invisible(self): await self.page.setContent('
hi
') element = await self.page.J('div') with self.assertLogs('pyppeteer.element_handle', logging.DEBUG): self.assertIsNone(await element.boxModel()) @sync async def test_debug_error(self): await self.page.setContent('
hi
') element = await self.page.J('div') pyppeteer.DEBUG = True with self.assertLogs('pyppeteer.element_handle', logging.ERROR): self.assertIsNone(await element.boxModel()) pyppeteer.DEBUG = False with self.assertRaises(AssertionError): with self.assertLogs('pyppeteer.element_handle', logging.INFO): self.assertIsNone(await element.boxModel()) class TestContentFrame(BaseTestCase): @sync async def test_content_frame(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') elementHandle = await self.page.J('#frame1') frame = await elementHandle.contentFrame() self.assertEqual(frame, self.page.frames[1]) class TestClick(BaseTestCase): @sync async def test_clik(self): await self.page.goto(self.url + 'static/button.html') button = await self.page.J('button') await button.click() self.assertEqual(await self.page.evaluate('result'), 'Clicked') @sync async def test_shadow_dom(self): await self.page.goto(self.url + 'static/shadow.html') button = await self.page.evaluateHandle('() => button') await button.click() self.assertTrue(await self.page.evaluate('clicked')) @sync async def test_text_node(self): await self.page.goto(self.url + 'static/button.html') buttonTextNode = await self.page.evaluateHandle( '() => document.querySelector("button").firstChild') with self.assertRaises(ElementHandleError) as cm: await buttonTextNode.click() self.assertEqual('Node is not of type HTMLElement', cm.exception.args[0]) @sync async def test_detached_node(self): await self.page.goto(self.url + 'static/button.html') button = await self.page.J('button') await self.page.evaluate('btn => btn.remove()', button) with self.assertRaises(ElementHandleError) as cm: await button.click() self.assertEqual('Node is detached from document', cm.exception.args[0]) @sync async def test_hidden_node(self): await self.page.goto(self.url + 'static/button.html') button = await self.page.J('button') await self.page.evaluate('btn => btn.style.display = "none"', button) with self.assertRaises(ElementHandleError) as cm: await button.click() self.assertEqual( 'Node is either not visible or not an HTMLElement', cm.exception.args[0], ) @sync async def test_recursively_hidden_node(self): await self.page.goto(self.url + 'static/button.html') button = await self.page.J('button') await self.page.evaluate( 'btn => btn.parentElement.style.display = "none"', button) with self.assertRaises(ElementHandleError) as cm: await button.click() self.assertEqual( 'Node is either not visible or not an HTMLElement', cm.exception.args[0], ) @sync async def test_br_node(self): await self.page.setContent('hello
goodbye') br = await self.page.J('br') with self.assertRaises(ElementHandleError) as cm: await br.click() self.assertEqual( 'Node is either not visible or not an HTMLElement', cm.exception.args[0], ) class TestHover(BaseTestCase): @sync async def test_hover(self): await self.page.goto(self.url + 'static/scrollable.html') button = await self.page.J('#button-6') await button.hover() self.assertEqual( await self.page.evaluate( 'document.querySelector("button:hover").id'), 'button-6' ) class TestIsIntersectingViewport(BaseTestCase): @sync async def test_is_intersecting_viewport(self): await self.page.goto(self.url + 'static/offscreenbuttons.html') for i in range(11): button = await self.page.J('#btn{}'.format(i)) visible = i < 10 self.assertEqual(await button.isIntersectingViewport(), visible) class TestScreenshot(BaseTestCase): @sync async def test_screenshot_larger_than_viewport(self): await self.page.setViewport({'width': 500, 'height': 500}) await self.page.setContent(''' something above
''') elementHandle = await self.page.J('div.to-screenshot') await elementHandle.screenshot() size = await self.page.evaluate( '() => ({ w: window.innerWidth, h: window.innerHeight })' ) self.assertEqual({'w': 500, 'h': 500}, size) class TestQuerySelector(BaseTestCase): @sync async def test_J(self): await self.page.setContent('''
A
''') html = await self.page.J('html') second = await html.J('.second') inner = await second.J('.inner') content = await self.page.evaluate('e => e.textContent', inner) self.assertEqual(content, 'A') @sync async def test_J_none(self): await self.page.setContent('''
A
''') html = await self.page.J('html') second = await html.J('.third') self.assertIsNone(second) @sync async def test_Jeval(self): await self.page.setContent('''
10
''') tweet = await self.page.J('.tweet') content = await tweet.Jeval('.like', 'node => node.innerText') self.assertEqual(content, '100') @sync async def test_Jeval_subtree(self): htmlContent = '
not-a-child-div
a-child-div
' # noqa: E501 await self.page.setContent(htmlContent) elementHandle = await self.page.J('#myId') content = await elementHandle.Jeval('.a', 'node => node.innerText') self.assertEqual(content, 'a-child-div') @sync async def test_Jeval_with_missing_selector(self): htmlContent = '
not-a-child-div
' # noqa: E501 await self.page.setContent(htmlContent) elementHandle = await self.page.J('#myId') with self.assertRaises(ElementHandleError) as cm: await elementHandle.Jeval('.a', 'node => node.innerText') self.assertIn('Error: failed to find element matching selector ".a"', cm.exception.args[0]) @sync async def test_JJ(self): await self.page.setContent('''
A

B
''') html = await self.page.J('html') elements = await html.JJ('div') self.assertEqual(len(elements), 2) if sys.version_info >= (3, 6): result = [] for elm in elements: result.append( await self.page.evaluate('(e) => e.textContent', elm) ) self.assertEqual(result, ['A', 'B']) @sync async def test_JJ_empty(self): await self.page.setContent(''' A
B ''') html = await self.page.J('html') elements = await html.JJ('div') self.assertEqual(len(elements), 0) @sync async def test_JJEval(self): await self.page.setContent( '
' '
' ) tweet = await self.page.J('.tweet') content = await tweet.JJeval( '.like', 'nodes => nodes.map(n => n.innerText)') self.assertEqual(content, ['100', '10']) @sync async def test_JJEval_subtree(self): await self.page.setContent( '
not-a-child-div
' '
' '
a1-child-div
' '
a2-child-div
' '
' ) elementHandle = await self.page.J('#myId') content = await elementHandle.JJeval( '.a', 'nodes => nodes.map(n => n.innerText)') self.assertEqual(content, ['a1-child-div', 'a2-child-div']) @sync async def test_JJEval_missing_selector(self): await self.page.setContent( '
not-a-child-div
') elementHandle = await self.page.J('#myId') nodesLength = await elementHandle.JJeval('.a', 'nodes => nodes.length') self.assertEqual(nodesLength, 0) @sync async def test_xpath(self): await self.page.setContent( '
A
' # noqa: E501 ) html = await self.page.querySelector('html') second = await html.xpath('./body/div[contains(@class, \'second\')]') inner = await second[0].xpath('./div[contains(@class, \'inner\')]') content = await self.page.evaluate('(e) => e.textContent', inner[0]) self.assertEqual(content, 'A') @sync async def test_xpath_not_found(self): await self.page.goto(self.url + 'empty') html = await self.page.querySelector('html') element = await html.xpath('/div[contains(@class, \'third\')]') self.assertEqual(element, []) ================================================ FILE: tests/test_execution_context.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from syncer import sync from pyppeteer.errors import ElementHandleError, NetworkError from .base import BaseTestCase class TestQueryObject(BaseTestCase): @sync async def test_query_objects(self): await self.page.goto(self.url + 'empty') await self.page.evaluate( '() => window.set = new Set(["hello", "world"])' ) prototypeHandle = await self.page.evaluateHandle('() => Set.prototype') objectsHandle = await self.page.queryObjects(prototypeHandle) count = await self.page.evaluate( 'objects => objects.length', objectsHandle, ) self.assertEqual(count, 1) values = await self.page.evaluate( 'objects => Array.from(objects[0].values())', objectsHandle, ) self.assertEqual(values, ['hello', 'world']) @sync async def test_query_objects_disposed(self): await self.page.goto(self.url + 'empty') prototypeHandle = await self.page.evaluateHandle( '() => HTMLBodyElement.prototype' ) await prototypeHandle.dispose() with self.assertRaises(ElementHandleError): await self.page.queryObjects(prototypeHandle) @sync async def test_query_objects_primitive_value_error(self): await self.page.goto(self.url + 'empty') prototypeHandle = await self.page.evaluateHandle('() => 42') with self.assertRaises(ElementHandleError): await self.page.queryObjects(prototypeHandle) class TestJSHandle(BaseTestCase): @sync async def test_get_property(self): handle1 = await self.page.evaluateHandle( '() => ({one: 1, two: 2, three: 3})' ) handle2 = await handle1.getProperty('two') self.assertEqual(await handle2.jsonValue(), 2) @sync async def test_json_value(self): handle1 = await self.page.evaluateHandle('() => ({foo: "bar"})') json = await handle1.jsonValue() self.assertEqual(json, {'foo': 'bar'}) @sync async def test_json_date_fail(self): handle = await self.page.evaluateHandle( '() => new Date("2017-09-26T00:00:00.000Z")' ) json = await handle.jsonValue() self.assertEqual(json, {}) @sync async def test_json_circular_object_error(self): windowHandle = await self.page.evaluateHandle('window') with self.assertRaises(NetworkError) as cm: await windowHandle.jsonValue() self.assertIn('Object reference chain is too long', cm.exception.args[0]) @sync async def test_get_properties(self): handle1 = await self.page.evaluateHandle('() => ({foo: "bar"})') properties = await handle1.getProperties() foo = properties.get('foo') self.assertTrue(foo) self.assertEqual(await foo.jsonValue(), 'bar') @sync async def test_return_non_own_properties(self): aHandle = await self.page.evaluateHandle('''() => { class A { constructor() { this.a = '1'; } } class B extends A { constructor() { super(); this.b = '2'; } } return new B(); }''') properties = await aHandle.getProperties() self.assertEqual(await properties.get('a').jsonValue(), '1') self.assertEqual(await properties.get('b').jsonValue(), '2') @sync async def test_as_element(self): aHandle = await self.page.evaluateHandle('() => document.body') element = aHandle.asElement() self.assertTrue(element) @sync async def test_as_element_non_element(self): aHandle = await self.page.evaluateHandle('() => 2') element = aHandle.asElement() self.assertIsNone(element) @sync async def test_as_element_text_node(self): await self.page.setContent('
ee!
') aHandle = await self.page.evaluateHandle( '() => document.querySelector("div").firstChild') element = aHandle.asElement() self.assertTrue(element) self.assertTrue(await self.page.evaluate( '(e) => e.nodeType === HTMLElement.TEXT_NODE', element, )) @sync async def test_to_string_number(self): handle = await self.page.evaluateHandle('() => 2') self.assertEqual(handle.toString(), 'JSHandle:2') @sync async def test_to_string_str(self): handle = await self.page.evaluateHandle('() => "a"') self.assertEqual(handle.toString(), 'JSHandle:a') @sync async def test_to_string_complicated_object(self): handle = await self.page.evaluateHandle('() => window') self.assertEqual(handle.toString(), 'JSHandle@object') ================================================ FILE: tests/test_frame.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio import time import unittest from syncer import sync from pyppeteer.errors import ElementHandleError, NetworkError, TimeoutError from .base import BaseTestCase from .frame_utils import attachFrame, detachFrame, dumpFrames, navigateFrame from .utils import waitEvent addElement = 'tag=>document.body.appendChild(document.createElement(tag))' class TestContext(BaseTestCase): @sync async def test_frame_context(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') self.assertEqual(len(self.page.frames), 2) frame1 = self.page.frames[0] frame2 = self.page.frames[1] context1 = await frame1.executionContext() context2 = await frame2.executionContext() self.assertTrue(context1) self.assertTrue(context2) self.assertTrue(context1 != context2) self.assertEqual(context1.frame, frame1) self.assertEqual(context2.frame, frame2) await context1.evaluate('() => window.a = 1') await context2.evaluate('() => window.a = 2') a1 = await context1.evaluate('() => window.a') a2 = await context2.evaluate('() => window.a') self.assertEqual(a1, 1) self.assertEqual(a2, 2) class TestEvaluateHandle(BaseTestCase): @sync async def test_evaluate_handle(self): await self.page.goto(self.url + 'empty') frame = self.page.mainFrame windowHandle = await frame.evaluateHandle('window') self.assertTrue(windowHandle) class TestEvaluate(BaseTestCase): @sync async def test_frame_evaluate(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') self.assertEqual(len(self.page.frames), 2) frame1 = self.page.frames[0] frame2 = self.page.frames[1] await frame1.evaluate('() => window.a = 1') await frame2.evaluate('() => window.a = 2') a1 = await frame1.evaluate('window.a') a2 = await frame2.evaluate('window.a') self.assertEqual(a1, 1) self.assertEqual(a2, 2) @sync async def test_frame_evaluate_after_navigation(self): self.result = None def frame_navigated(frame): self.result = asyncio.ensure_future(frame.evaluate('6 * 7')) self.page.on('framenavigated', frame_navigated) await self.page.goto(self.url + 'empty') self.assertIsNotNone(self.result) self.assertEqual(await self.result, 42) @sync async def test_frame_cross_site(self): await self.page.goto(self.url + 'empty') mainFrame = self.page.mainFrame loc = await mainFrame.evaluate('window.location.href') self.assertIn('localhost', loc) await self.page.goto('http://127.0.0.1:{}/empty'.format(self.port)) loc = await mainFrame.evaluate('window.location.href') self.assertIn('127.0.0.1', loc) class TestWaitForFunction(BaseTestCase): @sync async def test_wait_for_expression(self): fut = asyncio.ensure_future( self.page.waitForFunction('window.__FOO === 1') ) await self.page.evaluate('window.__FOO = 1;') await fut @sync async def test_wait_for_function(self): fut = asyncio.ensure_future( self.page.waitForFunction('() => window.__FOO === 1') ) await self.page.evaluate('window.__FOO = 1;') await fut @sync async def test_wait_for_function_args(self): fut = asyncio.ensure_future( self.page.waitForFunction( '(a, b) => a + b === 3', {}, 1, 2) ) await fut @sync async def test_before_execution_context_resolved(self): await self.page.evaluateOnNewDocument('() => window.__RELOADED = true') await self.page.waitForFunction('''() => { if (!window.__RELOADED) window.location.reload(); return true; }''') @sync async def test_poll_on_interval(self): result = [] start_time = time.perf_counter() fut = asyncio.ensure_future(self.page.waitForFunction( '() => window.__FOO === "hit"', polling=100, )) fut.add_done_callback(lambda _: result.append(True)) await asyncio.sleep(0) # once switch task await self.page.evaluate('window.__FOO = "hit"') await self.page.evaluate( 'document.body.appendChild(document.createElement("div"))' ) await asyncio.sleep(0.02) self.assertFalse(result) await fut self.assertGreater(time.perf_counter() - start_time, 0.1) self.assertEqual(await self.page.evaluate('window.__FOO'), 'hit') @sync async def test_poll_on_mutation(self): result = [] fut = asyncio.ensure_future(self.page.waitForFunction( '() => window.__FOO === "hit"', polling='mutation', )) fut.add_done_callback(lambda _: result.append(True)) await asyncio.sleep(0) # once switch task await self.page.evaluate('window.__FOO = "hit"') await asyncio.sleep(0.1) self.assertFalse(result) await self.page.evaluate( 'document.body.appendChild(document.createElement("div"))' ) await fut self.assertTrue(result) @sync async def test_poll_on_raf(self): result = [] fut = asyncio.ensure_future(self.page.waitForFunction( '() => window.__FOO === "hit"', polling='raf', )) fut.add_done_callback(lambda _: result.append(True)) await asyncio.sleep(0) # once switch task await self.page.evaluate('window.__FOO = "hit"') await asyncio.sleep(0) # once switch task self.assertFalse(result) await fut self.assertTrue(result) @sync async def test_csp(self): await self.page.goto(self.url + 'csp') fut = asyncio.ensure_future(self.page.waitForFunction( '() => window.__FOO === "hit"', polling='raf', )) await self.page.evaluate('window.__FOO = "hit"') await fut @sync async def test_bad_polling_value(self): with self.assertRaises(ValueError) as cm: await self.page.waitForFunction('() => true', polling='unknown') self.assertIn('polling', cm.exception.args[0]) @sync async def test_negative_polling_value(self): with self.assertRaises(ValueError) as cm: await self.page.waitForFunction('() => true', polling=-100) self.assertIn('Cannot poll with non-positive interval', cm.exception.args[0]) @sync async def test_wait_for_function_return_value(self): result = await self.page.waitForFunction('() => 5') self.assertEqual(await result.jsonValue(), 5) @sync async def test_wait_for_function_window(self): self.assertTrue(await self.page.waitForFunction('() => window')) @sync async def test_wait_for_function_arg_element(self): await self.page.setContent('
') div = await self.page.J('div') fut = asyncio.ensure_future( self.page.waitForFunction('e => !e.parentElement', {}, div)) fut.add_done_callback(lambda _: self.set_result(True)) await asyncio.sleep(0.1) self.assertFalse(self.result) await self.page.evaluate('e => e.remove()', div) await fut self.assertTrue(self.result) @sync async def test_respect_timeout(self): with self.assertRaises(TimeoutError) as cm: await self.page.waitForFunction('false', {'timeout': 10}) self.assertIn( 'Waiting for function failed: timeout', cm.exception.args[0], ) @sync async def test_disable_timeout(self): watchdog = self.page.waitForFunction( '''() => { window.__counter = (window.__counter || 0) + 1; return window.__injected; }''', timeout=0, polling=10, ) await self.page.waitForFunction('() => window.__counter > 10') await self.page.evaluate('window.__injected = true') await watchdog class TestWaitForSelector(BaseTestCase): @sync async def test_wait_for_selector_immediate(self): frame = self.page.mainFrame result = [] fut = asyncio.ensure_future(frame.waitForSelector('*')) fut.add_done_callback(lambda _: result.append(True)) await fut self.assertTrue(result) result.clear() await frame.evaluate(addElement, 'div') fut = asyncio.ensure_future(frame.waitForSelector('div')) fut.add_done_callback(lambda _: result.append(True)) await fut self.assertTrue(result) @sync async def test_wait_for_selector_after_node_appear(self): frame = self.page.mainFrame result = [] fut = asyncio.ensure_future(frame.waitForSelector('div')) fut.add_done_callback(lambda _: result.append(True)) self.assertEqual(await frame.evaluate('() => 42'), 42) await asyncio.sleep(0.1) self.assertFalse(result) await frame.evaluate(addElement, 'br') await asyncio.sleep(0.1) self.assertFalse(result) await frame.evaluate(addElement, 'div') await fut self.assertTrue(result) @sync async def test_wait_for_selector_inner_html(self): fut = asyncio.ensure_future(self.page.waitForSelector('h3 div')) await self.page.evaluate(addElement, 'span') await self.page.evaluate('() => document.querySelector("span").innerHTML = "

"') # noqa: E501 await fut @sync async def test_shortcut_for_main_frame(self): await attachFrame(self.page, 'frame1', self.url + 'empty') otherFrame = self.page.frames[1] fut = asyncio.ensure_future(self.page.waitForSelector('div')) fut.add_done_callback(lambda _: self.set_result(True)) await otherFrame.evaluate(addElement, 'div') await asyncio.sleep(0.1) self.assertFalse(self.result) await self.page.evaluate(addElement, 'div') await fut self.assertTrue(self.result) @sync async def test_run_in_specified_frame(self): await attachFrame(self.page, 'frame1', self.url + 'empty') await attachFrame(self.page, 'frame2', self.url + 'empty') frame1 = self.page.frames[1] frame2 = self.page.frames[2] fut = asyncio.ensure_future(frame2.waitForSelector('div')) fut.add_done_callback(lambda _: self.set_result(True)) await frame1.evaluate(addElement, 'div') await asyncio.sleep(0.1) self.assertFalse(self.result) await frame2.evaluate(addElement, 'div') await fut self.assertTrue(self.result) @sync async def test_wait_for_selector_fail(self): await self.page.evaluate('() => document.querySelector = null') with self.assertRaises(ElementHandleError): await self.page.waitForSelector('*') @sync async def test_wait_for_page_navigation(self): await self.page.goto(self.url + 'empty') task = self.page.waitForSelector('h1') await self.page.goto(self.url + '1') await task @sync async def test_fail_page_closed(self): page = await self.context.newPage() await page.goto(self.url + 'empty') task = page.waitForSelector('.box') await page.close() with self.assertRaises(NetworkError): await task @unittest.skip('Cannot catch error.') @sync async def test_fail_frame_detached(self): await attachFrame(self.page, 'frame1', self.url + 'empty') frame = self.page.frames[1] fut = frame.waitForSelector('.box') await detachFrame(self.page, 'frame1') with self.assertRaises(Exception): await fut @sync async def test_cross_process_navigation(self): fut = asyncio.ensure_future(self.page.waitForSelector('h1')) fut.add_done_callback(lambda _: self.set_result(True)) await self.page.goto(self.url + 'empty') await asyncio.sleep(0.1) self.assertFalse(self.result) await self.page.reload() await asyncio.sleep(0.1) self.assertFalse(self.result) await self.page.goto('http://127.0.0.1:{}/'.format(self.port)) await fut self.assertTrue(self.result) @sync async def test_wait_for_selector_visible(self): div = [] fut = asyncio.ensure_future( self.page.waitForSelector('div', visible=True)) fut.add_done_callback(lambda _: div.append(True)) await self.page.setContent( '
1
' ) await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.removeProperty("display")') # noqa: E501 await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.removeProperty("visibility")') # noqa: E501 await fut self.assertTrue(div) @sync async def test_wait_for_selector_visible_inner(self): div = [] fut = asyncio.ensure_future( self.page.waitForSelector('div#inner', visible=True)) fut.add_done_callback(lambda _: div.append(True)) await self.page.setContent( '
' '
hi
' ) await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.removeProperty("display")') # noqa: E501 await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.removeProperty("visibility")') # noqa: E501 await fut self.assertTrue(div) @sync async def test_wait_for_selector_hidden(self): div = [] await self.page.setContent('
') fut = asyncio.ensure_future( self.page.waitForSelector('div', hidden=True)) fut.add_done_callback(lambda _: div.append(True)) await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.setProperty("visibility", "hidden")') # noqa: E501 await fut self.assertTrue(div) @sync async def test_wait_for_selector_display_none(self): div = [] await self.page.setContent('
') fut = asyncio.ensure_future( self.page.waitForSelector('div', hidden=True)) fut.add_done_callback(lambda _: div.append(True)) await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").style.setProperty("display", "none")') # noqa: E501 await fut self.assertTrue(div) @sync async def test_wait_for_selector_remove(self): div = [] await self.page.setContent('
') fut = asyncio.ensure_future( self.page.waitForSelector('div', hidden=True)) fut.add_done_callback(lambda _: div.append(True)) await asyncio.sleep(0.1) self.assertFalse(div) await self.page.evaluate('() => document.querySelector("div").remove()') # noqa: E501 await fut self.assertTrue(div) @sync async def test_wait_for_selector_timeout(self): with self.assertRaises(TimeoutError) as cm: await self.page.waitForSelector('div', timeout=10) self.assertIn( 'Waiting for selector "div" failed: timeout', cm.exception.args[0], ) @sync async def test_error_msg_wait_for_hidden(self): await self.page.setContent('
') with self.assertRaises(TimeoutError) as cm: await self.page.waitForSelector('div', hidden=True, timeout=10) self.assertIn( 'Waiting for selector "div" to be hidden failed: timeout', cm.exception.args[0], ) @sync async def test_wait_for_selector_node_mutation(self): div = [] fut = asyncio.ensure_future(self.page.waitForSelector('.cls')) fut.add_done_callback(lambda _: div.append(True)) await self.page.setContent('
') self.assertFalse(div) await self.page.evaluate( '() => document.querySelector("div").className="cls"' ) await asyncio.sleep(0.1) self.assertTrue(div) @sync async def test_wait_for_selector_return_element(self): selector = asyncio.ensure_future(self.page.waitForSelector('.zombo')) await self.page.setContent('
anything
') self.assertEqual( await self.page.evaluate('e => e.textContent', await selector), 'anything', ) class TestWaitForXPath(BaseTestCase): @sync async def test_fancy_xpath(self): await self.page.setContent('

red herring

hello world

') waitForXPath = await self.page.waitForXPath('//p[normalize-space(.)="hello world"]') # noqa: E501 self.assertEqual( await self.page.evaluate('x => x.textContent', waitForXPath), 'hello world ' ) @sync async def test_timeout(self): with self.assertRaises(TimeoutError) as cm: await self.page.waitForXPath('//div', timeout=10) self.assertIn( 'Waiting for XPath "//div" failed: timeout', cm.exception.args[0], ) @sync async def test_specified_frame(self): await attachFrame(self.page, 'frame1', self.url + 'empty') await attachFrame(self.page, 'frame2', self.url + 'empty') frame1 = self.page.frames[1] frame2 = self.page.frames[2] fut = asyncio.ensure_future(frame2.waitForXPath('//div')) fut.add_done_callback(lambda _: self.set_result(True)) self.assertFalse(self.result) await frame1.evaluate(addElement, 'div') self.assertFalse(self.result) await frame2.evaluate(addElement, 'div') self.assertTrue(self.result) @sync async def test_evaluation_failed(self): await self.page.evaluateOnNewDocument( 'function() {document.evaluate = null;}') await self.page.goto(self.url + 'empty') with self.assertRaises(ElementHandleError): await self.page.waitForXPath('*') @unittest.skip('Cannot catch error') @sync async def test_frame_detached(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') frame = self.page.frames[1] waitPromise = frame.waitForXPath('//*[@class="box"]', timeout=1000) await detachFrame(self.page, 'frame1') with self.assertRaises(Exception): await waitPromise @sync async def test_hidden(self): await self.page.setContent('
') waitForXPath = asyncio.ensure_future( self.page.waitForXPath('//div', hidden=True)) waitForXPath.add_done_callback(lambda _: self.set_result(True)) await self.page.waitForXPath('//div') self.assertFalse(self.result) await self.page.evaluate('document.querySelector("div").style.setProperty("display", "none")') # noqa: E501 self.assertTrue(await waitForXPath) self.assertTrue(self.result) @sync async def test_return_element_handle(self): waitForXPath = self.page.waitForXPath('//*[@class="zombo"]') await self.page.setContent('
anything
') self.assertEqual( await self.page.evaluate('x => x.textContent', await waitForXPath), 'anything' ) @sync async def test_text_node(self): await self.page.setContent('
some text') text = await self.page.waitForXPath('//div/text()') self.assertEqual( await (await text.getProperty('nodeType')).jsonValue(), 3 # Node.TEXT_NODE ) @sync async def test_single_slash(self): await self.page.setContent('
some text
') waitForXPath = self.page.waitForXPath('/html/body/div') self.assertEqual( await self.page.evaluate('x => x.textContent', await waitForXPath), 'some text', ) class TestFrames(BaseTestCase): @sync async def test_frame_nested(self): await self.page.goto(self.url + 'static/nested-frames.html') dumped_frames = dumpFrames(self.page.mainFrame) try: self.assertEqual( dumped_frames, ''' http://localhost:{port}/static/nested-frames.html http://localhost:{port}/static/two-frames.html http://localhost:{port}/static/frame.html http://localhost:{port}/static/frame.html http://localhost:{port}/static/frame.html '''.format(port=self.port).strip() ) except AssertionError: print('\n== Nested frame test failed, which is unstable ==') print(dumpFrames(self.page.mainFrame)) @sync async def test_frame_events(self): await self.page.goto(self.url + 'empty') attachedFrames = [] self.page.on('frameattached', lambda f: attachedFrames.append(f)) await attachFrame(self.page, 'frame1', './static/frame.html') self.assertEqual(len(attachedFrames), 1) self.assertIn('static/frame.html', attachedFrames[0].url) navigatedFrames = [] self.page.on('framenavigated', lambda f: navigatedFrames.append(f)) await navigateFrame(self.page, 'frame1', '/empty') self.assertEqual(len(navigatedFrames), 1) self.assertIn('empty', navigatedFrames[0].url) detachedFrames = [] self.page.on('framedetached', lambda f: detachedFrames.append(f)) await detachFrame(self.page, 'frame1') self.assertEqual(len(detachedFrames), 1) self.assertTrue(detachedFrames[0].isDetached()) @sync async def test_anchor_url(self): await self.page.goto(self.url + 'empty') await asyncio.wait([ self.page.goto(self.url + 'empty#foo'), waitEvent(self.page, 'framenavigated'), ]) self.assertEqual(self.page.url, self.url+'empty#foo') @sync async def test_frame_cross_process(self): await self.page.goto(self.url + 'empty') mainFrame = self.page.mainFrame await self.page.goto('http://127.0.0.1:{}/empty'.format(self.port)) self.assertEqual(self.page.mainFrame, mainFrame) @sync async def test_frame_events_main(self): # no attach/detach events should be emitted on main frame events = [] navigatedFrames = [] self.page.on('frameattached', lambda f: events.append(f)) self.page.on('framedetached', lambda f: events.append(f)) self.page.on('framenavigated', lambda f: navigatedFrames.append(f)) await self.page.goto(self.url + 'empty') self.assertFalse(events) self.assertEqual(len(navigatedFrames), 1) @sync async def test_frame_events_child(self): attachedFrames = [] detachedFrames = [] navigatedFrames = [] self.page.on('frameattached', lambda f: attachedFrames.append(f)) self.page.on('framedetached', lambda f: detachedFrames.append(f)) self.page.on('framenavigated', lambda f: navigatedFrames.append(f)) await self.page.goto(self.url + 'static/nested-frames.html') self.assertEqual(len(attachedFrames), 4) self.assertEqual(len(detachedFrames), 0) self.assertEqual(len(navigatedFrames), 5) attachedFrames.clear() detachedFrames.clear() navigatedFrames.clear() await self.page.goto(self.url + 'empty') self.assertEqual(len(attachedFrames), 0) self.assertEqual(len(detachedFrames), 4) self.assertEqual(len(navigatedFrames), 1) @sync async def test_frame_name(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'FrameId', self.url + 'empty') await asyncio.sleep(0.1) await self.page.evaluate( '''(url) => { const frame = document.createElement('iframe'); frame.name = 'FrameName'; frame.src = url; document.body.appendChild(frame); return new Promise(x => frame.onload = x); }''', self.url + 'empty') await asyncio.sleep(0.1) frame1 = self.page.frames[0] frame2 = self.page.frames[1] frame3 = self.page.frames[2] self.assertEqual(frame1.name, '') self.assertEqual(frame2.name, 'FrameId') self.assertEqual(frame3.name, 'FrameName') @sync async def test_frame_parent(self): await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') await attachFrame(self.page, 'frame2', self.url + 'empty') frame1 = self.page.frames[0] frame2 = self.page.frames[1] frame3 = self.page.frames[2] self.assertEqual(frame1, self.page.mainFrame) self.assertEqual(frame1.parentFrame, None) self.assertEqual(frame2.parentFrame, frame1) self.assertEqual(frame3.parentFrame, frame1) ================================================ FILE: tests/test_input.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from pathlib import Path import sys import unittest from syncer import sync from pyppeteer.errors import PageError, PyppeteerError from .base import BaseTestCase from .frame_utils import attachFrame class TestClick(BaseTestCase): get_dimensions = ''' function () { const rect = document.querySelector('textarea').getBoundingClientRect(); return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; }''' # noqa: E501 @sync async def test_click(self): await self.page.goto(self.url + 'static/button.html') await self.page.click('button') self.assertEqual(await self.page.evaluate('result'), 'Clicked') @sync async def test_click_with_disabled_javascript(self): await self.page.setJavaScriptEnabled(False) await self.page.goto(self.url + 'static/wrappedlink.html') await asyncio.gather( self.page.click('a'), self.page.waitForNavigation(), ) self.assertEqual( self.page.url, self.url + 'static/wrappedlink.html#clicked', ) @sync async def test_click_offscreen_button(self): await self.page.goto(self.url + 'static/offscreenbuttons.html') messages = [] self.page.on('console', lambda msg: messages.append(msg.text)) for i in range(11): await self.page.evaluate('() => window.scrollTo(0, 0)') await self.page.click('#btn{}'.format(i)) self.assertEqual(messages, [ 'button #0 clicked', 'button #1 clicked', 'button #2 clicked', 'button #3 clicked', 'button #4 clicked', 'button #5 clicked', 'button #6 clicked', 'button #7 clicked', 'button #8 clicked', 'button #9 clicked', 'button #10 clicked', ]) @sync async def test_click_wrapped_links(self): await self.page.goto(self.url + 'static/wrappedlink.html') await asyncio.gather( self.page.click('a'), self.page.waitForNavigation(), ) self.assertEqual(self.page.url, self.url + 'static/wrappedlink.html#clicked') @sync async def test_click_events(self): await self.page.goto(self.url + 'static/checkbox.html') self.assertIsNone(await self.page.evaluate('result.check')) await self.page.click('input#agree') self.assertTrue(await self.page.evaluate('result.check')) events = await self.page.evaluate('result.events') self.assertEqual(events, [ 'mouseover', 'mouseenter', 'mousemove', 'mousedown', 'mouseup', 'click', 'input', 'change', ]) await self.page.click('input#agree') self.assertEqual(await self.page.evaluate('result.check'), False) @sync async def test_click_label(self): await self.page.goto(self.url + 'static/checkbox.html') self.assertIsNone(await self.page.evaluate('result.check')) await self.page.click('label[for="agree"]') self.assertTrue(await self.page.evaluate('result.check')) events = await self.page.evaluate('result.events') self.assertEqual(events, [ 'click', 'input', 'change', ]) await self.page.click('label[for="agree"]') self.assertEqual(await self.page.evaluate('result.check'), False) @sync async def test_click_fail(self): await self.page.goto(self.url + 'static/button.html') with self.assertRaises(PageError) as cm: await self.page.click('button.does-not-exist') self.assertEqual( 'No node found for selector: button.does-not-exist', cm.exception.args[0], ) @sync async def test_touch_enabled_viewport(self): await self.page.setViewport({ 'width': 375, 'height': 667, 'deviceScaleFactor': 2, 'isMobile': True, 'hasTouch': True, 'isLandscape': False, }) await self.page.mouse.down() await self.page.mouse.move(100, 10) await self.page.mouse.up() @sync async def test_click_after_navigation(self): await self.page.goto(self.url + 'static/button.html') await self.page.click('button') await self.page.goto(self.url + 'static/button.html') await self.page.click('button') self.assertEqual(await self.page.evaluate('result'), 'Clicked') @sync async def test_resize_textarea(self): await self.page.goto(self.url + 'static/textarea.html') dimensions = await self.page.evaluate(self.get_dimensions) x = dimensions['x'] y = dimensions['y'] width = dimensions['width'] height = dimensions['height'] mouse = self.page.mouse await mouse.move(x + width - 4, y + height - 4) await mouse.down() await mouse.move(x + width + 100, y + height + 100) await mouse.up() new_dimensions = await self.page.evaluate(self.get_dimensions) self.assertEqual(new_dimensions['width'], width + 104) self.assertEqual(new_dimensions['height'], height + 104) @sync async def test_scroll_and_click(self): await self.page.goto(self.url + 'static/scrollable.html') await self.page.click('#button-5') self.assertEqual(await self.page.evaluate( 'document.querySelector("#button-5").textContent'), 'clicked') await self.page.click('#button-80') self.assertEqual(await self.page.evaluate( 'document.querySelector("#button-80").textContent'), 'clicked') @sync async def test_double_click(self): await self.page.goto(self.url + 'static/button.html') await self.page.evaluate('''() => { window.double = false; const button = document.querySelector('button'); button.addEventListener('dblclick', event => { window.double = true; }); }''') button = await self.page.J('button') await button.click(clickCount=2) self.assertTrue(await self.page.evaluate('double')) self.assertEqual(await self.page.evaluate('result'), 'Clicked') @sync async def test_click_partially_obscured_button(self): await self.page.goto(self.url + 'static/button.html') await self.page.evaluate('''() => { const button = document.querySelector('button'); button.textContent = 'Some really long text that will go off screen'; button.style.position = 'absolute'; button.style.left = '368px'; }''') # noqa: 501 await self.page.click('button') self.assertEqual(await self.page.evaluate('result'), 'Clicked') @sync async def test_select_text_by_mouse(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.focus('textarea') text = 'This is the text that we are going to try to select. Let\'s see how it goes.' # noqa: E501 await self.page.keyboard.type(text) await self.page.evaluate( 'document.querySelector("textarea").scrollTop = 0') dimensions = await self.page.evaluate(self.get_dimensions) x = dimensions['x'] y = dimensions['y'] await self.page.mouse.move(x + 2, y + 2) await self.page.mouse.down() await self.page.mouse.move(100, 100) await self.page.mouse.up() self.assertEqual( await self.page.evaluate('window.getSelection().toString()'), text) @sync async def test_select_text_by_triple_click(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.focus('textarea') text = 'This is the text that we are going to try to select. Let\'s see how it goes.' # noqa: E501 await self.page.keyboard.type(text) await self.page.click('textarea') await self.page.click('textarea', clickCount=2) await self.page.click('textarea', clickCount=3) self.assertEqual( await self.page.evaluate('window.getSelection().toString()'), text) @sync async def test_trigger_hover(self): await self.page.goto(self.url + 'static/scrollable.html') await self.page.hover('#button-6') self.assertEqual(await self.page.evaluate( 'document.querySelector("button:hover").id'), 'button-6') await self.page.hover('#button-2') self.assertEqual(await self.page.evaluate( 'document.querySelector("button:hover").id'), 'button-2') await self.page.hover('#button-91') self.assertEqual(await self.page.evaluate( 'document.querySelector("button:hover").id'), 'button-91') @sync async def test_right_click(self): await self.page.goto(self.url + 'static/scrollable.html') await self.page.click('#button-8', button='right') self.assertEqual(await self.page.evaluate( 'document.querySelector("#button-8").textContent'), 'context menu') @sync async def test_click_with_modifier_key(self): await self.page.goto(self.url + 'static/scrollable.html') await self.page.evaluate('() => document.querySelector("#button-3").addEventListener("mousedown", e => window.lastEvent = e, true)') # noqa: E501 modifiers = { 'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey', } for key, value in modifiers.items(): await self.page.keyboard.down(key) await self.page.click('#button-3') self.assertTrue(await self.page.evaluate( 'mod => window.lastEvent[mod]', value)) await self.page.keyboard.up(key) await self.page.click('#button-3') for key, value in modifiers.items(): self.assertFalse(await self.page.evaluate( 'mod => window.lastEvent[mod]', value)) @sync async def test_click_link(self): await self.page.setContent( 'empty.html'.format(self.url + 'empty')) await self.page.click('a') @sync async def test_mouse_movement(self): await self.page.mouse.move(100, 100) await self.page.evaluate('''() => { window.result = []; document.addEventListener('mousemove', event => { window.result.push([event.clientX, event.clientY]); }); }''') await self.page.mouse.move(200, 300, steps=5) self.assertEqual(await self.page.evaluate('window.result'), [ [120, 140], [140, 180], [160, 220], [180, 260], [200, 300], ]) @sync async def test_tap_button(self): await self.page.goto(self.url + 'static/button.html') await self.page.tap('button') self.assertEqual(await self.page.evaluate('result'), 'Clicked') @unittest.skipIf(sys.version_info < (3, 6), 'Fails on 3.5') @sync async def test_touches_report(self): await self.page.goto(self.url + 'static/touches.html') button = await self.page.J('button') await button.tap() self.assertEqual(await self.page.evaluate('getResult()'), ['Touchstart: 0', 'Touchend: 0']) @sync async def test_click_insilde_frame(self): await self.page.goto(self.url + 'empty') await self.page.setContent( '
e.files[0].name', input), 'file-to-upload.txt', ) self.assertEqual( await self.page.evaluate('''e => { const reader = new FileReader(); const promise = new Promise(fulfill => reader.onload = fulfill); reader.readAsText(e.files[0]); return promise.then(() => reader.result); }''', input), # noqa: E501 'contents of the file\n', ) class TestType(BaseTestCase): @sync async def test_key_type(self): await self.page.goto(self.url + 'static/textarea.html') textarea = await self.page.J('textarea') text = 'Type in this text!' await textarea.type(text) result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, text) result = await self.page.evaluate('() => result') self.assertEqual(result, text) @sync async def test_key_arrowkey(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.type('textarea', 'Hello World!') for _ in 'World!': await self.page.keyboard.press('ArrowLeft') await self.page.keyboard.type('inserted ') result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, 'Hello inserted World!') await self.page.keyboard.down('Shift') for _ in 'inserted ': await self.page.keyboard.press('ArrowLeft') await self.page.keyboard.up('Shift') await self.page.keyboard.press('Backspace') result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, 'Hello World!') @sync async def test_key_press_element_handle(self): await self.page.goto(self.url + 'static/textarea.html') textarea = await self.page.J('textarea') await textarea.press('a', text='f') result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, 'f') await self.page.evaluate( '() => window.addEventListener("keydown", e => e.preventDefault(), true)' # noqa: E501 ) await textarea.press('a', text='y') self.assertEqual(result, 'f') @sync async def test_key_send_char(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.focus('textarea') await self.page.keyboard.sendCharacter('朝') result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, '朝') await self.page.evaluate( '() => window.addEventListener("keydown", e => e.preventDefault(), true)' # noqa: E501 ) await self.page.keyboard.sendCharacter('a') result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, '朝a') @sync async def test_repeat_shift_key(self): await self.page.goto(self.url + 'static/keyboard.html') keyboard = self.page.keyboard codeForKey = {'Shift': 16, 'Alt': 18, 'Meta': 91, 'Control': 17} for key, code in codeForKey.items(): await keyboard.down(key) self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: {key} {key}Left {code} [{key}]'.format( key=key, code=code), ) await keyboard.down('!') if key == 'Shift': self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: ! Digit1 49 [{key}]\n' 'Keypress: ! Digit1 33 33 33 [{key}]'.format(key=key), ) else: self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: ! Digit1 49 [{key}]'.format(key=key), ) await keyboard.up('!') self.assertEqual( await self.page.evaluate('getResult()'), 'Keyup: ! Digit1 49 [{key}]'.format(key=key), ) await keyboard.up(key) self.assertEqual( await self.page.evaluate('getResult()'), 'Keyup: {key} {key}Left {code} []'.format(key=key, code=code), ) @sync async def test_repeat_multiple_modifiers(self): await self.page.goto(self.url + 'static/keyboard.html') keyboard = self.page.keyboard await keyboard.down('Control') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: Control ControlLeft 17 [Control]', ) await keyboard.down('Meta') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: Meta MetaLeft 91 [Control Meta]', ) await keyboard.down(';') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: ; Semicolon 186 [Control Meta]', ) await keyboard.up(';') self.assertEqual( await self.page.evaluate('getResult()'), 'Keyup: ; Semicolon 186 [Control Meta]', ) await keyboard.up('Control') self.assertEqual( await self.page.evaluate('getResult()'), 'Keyup: Control ControlLeft 17 [Meta]', ) await keyboard.up('Meta') self.assertEqual( await self.page.evaluate('getResult()'), 'Keyup: Meta MetaLeft 91 []', ) @sync async def test_send_proper_code_while_typing(self): await self.page.goto(self.url + 'static/keyboard.html') await self.page.keyboard.type('!') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: ! Digit1 49 []\n' 'Keypress: ! Digit1 33 33 33 []\n' 'Keyup: ! Digit1 49 []' ) await self.page.keyboard.type('^') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: ^ Digit6 54 []\n' 'Keypress: ^ Digit6 94 94 94 []\n' 'Keyup: ^ Digit6 54 []' ) @sync async def test_send_proper_code_while_typing_with_shift(self): await self.page.goto(self.url + 'static/keyboard.html') await self.page.keyboard.down('Shift') await self.page.keyboard.type('~') self.assertEqual( await self.page.evaluate('getResult()'), 'Keydown: Shift ShiftLeft 16 [Shift]\n' 'Keydown: ~ Backquote 192 [Shift]\n' 'Keypress: ~ Backquote 126 126 126 [Shift]\n' 'Keyup: ~ Backquote 192 [Shift]' ) await self.page.keyboard.up('Shift') @sync async def test_not_type_prevent_events(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.focus('textarea') await self.page.evaluate(''' window.addEventListener('keydown', event => { event.stopPropagation(); event.stopImmediatePropagation(); if (event.key === 'l') event.preventDefault(); if (event.key === 'o') Promise.resolve().then(() => event.preventDefault()); }, false);''', force_expr=True) await self.page.keyboard.type('Hello World!') self.assertEqual(await self.page.evaluate('textarea.value'), 'He Wrd!') @sync async def test_key_modifiers(self): keyboard = self.page.keyboard self.assertEqual(keyboard._modifiers, 0) await keyboard.down('Shift') self.assertEqual(keyboard._modifiers, 8) await keyboard.down('Alt') self.assertEqual(keyboard._modifiers, 9) await keyboard.up('Shift') self.assertEqual(keyboard._modifiers, 1) await keyboard.up('Alt') self.assertEqual(keyboard._modifiers, 0) @sync async def test_repeat_properly(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.focus('textarea') await self.page.evaluate( 'document.querySelector("textarea").addEventListener("keydown",' ' e => window.lastEvent = e, true)', force_expr=True, ) await self.page.keyboard.down('a') self.assertFalse(await self.page.evaluate('window.lastEvent.repeat')) await self.page.keyboard.press('a') self.assertTrue(await self.page.evaluate('window.lastEvent.repeat')) await self.page.keyboard.down('b') self.assertFalse(await self.page.evaluate('window.lastEvent.repeat')) await self.page.keyboard.down('b') self.assertTrue(await self.page.evaluate('window.lastEvent.repeat')) await self.page.keyboard.up('a') await self.page.keyboard.down('a') self.assertFalse(await self.page.evaluate('window.lastEvent.repeat')) @sync async def test_key_type_long(self): await self.page.goto(self.url + 'static/textarea.html') textarea = await self.page.J('textarea') text = 'This text is two lines.\\nThis is character 朝.' await textarea.type(text) result = await self.page.evaluate( '() => document.querySelector("textarea").value' ) self.assertEqual(result, text) result = await self.page.evaluate('() => result') self.assertEqual(result, text) @sync async def test_key_location(self): await self.page.goto(self.url + 'static/textarea.html') textarea = await self.page.J('textarea') await self.page.evaluate( '() => window.addEventListener("keydown", e => window.keyLocation = e.location, true)' # noqa: E501 ) await textarea.press('Digit5') self.assertEqual(await self.page.evaluate('keyLocation'), 0) await textarea.press('ControlLeft') self.assertEqual(await self.page.evaluate('keyLocation'), 1) await textarea.press('ControlRight') self.assertEqual(await self.page.evaluate('keyLocation'), 2) await textarea.press('NumpadSubtract') self.assertEqual(await self.page.evaluate('keyLocation'), 3) @sync async def test_key_unknown(self): with self.assertRaises(PyppeteerError): await self.page.keyboard.press('NotARealKey') with self.assertRaises(PyppeteerError): await self.page.keyboard.press('ё') with self.assertRaises(PyppeteerError): await self.page.keyboard.press('😊') @sync async def test_emoji(self): await self.page.goto(self.url + 'static/textarea.html') await self.page.type('textarea', '👹 Tokyo street Japan 🇯🇵') self.assertEqual( await self.page.Jeval('textarea', 'textarea => textarea.value'), '👹 Tokyo street Japan 🇯🇵', ) @sync async def test_emoji_in_iframe(self): await self.page.goto(self.url + 'empty') await attachFrame( self.page, 'emoji-test', self.url + 'static/textarea.html', ) frame = self.page.frames[1] textarea = await frame.J('textarea') await textarea.type('👹 Tokyo street Japan 🇯🇵') self.assertEqual( await frame.Jeval('textarea', 'textarea => textarea.value'), '👹 Tokyo street Japan 🇯🇵', ) ================================================ FILE: tests/test_launcher.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from copy import deepcopy import glob import logging import os import shutil import subprocess import sys import tempfile import time import unittest from unittest import mock from syncer import sync import websockets from pyppeteer import connect, launch, executablePath, defaultArgs from pyppeteer.chromium_downloader import chromium_executable, current_platform from pyppeteer.errors import NetworkError from pyppeteer.launcher import Launcher from pyppeteer.util import get_free_port from .base import DEFAULT_OPTIONS from .server import get_application class TestLauncher(unittest.TestCase): def setUp(self): self.headless_options = [ '--headless', '--hide-scrollbars', '--mute-audio', ] if current_platform().startswith('win'): self.headless_options.append('--disable-gpu') def check_default_args(self, launcher): for opt in self.headless_options: self.assertIn(opt, launcher.chromeArguments) self.assertTrue(any(opt for opt in launcher.chromeArguments if opt.startswith('--user-data-dir'))) def test_no_option(self): launcher = Launcher() self.check_default_args(launcher) self.assertEqual(launcher.chromeExecutable, str(chromium_executable())) def test_disable_headless(self): launcher = Launcher({'headless': False}) for opt in self.headless_options: self.assertNotIn(opt, launcher.chromeArguments) def test_disable_default_args(self): launcher = Launcher(ignoreDefaultArgs=True) # check default args self.assertNotIn('--no-first-run', launcher.chromeArguments) # check automation args self.assertNotIn('--enable-automation', launcher.chromeArguments) def test_executable(self): launcher = Launcher({'executablePath': '/path/to/chrome'}) self.assertEqual(launcher.chromeExecutable, '/path/to/chrome') def test_args(self): launcher = Launcher({'args': ['--some-args']}) self.check_default_args(launcher) self.assertIn('--some-args', launcher.chromeArguments) def test_filter_ignore_default_args(self): _defaultArgs = defaultArgs() options = deepcopy(DEFAULT_OPTIONS) launcher = Launcher( options, # ignore first and third default arguments ignoreDefaultArgs=[_defaultArgs[0], _defaultArgs[2]], ) self.assertNotIn(_defaultArgs[0], launcher.cmd) self.assertIn(_defaultArgs[1], launcher.cmd) self.assertNotIn(_defaultArgs[2], launcher.cmd) def test_user_data_dir(self): launcher = Launcher({'args': ['--user-data-dir=/path/to/profile']}) self.check_default_args(launcher) self.assertIn('--user-data-dir=/path/to/profile', launcher.chromeArguments) self.assertIsNone(launcher.temporaryUserDataDir) @sync async def test_close_no_connection(self): browser = await launch(args=['--no-sandbox']) await browser.close() @sync async def test_launch(self): browser = await launch(DEFAULT_OPTIONS) await browser.newPage() await browser.close() @unittest.skip('should fix ignoreHTTPSErrors.') @sync async def test_ignore_https_errors(self): browser = await launch(DEFAULT_OPTIONS, ignoreHTTPSErrors=True) page = await browser.newPage() port = get_free_port() time.sleep(0.1) app = get_application() server = app.listen(port) response = await page.goto('https://localhost:{}'.format(port)) self.assertTrue(response.ok) await browser.close() server.stop() @sync async def test_ignore_https_errors_interception(self): browser = await launch(DEFAULT_OPTIONS, ignoreHTTPSErrors=True) page = await browser.newPage() await page.setRequestInterception(True) async def check(req) -> None: await req.continue_() page.on('request', lambda req: asyncio.ensure_future(check(req))) # TODO: should use user-signed cert response = await page.goto('https://google.com/') self.assertIsNotNone(response) self.assertEqual(response.status, 200) @sync async def test_await_after_close(self): browser = await launch(DEFAULT_OPTIONS) page = await browser.newPage() promise = page.evaluate('() => new Promise(r => {})') await browser.close() with self.assertRaises(NetworkError): await promise @sync async def test_invalid_executable_path(self): with self.assertRaises(FileNotFoundError): await launch(DEFAULT_OPTIONS, executablePath='not-a-path') @unittest.skipIf(sys.platform.startswith('win'), 'skip on windows') def test_dumpio_default(self): basedir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(basedir, 'dumpio.py') proc = subprocess.run( [sys.executable, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self.assertNotIn('DUMPIO_TEST', proc.stdout.decode()) self.assertNotIn('DUMPIO_TEST', proc.stderr.decode()) @unittest.skipIf(sys.platform.startswith('win'), 'skip on windows') def test_dumpio_enable(self): basedir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(basedir, 'dumpio.py') proc = subprocess.run( [sys.executable, path, '--dumpio'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # console.log output is sent to stderr self.assertNotIn('DUMPIO_TEST', proc.stdout.decode()) self.assertIn('DUMPIO_TEST', proc.stderr.decode()) @sync async def test_default_viewport(self): options = deepcopy(DEFAULT_OPTIONS) options['defaultViewport'] = { 'width': 456, 'height': 789, } browser = await launch(options) page = await browser.newPage() self.assertEqual(await page.evaluate('window.innerWidth'), 456) self.assertEqual(await page.evaluate('window.innerHeight'), 789) await browser.close() @sync async def test_disable_default_viewport(self): options = deepcopy(DEFAULT_OPTIONS) options['defaultViewport'] = None browser = await launch(options) page = await browser.newPage() self.assertIsNone(page.viewport) await browser.close() class TestDefaultURL(unittest.TestCase): @sync async def test_default_url(self): browser = await launch(DEFAULT_OPTIONS) pages = await browser.pages() url_list = [] for page in pages: url_list.append(page.url) self.assertEqual(url_list, ['about:blank']) await browser.close() @unittest.skipIf('CI' in os.environ, 'Skip in-browser test on CI') @sync async def test_default_url_not_headless(self): options = deepcopy(DEFAULT_OPTIONS) options['headless'] = False browser = await launch(options) pages = await browser.pages() url_list = [] for page in pages: url_list.append(page.url) self.assertEqual(url_list, ['about:blank']) await browser.close() @sync async def test_custom_url(self): customUrl = 'http://example.com/' options = deepcopy(DEFAULT_OPTIONS) options['args'].append(customUrl) browser = await launch(options) pages = await browser.pages() self.assertEqual(len(pages), 1) if pages[0].url != customUrl: await pages[0].waitForNavigation() self.assertEqual(pages[0].url, customUrl) await browser.close() class TestMixedContent(unittest.TestCase): @unittest.skip('need server-side implementation') @sync async def test_mixed_content(self) -> None: options = {'ignoreHTTPSErrors': True} options.update(DEFAULT_OPTIONS) browser = await launch(options) page = await browser.newPage() # page.goto() await page.close() await browser.close() class TestLogLevel(unittest.TestCase): def setUp(self): self.logger = logging.getLogger('pyppeteer') self.mock = mock.Mock() self._orig_stderr = sys.stderr.write sys.stderr.write = self.mock def tearDown(self): sys.stderr.write = self._orig_stderr logging.getLogger('pyppeteer').setLevel(logging.NOTSET) @sync async def test_level_default(self): browser = await launch(args=['--no-sandbox']) await browser.close() self.assertTrue(self.logger.isEnabledFor(logging.WARN)) self.assertFalse(self.logger.isEnabledFor(logging.INFO)) self.assertFalse(self.logger.isEnabledFor(logging.DEBUG)) self.mock.assert_not_called() @unittest.skipIf(current_platform().startswith('win'), 'error on windows') @sync async def test_level_info(self): browser = await launch(args=['--no-sandbox'], logLevel=logging.INFO) await browser.close() self.assertTrue(self.logger.isEnabledFor(logging.WARN)) self.assertTrue(self.logger.isEnabledFor(logging.INFO)) self.assertFalse(self.logger.isEnabledFor(logging.DEBUG)) self.assertIn('listening on', self.mock.call_args_list[0][0][0]) @unittest.skipIf(current_platform().startswith('win'), 'error on windows') @sync async def test_level_debug(self): browser = await launch(args=['--no-sandbox'], logLevel=logging.DEBUG) await browser.close() self.assertTrue(self.logger.isEnabledFor(logging.WARN)) self.assertTrue(self.logger.isEnabledFor(logging.INFO)) self.assertTrue(self.logger.isEnabledFor(logging.DEBUG)) self.assertIn('listening on', self.mock.call_args_list[0][0][0]) if self.mock.call_args_list[1][0][0] == '\n': # python < 3.7.3 self.assertIn('SEND', self.mock.call_args_list[2][0][0]) self.assertIn('RECV', self.mock.call_args_list[4][0][0]) else: self.assertIn('SEND', self.mock.call_args_list[1][0][0]) self.assertIn('RECV', self.mock.call_args_list[2][0][0]) @unittest.skipIf(current_platform().startswith('win'), 'error on windows') @sync async def test_connect_debug(self): browser = await launch(args=['--no-sandbox']) browser2 = await connect( browserWSEndpoint=browser.wsEndpoint, logLevel=logging.DEBUG, ) page = await browser2.newPage() await page.close() await browser2.disconnect() await browser.close() self.assertTrue(self.logger.isEnabledFor(logging.WARN)) self.assertTrue(self.logger.isEnabledFor(logging.INFO)) self.assertTrue(self.logger.isEnabledFor(logging.DEBUG)) self.assertIn('SEND', self.mock.call_args_list[0][0][0]) self.assertIn('RECV', self.mock.call_args_list[2][0][0]) class TestUserDataDir(unittest.TestCase): @classmethod def setUpClass(cls): cls.port = get_free_port() time.sleep(0.1) cls.app = get_application() cls.server = cls.app.listen(cls.port) cls.url = 'http://localhost:{}/'.format(cls.port) def setUp(self): self.datadir = tempfile.mkdtemp() def tearDown(self): if 'CI' not in os.environ: for _ in range(100): shutil.rmtree(self.datadir, ignore_errors=True) if os.path.exists(self.datadir): time.sleep(0.01) else: break else: raise IOError('Unable to remove Temporary User Data') @classmethod def tearDownClass(cls): cls.server.stop() @unittest.skipIf(sys.platform.startswith('cyg'), 'Fails on cygwin') @sync async def test_user_data_dir_option(self): browser = await launch(DEFAULT_OPTIONS, userDataDir=self.datadir) # Open a page to make sure its functional await browser.newPage() self.assertGreater(len(glob.glob(os.path.join(self.datadir, '**'))), 0) await browser.close() self.assertGreater(len(glob.glob(os.path.join(self.datadir, '**'))), 0) @unittest.skipIf(sys.platform.startswith('cyg'), 'Fails on cygwin') @sync async def test_user_data_dir_args(self): options = {} options.update(DEFAULT_OPTIONS) options['args'] = (options['args'] + ['--user-data-dir={}'.format(self.datadir)]) browser = await launch(options) self.assertGreater(len(glob.glob(os.path.join(self.datadir, '**'))), 0) await browser.close() self.assertGreater(len(glob.glob(os.path.join(self.datadir, '**'))), 0) @sync async def test_user_data_dir_restore_state(self): browser = await launch(DEFAULT_OPTIONS, userDataDir=self.datadir) page = await browser.newPage() await page.goto(self.url + 'empty') await page.evaluate('() => localStorage.hey = "hello"') await browser.close() browser2 = await launch(DEFAULT_OPTIONS, userDataDir=self.datadir) page2 = await browser2.newPage() await page2.goto(self.url + 'empty') result = await page2.evaluate('() => localStorage.hey') await browser2.close() self.assertEqual(result, 'hello') @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') @sync async def test_user_data_dir_restore_cookie_in_browser(self): browser = await launch( DEFAULT_OPTIONS, userDataDir=self.datadir, headless=False) page = await browser.newPage() await page.goto(self.url + 'empty') await page.evaluate('() => document.cookie = "foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT"') # noqa: E501 await browser.close() browser2 = await launch(DEFAULT_OPTIONS, userDataDir=self.datadir) page2 = await browser2.newPage() await page2.goto(self.url + 'empty') result = await page2.evaluate('() => document.cookie') await browser2.close() self.assertEqual(result, 'foo=true') class TestTargetEvents(unittest.TestCase): @classmethod def setUpClass(cls): cls.port = get_free_port() time.sleep(0.1) cls.app = get_application() cls.server = cls.app.listen(cls.port) cls.url = 'http://localhost:{}/'.format(cls.port) @classmethod def tearDownClass(cls): cls.server.stop() @sync async def test_target_events(self): browser = await launch(DEFAULT_OPTIONS) events = list() browser.on('targetcreated', lambda _: events.append('CREATED')) browser.on('targetchanged', lambda _: events.append('CHANGED')) browser.on('targetdestroyed', lambda _: events.append('DESTROYED')) page = await browser.newPage() await page.goto(self.url + 'empty') await page.close() self.assertEqual(['CREATED', 'CHANGED', 'DESTROYED'], events) await browser.close() class TestClose(unittest.TestCase): @sync async def test_close(self): curdir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(curdir, 'closeme.py') proc = subprocess.run( [sys.executable, path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) self.assertEqual(proc.returncode, 0) wsEndPoint = proc.stdout.decode() # chrome should be already closed, so fail to connect websocket with self.assertRaises(OSError): await websockets.client.connect(wsEndPoint) class TestEventLoop(unittest.TestCase): def test_event_loop(self): loop = asyncio.new_event_loop() async def inner(_loop) -> None: browser = await launch(args=['--no-sandbox'], loop=_loop) page = await browser.newPage() await page.goto('http://example.com') result = await page.evaluate('() => 1 + 2') self.assertEqual(result, 3) await page.close() await browser.close() loop.run_until_complete(inner(loop)) class TestConnect(unittest.TestCase): @sync async def test_connect(self): browser = await launch(DEFAULT_OPTIONS) browser2 = await connect(browserWSEndpoint=browser.wsEndpoint) page = await browser2.newPage() self.assertEqual(await page.evaluate('() => 7 * 8'), 56) await browser2.disconnect() page2 = await browser.newPage() self.assertEqual(await page2.evaluate('() => 7 * 6'), 42) await browser.close() @sync async def test_reconnect(self): browser = await launch(DEFAULT_OPTIONS) browserWSEndpoint = browser.wsEndpoint await browser.disconnect() browser2 = await connect(browserWSEndpoint=browserWSEndpoint) page = await browser2.newPage() self.assertEqual(await page.evaluate('() => 7 * 8'), 56) await browser.close() @unittest.skip('This test hangs') @sync async def test_fail_to_connect_closed_chrome(self): browser = await launch(DEFAULT_OPTIONS) browserWSEndpoint = browser.wsEndpoint await browser.close() with self.assertRaises(Exception): await connect(browserWSEndpoint=browserWSEndpoint) @sync async def test_executable_path(self): self.assertTrue(os.path.exists(executablePath())) ================================================ FILE: tests/test_misc.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging import unittest import pyppeteer from pyppeteer.helper import debugError, get_positive_int from pyppeteer.page import convertPrintParameterToInches class TestVersion(unittest.TestCase): def test_version(self): version = pyppeteer.version self.assertTrue(isinstance(version, str)) self.assertEqual(version.count('.'), 2) class TestDefaultArgs(unittest.TestCase): def test_default_args(self): self.assertIn('--no-first-run', pyppeteer.defaultArgs()) self.assertIn('--headless', pyppeteer.defaultArgs()) self.assertNotIn('--headless', pyppeteer.defaultArgs({'headless': False})) # noqa: E501 self.assertIn('--user-data-dir=foo', pyppeteer.defaultArgs(userDataDir='foo')) # noqa: E501 class TestToInches(unittest.TestCase): def test_px(self): self.assertEqual( convertPrintParameterToInches('12px'), 12.0 / 96, ) def test_inch(self): self.assertAlmostEqual( convertPrintParameterToInches('12in'), 12.0, ) def test_cm(self): self.assertAlmostEqual( convertPrintParameterToInches('12cm'), 12.0 * 37.8 / 96, ) def test_mm(self): self.assertAlmostEqual( convertPrintParameterToInches('12mm'), 12.0 * 3.78 / 96, ) class TestPositiveInt(unittest.TestCase): def test_badtype(self): with self.assertRaises(TypeError): get_positive_int({'a': 'b'}, 'a') def test_negative_int(self): with self.assertRaises(ValueError): get_positive_int({'a': -1}, 'a') class TestDebugError(unittest.TestCase): def setUp(self): self._old_debug = pyppeteer.DEBUG self.logger = logging.getLogger('pyppeteer.test') def tearDown(self): pyppeteer.DEBUG = self._old_debug def test_debug_default(self): with self.assertLogs('pyppeteer.test', logging.DEBUG): debugError(self.logger, 'test') with self.assertRaises(AssertionError): with self.assertLogs('pyppeteer', logging.INFO): debugError(self.logger, 'test') def test_debug_enabled(self): pyppeteer.DEBUG = True with self.assertLogs('pyppeteer.test', logging.ERROR): debugError(self.logger, 'test') def test_debug_enable_disable(self): pyppeteer.DEBUG = True with self.assertLogs('pyppeteer.test', logging.ERROR): debugError(self.logger, 'test') pyppeteer.DEBUG = False with self.assertLogs('pyppeteer.test', logging.DEBUG): debugError(self.logger, 'test') with self.assertRaises(AssertionError): with self.assertLogs('pyppeteer.test', logging.INFO): debugError(self.logger, 'test') def test_debug_logger(self): with self.assertRaises(AssertionError): with self.assertLogs('pyppeteer', logging.DEBUG): debugError(logging.getLogger('test'), 'test message') ================================================ FILE: tests/test_network.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from pathlib import Path import sys import unittest from syncer import sync from pyppeteer.errors import NetworkError, PageError from .base import BaseTestCase class TestNetworkEvent(BaseTestCase): @sync async def test_request(self): requests = [] self.page.on('request', lambda req: requests.append(req)) await self.page.goto(self.url + 'empty') self.assertEqual(len(requests), 1) req = requests[0] self.assertEqual(req.url, self.url + 'empty') self.assertEqual(req.resourceType, 'document') self.assertEqual(req.method, 'GET') self.assertTrue(req.response) self.assertEqual(req.frame, self.page.mainFrame) self.assertEqual(req.frame.url, self.url + 'empty') @sync async def test_request_post(self): await self.page.goto(self.url + 'empty') from tornado.web import RequestHandler class PostHandler(RequestHandler): def post(self): self.write('') self.app.add_handlers('localhost', [('/post', PostHandler)]) requests = [] self.page.on('request', lambda req: requests.append(req)) await self.page.evaluate('fetch("/post", {method: "POST", body: JSON.stringify({foo: "bar"})})') # noqa: E501 self.assertEqual(len(requests), 1) req = requests[0] self.assertTrue(req) self.assertEqual(req.postData, '{"foo":"bar"}') @sync async def test_response(self): responses = [] self.page.on('response', lambda res: responses.append(res)) await self.page.goto(self.url + 'empty') self.assertEqual(len(responses), 1) response = responses[0] self.assertEqual(response.url, self.url + 'empty') self.assertEqual(response.status, 200) # self.assertTrue(response.ok) self.assertFalse(response.fromCache) self.assertFalse(response.fromServiceWorker) self.assertTrue(response.request) self.assertEqual(response.securityDetails, {}) @sync async def test_response_https(self): responses = [] self.page.on('response', lambda res: responses.append(res)) await self.page.goto('https://example.com/') self.assertEqual(len(responses), 1) response = responses[0] self.assertEqual(response.url, 'https://example.com/') self.assertEqual(response.status, 200) self.assertTrue(response.ok) self.assertFalse(response.fromCache) self.assertFalse(response.fromServiceWorker) self.assertTrue(response.request) self.assertTrue(response.securityDetails) self.assertEqual(response.securityDetails.protocol, 'TLS 1.2') @sync async def test_from_cache(self): responses = {} def set_response(resp): basename = resp.url.split('/').pop() responses[basename] = resp self.page.on('response', set_response) await self.page.goto(self.url + 'static/cached/one-style.html') await self.page.reload() self.assertEqual(len(responses), 2) self.assertEqual(responses['one-style.html'].status, 304) self.assertFalse(responses['one-style.html'].fromCache) self.assertEqual(responses['one-style.css'].status, 200) self.assertTrue(responses['one-style.css'].fromCache) @sync async def test_response_from_service_worker(self): responses = {} def set_response(resp): basename = resp.url.split('/').pop() responses[basename] = resp self.page.on('response', set_response) await self.page.goto( self.url + 'static/serviceworkers/fetch/sw.html', waitUntil='networkidle2', ) await self.page.evaluate('async() => await window.activationPromise') await self.page.reload() self.assertEqual(len(responses), 2) self.assertEqual(responses['sw.html'].status, 200) self.assertTrue(responses['sw.html'].fromServiceWorker) self.assertEqual(responses['style.css'].status, 200) self.assertTrue(responses['style.css'].fromServiceWorker) @unittest.skipIf(sys.platform.startswith('msys'), 'Fails on MSYS') @sync async def test_response_body(self): responses = [] self.page.on('response', lambda res: responses.append(res)) await self.page.goto(self.url + 'static/simple.json') self.assertEqual(len(responses), 1) res = responses[0] self.assertTrue(res) self.assertEqual(await res.text(), '{"foo": "bar"}\n') self.assertEqual(await res.json(), {'foo': 'bar'}) @sync async def test_fail_get_redirected_body(self): response = await self.page.goto(self.url + 'redirect1') redirectChain = response.request.redirectChain self.assertEqual(len(redirectChain), 1) redirected = redirectChain[0].response self.assertEqual(redirected.status, 302) with self.assertRaises(NetworkError) as cm: await redirected.text() self.assertIn( 'Response body is unavailable for redirect response', cm.exception.args[0], ) @unittest.skip('This test hangs') @sync async def test_not_report_body_unless_finished(self): await self.page.goto(self.url + 'empty') serverResponses = [] from tornado.web import RequestHandler class GetHandler(RequestHandler): def get(self): serverResponses.append(self) self.write('hello ') self.app.add_handlers('localhost', [('/get', GetHandler)]) pageResponse = asyncio.get_event_loop().create_future() finishedRequests = [] self.page.on('response', lambda res: pageResponse.set_result(res)) self.page.on('requestfinished', lambda: finishedRequests.append(True)) asyncio.ensure_future( self.page.evaluate('fetch("./get", {method: "GET"})')) response = await pageResponse self.assertTrue(serverResponses) self.assertTrue(response) self.assertEqual(response.status, 200) self.assertFalse(finishedRequests) responseText = response.text() serverResponses[0].write('wor') serverResponses[0].finish('ld!') self.assertEqual(await responseText, 'hello world!') @sync async def test_request_failed(self): await self.page.setRequestInterception(True) async def interception(req): if req.url.endswith('css'): await req.abort() else: await req.continue_() self.page.on( 'request', lambda req: asyncio.ensure_future(interception(req))) failedRequests = [] self.page.on('requestfailed', lambda req: failedRequests.append(req)) await self.page.goto(self.url + 'static/one-style.html') self.assertEqual(len(failedRequests), 1) self.assertIn('one-style.css', failedRequests[0].url) self.assertIsNone(failedRequests[0].response) self.assertEqual(failedRequests[0].resourceType, 'stylesheet') self.assertEqual( failedRequests[0].failure()['errorText'], 'net::ERR_FAILED') self.assertTrue(failedRequests[0].frame) @sync async def test_request_finished(self): requests = [] self.page.on('requestfinished', lambda req: requests.append(req)) await self.page.goto(self.url + 'empty') self.assertEqual(len(requests), 1) req = requests[0] self.assertEqual(req.url, self.url + 'empty') self.assertTrue(req.response) self.assertEqual(req.frame, self.page.mainFrame) self.assertEqual(req.frame.url, self.url + 'empty') @sync async def test_events_order(self): events = [] self.page.on('request', lambda req: events.append('request')) self.page.on('response', lambda res: events.append('response')) self.page.on( 'requestfinished', lambda req: events.append('requestfinished')) await self.page.goto(self.url + 'empty') self.assertEqual(events, ['request', 'response', 'requestfinished']) @sync async def test_redirects(self): events = [] self.page.on('request', lambda req: events.append( '{} {}'.format(req.method, req.url))) self.page.on('response', lambda res: events.append( '{} {}'.format(res.status, res.url))) self.page.on('requestfinished', lambda req: events.append( 'DONE {}'.format(req.url))) self.page.on('requestfailed', lambda req: events.append( 'FAIL {}'.format(req.url))) response = await self.page.goto(self.url + 'redirect1') self.assertEqual(events, [ 'GET {}'.format(self.url + 'redirect1'), '302 {}'.format(self.url + 'redirect1'), 'DONE {}'.format(self.url + 'redirect1'), 'GET {}'.format(self.url + 'redirect2'), '200 {}'.format(self.url + 'redirect2'), 'DONE {}'.format(self.url + 'redirect2'), ]) # check redirect chain redirectChain = response.request.redirectChain self.assertEqual(len(redirectChain), 1) self.assertIn('redirect1', redirectChain[0].url) class TestRequestInterception(BaseTestCase): @sync async def test_request_interception(self): await self.page.setRequestInterception(True) async def request_check(req): self.assertIn('empty', req.url) self.assertTrue(req.headers.get('user-agent')) self.assertEqual(req.method, 'GET') self.assertIsNone(req.postData) self.assertTrue(req.isNavigationRequest()) self.assertEqual(req.resourceType, 'document') self.assertEqual(req.frame, self.page.mainFrame) self.assertEqual(req.frame.url, 'about:blank') await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) res = await self.page.goto(self.url + 'empty') self.assertEqual(res.status, 200) @sync async def test_referer_header(self): await self.page.setRequestInterception(True) requests = list() async def set_request(req): requests.append(req) await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(set_request(req))) await self.page.goto(self.url + 'static/one-style.html') self.assertIn('/one-style.css', requests[1].url) self.assertIn('/one-style.html', requests[1].headers['referer']) @sync async def test_response_with_cookie(self): await self.page.goto(self.url + 'empty') await self.page.setCookie({'name': 'foo', 'value': 'bar'}) await self.page.setRequestInterception(True) async def continue_(req): await req.continue_() self.page.on('request', lambda r: asyncio.ensure_future(continue_(r))) response = await self.page.reload() self.assertEqual(response.status, 200) @sync async def test_request_interception_stop(self): await self.page.setRequestInterception(True) self.page.once('request', lambda req: asyncio.ensure_future(req.continue_())) await self.page.goto(self.url + 'empty') await self.page.setRequestInterception(False) await self.page.goto(self.url + 'empty') @sync async def test_request_interception_custom_header(self): await self.page.setExtraHTTPHeaders({'foo': 'bar'}) await self.page.setRequestInterception(True) async def request_check(req): self.assertEqual(req.headers['foo'], 'bar') await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) res = await self.page.goto(self.url + 'empty') self.assertEqual(res.status, 200) @sync async def test_request_interception_custom_referer_header(self): await self.page.goto(self.url + 'empty') await self.page.setExtraHTTPHeaders({'referer': self.url + 'empty'}) await self.page.setRequestInterception(True) async def request_check(req): self.assertEqual(req.headers['referer'], self.url + 'empty') await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) res = await self.page.goto(self.url + 'empty') self.assertEqual(res.status, 200) @sync async def test_request_interception_abort(self): await self.page.setRequestInterception(True) async def request_check(req): if req.url.endswith('.css'): await req.abort() else: await req.continue_() failedRequests = [] self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) self.page.on('requestfailed', lambda e: failedRequests.append(e)) res = await self.page.goto(self.url + 'static/one-style.html') self.assertTrue(res.ok) self.assertIsNone(res.request.failure()) self.assertEqual(len(failedRequests), 1) @sync async def test_request_interception_custom_error_code(self): await self.page.setRequestInterception(True) async def request_check(req): await req.abort('internetdisconnected') self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) failedRequests = [] self.page.on('requestfailed', lambda req: failedRequests.append(req)) with self.assertRaises(PageError): await self.page.goto(self.url + 'empty') self.assertEqual(len(failedRequests), 1) failedRequest = failedRequests[0] self.assertEqual( failedRequest.failure()['errorText'], 'net::ERR_INTERNET_DISCONNECTED', ) @unittest.skip('Need server-side implementation') @sync async def test_request_interception_amend_http_header(self): pass @sync async def test_request_interception_abort_main(self): await self.page.setRequestInterception(True) async def request_check(req): await req.abort() self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) with self.assertRaises(PageError) as cm: await self.page.goto(self.url + 'empty') self.assertIn('net::ERR_FAILED', cm.exception.args[0]) @sync async def test_request_interception_redirects(self): await self.page.setRequestInterception(True) requests = [] async def check(req): await req.continue_() requests.append(req) self.page.on('request', lambda req: asyncio.ensure_future(check(req))) response = await self.page.goto(self.url + 'redirect1') self.assertEqual(response.status, 200) @sync async def test_redirect_for_subresource(self): await self.page.setRequestInterception(True) requests = list() async def check(req): await req.continue_() requests.append(req) self.page.on('request', lambda req: asyncio.ensure_future(check(req))) response = await self.page.goto(self.url + 'one-style.html') self.assertEqual(response.status, 200) self.assertIn('one-style.html', response.url) self.assertEqual(len(requests), 5) self.assertEqual(requests[0].resourceType, 'document') self.assertEqual(requests[1].resourceType, 'stylesheet') # check redirect chain redirectChain = requests[1].redirectChain self.assertEqual(len(redirectChain), 3) self.assertIn('/one-style.css', redirectChain[0].url) self.assertIn('/three-style.css', redirectChain[2].url) @unittest.skip('This test is not implemented') @sync async def test_request_interception_abort_redirects(self): pass @unittest.skip('This test is not implemented') @sync async def test_request_interception_equal_requests(self): pass @sync async def test_request_interception_data_url(self): await self.page.setRequestInterception(True) requests = [] async def check(req): requests.append(req) await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(check(req))) dataURL = 'data:text/html,
yo
' response = await self.page.goto(dataURL) self.assertEqual(response.status, 200) self.assertEqual(len(requests), 1) self.assertEqual(requests[0].url, dataURL) @sync async def test_request_interception_abort_data_url(self): await self.page.setRequestInterception(True) async def request_check(req): await req.abort() self.page.on('request', lambda req: asyncio.ensure_future(request_check(req))) with self.assertRaises(PageError) as cm: await self.page.goto('data:text/html,No way!') self.assertIn('net::ERR_FAILED', cm.exception.args[0]) @sync async def test_request_interception_with_hash(self): await self.page.setRequestInterception(True) requests = [] async def check(req): requests.append(req) await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(check(req))) response = await self.page.goto(self.url + 'empty#hash') self.assertEqual(response.status, 200) self.assertEqual(response.url, self.url + 'empty') self.assertEqual(len(requests), 1) self.assertEqual(requests[0].url, self.url + 'empty') @sync async def test_request_interception_encoded_server(self): await self.page.setRequestInterception(True) async def check(req): await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(check(req))) response = await self.page.goto(self.url + 'non existing page') self.assertEqual(response.status, 404) @unittest.skip('Need server-side implementation') @sync async def test_request_interception_badly_encoded_server(self): pass @unittest.skip('Need server-side implementation') @sync async def test_request_interception_encoded_server_2(self): pass @unittest.skip('This test is not implemented') @sync async def test_request_interception_invalid_interception_id(self): pass @sync async def test_request_interception_disabled(self): error = None async def check(req): try: await req.continue_() except Exception as e: nonlocal error error = e self.page.on('request', lambda req: asyncio.ensure_future(check(req))) await self.page.goto(self.url + 'empty') self.assertIsNotNone(error) self.assertIn('Request interception is not enabled', error.args[0]) @sync async def test_request_interception_with_file_url(self): await self.page.setRequestInterception(True) urls = [] async def set_urls(req): urls.append(req.url.split('/').pop()) await req.continue_() self.page.on( 'request', lambda req: asyncio.ensure_future(set_urls(req))) def pathToFileURL(path: Path): pathName = str(path).replace('\\', '/') if not pathName.startswith('/'): pathName = '/{}'.format(pathName) return 'file://{}'.format(pathName) target = Path(__file__).parent / 'static' / 'one-style.html' await self.page.goto(pathToFileURL(target)) self.assertEqual(len(urls), 2) self.assertIn('one-style.html', urls) self.assertIn('one-style.css', urls) @sync async def test_request_respond(self): await self.page.setRequestInterception(True) async def interception(req): await req.respond({ 'status': 201, 'headers': {'foo': 'bar'}, 'body': 'intercepted', }) self.page.on( 'request', lambda req: asyncio.ensure_future(interception(req))) response = await self.page.goto(self.url + 'empty') self.assertEqual(response.status, 201) self.assertEqual(response.headers['foo'], 'bar') body = await self.page.evaluate('() => document.body.textContent') self.assertEqual(body, 'intercepted') @unittest.skip('Sending binary object is not implemented') @sync async def test_request_respond_bytes(self): pass class TestNavigationRequest(BaseTestCase): @sync async def test_navigation_request(self): requests = dict() def set_request(req): requests[req.url.split('/').pop()] = req self.page.on('request', set_request) await self.page.goto(self.url + 'redirect3') self.assertTrue(requests['redirect3'].isNavigationRequest()) self.assertTrue(requests['one-frame.html'].isNavigationRequest()) self.assertTrue(requests['frame.html'].isNavigationRequest()) self.assertFalse(requests['script.js'].isNavigationRequest()) self.assertFalse(requests['style.css'].isNavigationRequest()) @sync async def test_interception(self): requests = dict() async def on_request(req): requests[req.url.split('/').pop()] = req await req.continue_() self.page.on('request', lambda req: asyncio.ensure_future(on_request(req))) await self.page.setRequestInterception(True) await self.page.goto(self.url + 'redirect3') self.assertTrue(requests['redirect3'].isNavigationRequest()) self.assertTrue(requests['one-frame.html'].isNavigationRequest()) self.assertTrue(requests['frame.html'].isNavigationRequest()) self.assertFalse(requests['script.js'].isNavigationRequest()) self.assertFalse(requests['style.css'].isNavigationRequest()) @sync async def test_image(self): requests = [] self.page.on('request', lambda req: requests.append(req)) await self.page.goto(self.url + 'static/huge-image.png') self.assertEqual(len(requests), 1) self.assertTrue(requests[0].isNavigationRequest()) ================================================ FILE: tests/test_page.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio import math import os from pathlib import Path import sys import time import unittest from syncer import sync from pyppeteer.errors import ElementHandleError, NetworkError, PageError from pyppeteer.errors import TimeoutError from .base import BaseTestCase from .frame_utils import attachFrame from .utils import waitEvent iPhone = { 'name': 'iPhone 6', 'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', # noqa: E501 'viewport': { 'width': 375, 'height': 667, 'deviceScaleFactor': 2, 'isMobile': True, 'hasTouch': True, 'isLandscape': False, } } class TestEvaluate(BaseTestCase): @sync async def test_evaluate(self): result = await self.page.evaluate('() => 7 * 3') self.assertEqual(result, 21) @sync async def test_await_promise(self): result = await self.page.evaluate('() => Promise.resolve(8 * 7)') self.assertEqual(result, 56) @sync async def test_error_on_reload(self): with self.assertRaises(Exception) as cm: await self.page.evaluate('''() => { location.reload(); return new Promise(resolve => { setTimeout(() => resolve(1), 0); } )}''') self.assertIn('Protocol error', cm.exception.args[0]) @sync async def test_after_framenavigation(self): frameEvaluation = asyncio.get_event_loop().create_future() async def evaluate_frame(frame): frameEvaluation.set_result(await frame.evaluate('() => 6 * 7')) self.page.on( 'framenavigated', lambda frame: asyncio.ensure_future(evaluate_frame(frame)), ) await self.page.goto(self.url + 'empty') await frameEvaluation self.assertEqual(frameEvaluation.result(), 42) @unittest.skip('Pyppeteer does not support async for exposeFunction') @sync async def test_inside_expose_function(self): async def callController(a, b): result = await self.page.evaluate('(a, b) => a + b', a, b) return result await self.page.exposeFunction( 'callController', lambda *args: asyncio.ensure_future(callController(*args)) ) result = await self.page.evaluate( 'async function() { return await callController(9, 3); }' ) self.assertEqual(result, 27) @sync async def test_promise_reject(self): with self.assertRaises(ElementHandleError) as cm: await self.page.evaluate('() => not.existing.object.property') self.assertIn('not is not defined', cm.exception.args[0]) @sync async def test_string_as_error_message(self): with self.assertRaises(Exception) as cm: await self.page.evaluate('() => { throw "qwerty"; }') self.assertIn('qwerty', cm.exception.args[0]) @sync async def test_number_as_error_message(self): with self.assertRaises(Exception) as cm: await self.page.evaluate('() => { throw 100500; }') self.assertIn('100500', cm.exception.args[0]) @sync async def test_return_complex_object(self): obj = {'foo': 'bar!'} result = await self.page.evaluate('(a) => a', obj) self.assertIsNot(result, obj) self.assertEqual(result, obj) @sync async def test_return_nan(self): result = await self.page.evaluate('() => NaN') self.assertIsNone(result) @sync async def test_return_minus_zero(self): result = await self.page.evaluate('() => -0') self.assertEqual(result, -0) @sync async def test_return_infinity(self): result = await self.page.evaluate('() => Infinity') self.assertEqual(result, math.inf) @sync async def test_return_infinity_minus(self): result = await self.page.evaluate('() => -Infinity') self.assertEqual(result, -math.inf) @sync async def test_accept_none(self): result = await self.page.evaluate( '(a, b) => Object.is(a, null) && Object.is(b, "foo")', None, 'foo', ) self.assertTrue(result) @sync async def test_serialize_null_field(self): result = await self.page.evaluate('() => ({a: undefined})') self.assertEqual(result, {}) @sync async def test_fail_window_object(self): self.assertIsNone(await self.page.evaluate('() => window')) self.assertIsNone(await self.page.evaluate('() => [Symbol("foo4")]')) @sync async def test_fail_for_circular_object(self): result = await self.page.evaluate('''() => { const a = {}; const b = {a}; a.b = b; return a; }''') self.assertIsNone(result) @sync async def test_accept_string(self): result = await self.page.evaluate('1 + 2') self.assertEqual(result, 3) @sync async def test_evaluate_force_expression(self): result = await self.page.evaluate( '() => null;\n1 + 2;', force_expr=True) self.assertEqual(result, 3) @sync async def test_accept_string_with_semicolon(self): result = await self.page.evaluate('1 + 5;') self.assertEqual(result, 6) @sync async def test_accept_string_with_comments(self): result = await self.page.evaluate('2 + 5;\n// do some math!') self.assertEqual(result, 7) @sync async def test_element_handle_as_argument(self): await self.page.setContent('
42
') element = await self.page.J('section') text = await self.page.evaluate('(e) => e.textContent', element) self.assertEqual(text, '42') @sync async def test_element_handle_disposed(self): await self.page.setContent('
39
') element = await self.page.J('section') self.assertTrue(element) await element.dispose() with self.assertRaises(ElementHandleError) as cm: await self.page.evaluate('(e) => e.textContent', element) self.assertIn('JSHandle is disposed', cm.exception.args[0]) @sync async def test_element_handle_from_other_frame(self): await attachFrame(self.page, 'frame1', self.url + 'empty') body = await self.page.frames[1].J('body') with self.assertRaises(ElementHandleError) as cm: await self.page.evaluate('body => body.innerHTML', body) self.assertIn( 'JSHandles can be evaluated only in the context they were created', cm.exception.args[0], ) @sync async def test_object_handle_as_argument(self): navigator = await self.page.evaluateHandle('() => navigator') self.assertTrue(navigator) text = await self.page.evaluate('(e) => e.userAgent', navigator) self.assertIn('Mozilla', text) @sync async def test_object_handle_to_primitive_value(self): aHandle = await self.page.evaluateHandle('() => 5') isFive = await self.page.evaluate('(e) => Object.is(e, 5)', aHandle) self.assertTrue(isFive) @sync async def test_simulate_user_gesture(self): playAudio = '''function playAudio() { const audio = document.createElement('audio'); audio.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='; return audio.play(); }''' # noqa: E501 await self.page.evaluate(playAudio) await self.page.evaluate('({})()'.format(playAudio), force_expr=True) @sync async def test_nice_error_after_navigation(self): executionContext = await self.page.mainFrame.executionContext() await asyncio.wait([ self.page.waitForNavigation(), executionContext.evaluate('window.location.reload()'), ]) with self.assertRaises(NetworkError) as cm: await executionContext.evaluate('() => null') self.assertIn('navigation', cm.exception.args[0]) class TestOfflineMode(BaseTestCase): @sync async def test_offline_mode(self): await self.page.setOfflineMode(True) with self.assertRaises(PageError): await self.page.goto(self.url) await self.page.setOfflineMode(False) res = await self.page.reload() self.assertEqual(res.status, 200) @sync async def test_emulate_navigator_offline(self): self.assertTrue(await self.page.evaluate('window.navigator.onLine')) await self.page.setOfflineMode(True) self.assertFalse(await self.page.evaluate('window.navigator.onLine')) await self.page.setOfflineMode(False) self.assertTrue(await self.page.evaluate('window.navigator.onLine')) class TestEvaluateHandle(BaseTestCase): @sync async def test_evaluate_handle(self): windowHandle = await self.page.evaluateHandle('() => window') self.assertTrue(windowHandle) class TestWaitFor(BaseTestCase): @sync async def test_wait_for_selector(self): fut = asyncio.ensure_future(self.page.waitFor('div')) fut.add_done_callback(lambda f: self.set_result(True)) await self.page.goto(self.url + 'empty') self.assertFalse(self.result) await self.page.goto(self.url + 'static/grid.html') await fut self.assertTrue(self.result) @sync async def test_wait_for_xpath(self): waitFor = asyncio.ensure_future(self.page.waitFor('//div')) waitFor.add_done_callback(lambda fut: self.set_result(True)) await self.page.goto(self.url + 'empty') self.assertFalse(self.result) await self.page.goto(self.url + 'static/grid.html') await waitFor self.assertTrue(self.result) @sync async def test_single_slash_fail(self): await self.page.setContent('
some text
') with self.assertRaises(Exception): await self.page.waitFor('/html/body/div') @sync async def test_wait_for_timeout(self): start_time = time.perf_counter() fut = asyncio.ensure_future(self.page.waitFor(100)) fut.add_done_callback(lambda f: self.set_result(True)) await fut self.assertGreater(time.perf_counter() - start_time, 0.1) self.assertTrue(self.result) @sync async def test_wait_for_error_type(self): with self.assertRaises(TypeError) as cm: await self.page.waitFor({'a': 1}) self.assertIn('Unsupported target type', cm.exception.args[0]) @sync async def test_wait_for_func_with_args(self): await self.page.waitFor('(arg1, arg2) => arg1 !== arg2', {}, 1, 2) class TestConsole(BaseTestCase): @sync async def test_console_event(self): messages = [] self.page.once('console', lambda m: messages.append(m)) await self.page.evaluate('() => console.log("hello", 5, {foo: "bar"})') await asyncio.sleep(0.01) self.assertEqual(len(messages), 1) msg = messages[0] self.assertEqual(msg.type, 'log') self.assertEqual(msg.text, 'hello 5 JSHandle@object') self.assertEqual(await msg.args[0].jsonValue(), 'hello') self.assertEqual(await msg.args[1].jsonValue(), 5) self.assertEqual(await msg.args[2].jsonValue(), {'foo': 'bar'}) @sync async def test_console_event_many(self): messages = [] self.page.on('console', lambda m: messages.append(m)) await self.page.evaluate(''' // A pair of time/timeEnd generates only one Console API call. console.time('calling console.time'); console.timeEnd('calling console.time'); console.trace('calling console.trace'); console.dir('calling console.dir'); console.warn('calling console.warn'); console.error('calling console.error'); console.log(Promise.resolve('should not wait until resolved!')); ''') await asyncio.sleep(0.1) self.assertEqual( [msg.type for msg in messages], ['timeEnd', 'trace', 'dir', 'warning', 'error', 'log'], ) self.assertIn('calling console.time', messages[0].text) self.assertEqual([msg.text for msg in messages[1:]], [ 'calling console.trace', 'calling console.dir', 'calling console.warn', 'calling console.error', 'JSHandle@promise', ]) @sync async def test_console_window(self): messages = [] self.page.once('console', lambda m: messages.append(m)) await self.page.evaluate('console.error(window);') await asyncio.sleep(0.1) self.assertEqual(len(messages), 1) msg = messages[0] self.assertEqual(msg.text, 'JSHandle@object') @sync async def test_trigger_correct_log(self): await self.page.goto('about:blank') messages = [] self.page.on('console', lambda m: messages.append(m)) asyncio.ensure_future(self.page.evaluate( 'async url => fetch(url).catch(e => {})', self.url + 'empty')) await waitEvent(self.page, 'console') self.assertEqual(len(messages), 1) message = messages[0] self.assertIn('No \'Access-Control-Allow-Origin\'', message.text) self.assertEqual(message.type, 'error') class TestDOMContentLoaded(BaseTestCase): @sync async def test_fired(self): self.page.once('domcontentloaded', self.set_result(True)) self.assertTrue(self.result) class TestMetrics(BaseTestCase): def checkMetrics(self, metrics): metrics_to_check = { 'Timestamp', 'Documents', 'Frames', 'JSEventListeners', 'Nodes', 'LayoutCount', 'RecalcStyleCount', 'LayoutDuration', 'RecalcStyleDuration', 'ScriptDuration', 'TaskDuration', 'JSHeapUsedSize', 'JSHeapTotalSize', } for name, value in metrics.items(): self.assertTrue(name in metrics_to_check) self.assertTrue(value >= 0) metrics_to_check.remove(name) self.assertEqual(len(metrics_to_check), 0) @sync async def test_metrics(self): await self.page.goto('about:blank') metrics = await self.page.metrics() self.checkMetrics(metrics) @sync async def test_metrics_event(self): fut = asyncio.get_event_loop().create_future() self.page.on('metrics', lambda metrics: fut.set_result(metrics)) await self.page.evaluate('() => console.timeStamp("test42")') metrics = await fut self.assertEqual(metrics['title'], 'test42') self.checkMetrics(metrics['metrics']) class TestGoto(BaseTestCase): @sync async def test_get_http(self): response = await self.page.goto('http://example.com/') self.assertEqual(response.status, 200) self.assertEqual(self.page.url, 'http://example.com/') @sync async def test_goto_blank(self): response = await self.page.goto('about:blank') self.assertIsNone(response) @sync async def test_response_when_page_changes_url(self): response = await self.page.goto(self.url + 'static/historyapi.html') self.assertTrue(response) self.assertEqual(response.status, 200) @sync async def test_goto_subframe_204(self): await self.page.goto(self.url + 'static/frame-204.html') @sync async def test_goto_fail_204(self): with self.assertRaises(PageError) as cm: await self.page.goto('http://httpstat.us/204') self.assertIn('net::ERR_ABORTED', cm.exception.args[0]) @sync async def test_goto_documentloaded(self): import logging with self.assertLogs('pyppeteer', logging.WARNING): response = await self.page.goto( self.url + 'empty', waitUntil='documentloaded') self.assertEqual(response.status, 200) @sync async def test_goto_domcontentloaded(self): response = await self.page.goto(self.url + 'empty', waitUntil='domcontentloaded') self.assertEqual(response.status, 200) @unittest.skip('This test should be fixed') @sync async def test_goto_history_api_beforeunload(self): await self.page.goto(self.url + 'empty') await self.page.evaluate('''() => { window.addEventListener( 'beforeunload', () => history.replaceState(null, 'initial', window.location.href), false, ); }''') # noqa: E501 response = await self.page.goto(self.url + 'static/grid.html') self.assertTrue(response) self.assertEqual(response.status, 200) @sync async def test_goto_networkidle(self): with self.assertRaises(ValueError): await self.page.goto(self.url + 'empty', waitUntil='networkidle') @sync async def test_nav_networkidle0(self): response = await self.page.goto(self.url + 'empty', waitUntil='networkidle0') self.assertEqual(response.status, 200) @sync async def test_nav_networkidle2(self): response = await self.page.goto(self.url + 'empty', waitUntil='networkidle2') self.assertEqual(response.status, 200) @sync async def test_goto_bad_url(self): with self.assertRaises(NetworkError): await self.page.goto('asdf') @sync async def test_goto_bad_resource(self): with self.assertRaises(PageError): await self.page.goto('http://localhost:44123/non-existing-url') @sync async def test_timeout(self): with self.assertRaises(TimeoutError): await self.page.goto(self.url + 'long', timeout=1) @sync async def test_timeout_default(self): self.page.setDefaultNavigationTimeout(1) with self.assertRaises(TimeoutError): await self.page.goto(self.url + 'long') @sync async def test_no_timeout(self): await self.page.goto(self.url + 'long', timeout=0) @sync async def test_valid_url(self): response = await self.page.goto(self.url + 'empty') self.assertEqual(response.status, 200) @sync async def test_data_url(self): response = await self.page.goto('data:text/html,hello') self.assertTrue(response.ok) @sync async def test_404(self): response = await self.page.goto(self.url + '/not-found') self.assertFalse(response.ok) self.assertEqual(response.status, 404) @sync async def test_redirect(self): response = await self.page.goto(self.url + 'redirect1') self.assertTrue(response.ok) self.assertEqual(response.url, self.url + 'redirect2') @unittest.skip('This test is not implemented') @sync async def test_wait_for_network_idle(self): pass @sync async def test_data_url_request(self): requests = [] self.page.on('request', lambda req: requests.append(req)) dataURL = 'data:text/html,
yo
' response = await self.page.goto(dataURL) self.assertTrue(response.ok) self.assertEqual(response.status, 200) self.assertEqual(len(requests), 1) self.assertEqual(requests[0].url, dataURL) @sync async def test_url_with_hash(self): requests = [] self.page.on('request', lambda req: requests.append(req)) response = await self.page.goto(self.url + 'empty#hash') self.assertEqual(response.status, 200) self.assertEqual(response.url, self.url + 'empty') self.assertEqual(len(requests), 1) self.assertEqual(requests[0].url, self.url + 'empty') @sync async def test_self_request_page(self): response = await self.page.goto(self.url + 'static/self-request.html') self.assertEqual(response.status, 200) self.assertIn('self-request.html', response.url) @sync async def test_show_url_in_error_message(self): dummy_port = 9000 if '9000' not in self.url else 9001 url = 'http://localhost:{}/test/1.html'.format(dummy_port) with self.assertRaises(PageError) as cm: await self.page.goto(url) self.assertIn(url, cm.exception.args[0]) class TestWaitForNavigation(BaseTestCase): @sync async def test_wait_for_navigatoin(self): await self.page.goto(self.url + 'empty') results = await asyncio.gather( self.page.waitForNavigation(), self.page.evaluate('(url) => window.location.href = url', self.url) ) response = results[0] self.assertEqual(response.status, 200) self.assertEqual(response.url, self.url) @unittest.skip('Need server-side implementation') @sync async def test_both_domcontentloaded_loaded(self): pass @sync async def test_click_anchor_link(self): await self.page.goto(self.url + 'empty') await self.page.setContent('foobar') results = await asyncio.gather( self.page.waitForNavigation(), self.page.click('a'), ) self.assertIsNone(results[0]) self.assertEqual(self.page.url, self.url + 'empty#foobar') @sync async def test_return_nevigated_response_reload(self): await self.page.goto(self.url + 'empty') navPromise = asyncio.ensure_future(self.page.waitForNavigation()) await self.page.reload() response = await navPromise self.assertEqual(response.url, self.url + 'empty') @sync async def test_history_push_state(self): await self.page.goto(self.url + 'empty') await self.page.setContent(''' SPA ''') results = await asyncio.gather( self.page.waitForNavigation(), self.page.click('a'), ) self.assertIsNone(results[0]) self.assertEqual(self.page.url, self.url + 'wow.html') @sync async def test_history_replace_state(self): await self.page.goto(self.url + 'empty') await self.page.setContent(''' SPA ''') results = await asyncio.gather( self.page.waitForNavigation(), self.page.click('a'), ) self.assertIsNone(results[0]) self.assertEqual(self.page.url, self.url + 'replaced.html') @sync async def test_dom_history_back_forward(self): await self.page.goto(self.url + 'empty') await self.page.setContent(''' back forward ''') self.assertEqual(self.page.url, self.url + 'second.html') results_back = await asyncio.gather( self.page.waitForNavigation(), self.page.click('a#back'), ) self.assertIsNone(results_back[0]) self.assertEqual(self.page.url, self.url + 'first.html') results_forward = await asyncio.gather( self.page.waitForNavigation(), self.page.click('a#forward'), ) self.assertIsNone(results_forward[0]) self.assertEqual(self.page.url, self.url + 'second.html') @sync async def test_subframe_issues(self): navigationPromise = asyncio.ensure_future( self.page.goto(self.url + 'static/one-frame.html')) frame = await waitEvent(self.page, 'frameattached') fut = asyncio.get_event_loop().create_future() def is_same_frame(f): if f == frame: fut.set_result(True) self.page.on('framenavigated', is_same_frame) asyncio.ensure_future(frame.evaluate('window.stop()')) await navigationPromise class TestWaitForRequest(BaseTestCase): @sync async def test_wait_for_request(self): await self.page.goto(self.url + 'empty') results = await asyncio.gather( self.page.waitForRequest(self.url + 'static/digits/2.png'), self.page.evaluate('''() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }''') ) request = results[0] self.assertEqual(request.url, self.url + 'static/digits/2.png') @sync async def test_predicate(self): await self.page.goto(self.url + 'empty') def predicate(req): return req.url == self.url + 'static/digits/2.png' results = await asyncio.gather( self.page.waitForRequest(predicate), self.page.evaluate('''() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }''') ) request = results[0] self.assertEqual(request.url, self.url + 'static/digits/2.png') @sync async def test_no_timeout(self): await self.page.goto(self.url + 'empty') results = await asyncio.gather( self.page.waitForRequest( self.url + 'static/digits/2.png', timeout=0, ), self.page.evaluate('''() => setTimeout(() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }, 50)''') ) request = results[0] self.assertEqual(request.url, self.url + 'static/digits/2.png') class TestWaitForResponse(BaseTestCase): @sync async def test_wait_for_response(self): await self.page.goto(self.url + 'empty') results = await asyncio.gather( self.page.waitForResponse(self.url + 'static/digits/2.png'), self.page.evaluate('''() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }''') ) response = results[0] self.assertEqual(response.url, self.url + 'static/digits/2.png') @sync async def test_predicate(self): await self.page.goto(self.url + 'empty') def predicate(response): return response.url == self.url + 'static/digits/2.png' results = await asyncio.gather( self.page.waitForResponse(predicate), self.page.evaluate('''() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }''') ) response = results[0] self.assertEqual(response.url, self.url + 'static/digits/2.png') @sync async def test_no_timeout(self): await self.page.goto(self.url + 'empty') results = await asyncio.gather( self.page.waitForResponse( self.url + 'static/digits/2.png', timeout=0, ), self.page.evaluate('''() => setTimeout(() => { fetch('/static/digits/1.png'); fetch('/static/digits/2.png'); fetch('/static/digits/3.png'); }, 50)''') ) response = results[0] self.assertEqual(response.url, self.url + 'static/digits/2.png') class TestGoBack(BaseTestCase): @sync async def test_back(self): await self.page.goto(self.url + 'empty') await self.page.goto(self.url + 'static/textarea.html') response = await self.page.goBack() self.assertTrue(response.ok) self.assertIn('empty', response.url) response = await self.page.goForward() self.assertTrue(response.ok) self.assertIn('static/textarea.html', response.url) response = await self.page.goForward() self.assertIsNone(response) @sync async def test_history_api(self): await self.page.goto(self.url + 'empty') await self.page.evaluate('''() => { history.pushState({}, '', '/first.html'); history.pushState({}, '', '/second.html'); }''') self.assertEqual(self.page.url, self.url + 'second.html') await self.page.goBack() self.assertEqual(self.page.url, self.url + 'first.html') await self.page.goBack() self.assertEqual(self.page.url, self.url + 'empty') await self.page.goForward() self.assertEqual(self.page.url, self.url + 'first.html') class TestExposeFunction(BaseTestCase): @sync async def test_expose_function(self): await self.page.goto(self.url + 'empty') await self.page.exposeFunction('compute', lambda a, b: a * b) result = await self.page.evaluate('(a, b) => compute(a, b)', 9, 4) self.assertEqual(result, 36) @sync async def test_call_from_evaluate_on_document(self): await self.page.goto(self.url + 'empty') called = list() def woof(): called.append(True) await self.page.exposeFunction('woof', woof) await self.page.evaluateOnNewDocument('() => woof()') await self.page.reload() self.assertTrue(called) @sync async def test_expose_function_other_page(self): await self.page.exposeFunction('compute', lambda a, b: a * b) await self.page.goto(self.url + 'empty') result = await self.page.evaluate('(a, b) => compute(a, b)', 9, 4) self.assertEqual(result, 36) @unittest.skip('Python does not support promise in expose function') @sync async def test_expose_function_return_promise(self): async def compute(a, b): return a * b await self.page.exposeFunction('compute', compute) result = await self.page.evaluate('() => compute(3, 5)') self.assertEqual(result, 15) @sync async def test_expose_function_frames(self): await self.page.exposeFunction('compute', lambda a, b: a * b) await self.page.goto(self.url + 'static/nested-frames.html') frame = self.page.frames[1] result = await frame.evaluate('() => compute(3, 5)') self.assertEqual(result, 15) @sync async def test_expose_function_frames_before_navigation(self): await self.page.goto(self.url + 'static/nested-frames.html') await self.page.exposeFunction('compute', lambda a, b: a * b) frame = self.page.frames[1] result = await frame.evaluate('() => compute(3, 5)') self.assertEqual(result, 15) class TestErrorPage(BaseTestCase): @sync async def test_error_page(self): error = None def check(e): nonlocal error error = e self.page.on('pageerror', check) await self.page.goto(self.url + 'static/error.html') self.assertIsNotNone(error) self.assertIn('Fancy', error.args[0]) class TestRequest(BaseTestCase): @sync async def test_request(self): requests = [] self.page.on('request', lambda req: requests.append(req)) await self.page.goto(self.url + 'empty') await attachFrame(self.page, 'frame1', self.url + 'empty') self.assertEqual(len(requests), 2) self.assertEqual(requests[0].url, self.url + 'empty') self.assertEqual(requests[0].frame, self.page.mainFrame) self.assertEqual(requests[0].frame.url, self.url + 'empty') self.assertEqual(requests[1].url, self.url + 'empty') self.assertEqual(requests[1].frame, self.page.frames[1]) self.assertEqual(requests[1].frame.url, self.url + 'empty') class TestQuerySelector(BaseTestCase): @sync async def test_jeval(self): await self.page.setContent( '
43543
') idAttribute = await self.page.Jeval('section', 'e => e.id') self.assertEqual(idAttribute, 'testAttribute') @sync async def test_jeval_argument(self): await self.page.setContent('
hello
') text = await self.page.Jeval( 'section', '(e, suffix) => e.textContent + suffix', ' world!') self.assertEqual(text, 'hello world!') @sync async def test_jeval_argument_element(self): await self.page.setContent('
hello
world
') divHandle = await self.page.J('div') text = await self.page.Jeval( 'section', '(e, div) => e.textContent + div.textContent', divHandle, ) self.assertEqual(text, 'hello world') @sync async def test_jeval_not_found(self): await self.page.goto(self.url + 'empty') with self.assertRaises(ElementHandleError) as cm: await self.page.Jeval('section', 'e => e.id') self.assertIn( 'failed to find element matching selector "section"', cm.exception.args[0], ) @sync async def test_JJeval(self): await self.page.setContent( '
hello
beautiful
world
') divsCount = await self.page.JJeval('div', 'divs => divs.length') self.assertEqual(divsCount, 3) @sync async def test_query_selector(self): await self.page.setContent('
test
') element = await self.page.J('section') self.assertTrue(element) @unittest.skipIf(sys.version_info < (3, 6), 'Elements order is unstable') @sync async def test_query_selector_all(self): await self.page.setContent('
A

B
') elements = await self.page.JJ('div') self.assertEqual(len(elements), 2) results = [] for e in elements: results.append(await self.page.evaluate('e => e.textContent', e)) self.assertEqual(results, ['A', 'B']) @sync async def test_query_selector_all_not_found(self): await self.page.goto(self.url + 'empty') elements = await self.page.JJ('div') self.assertEqual(len(elements), 0) @sync async def test_xpath(self): await self.page.setContent('
test
') element = await self.page.xpath('/html/body/section') self.assertTrue(element) @sync async def test_xpath_alias(self): await self.page.setContent('
test
') element = await self.page.Jx('/html/body/section') self.assertTrue(element) @sync async def test_xpath_not_found(self): element = await self.page.xpath('/html/body/no-such-tag') self.assertEqual(element, []) @sync async def test_xpath_multiple(self): await self.page.setContent('
') element = await self.page.xpath('/html/body/div') self.assertEqual(len(element), 2) class TestUserAgent(BaseTestCase): @sync async def test_user_agent(self): self.assertIn('Mozilla', await self.page.evaluate( '() => navigator.userAgent')) await self.page.setUserAgent('foobar') await self.page.goto(self.url) self.assertEqual('foobar', await self.page.evaluate( '() => navigator.userAgent')) @sync async def test_user_agent_mobile_emulate(self): await self.page.goto(self.url + 'static/mobile.html') self.assertIn( 'Chrome', await self.page.evaluate('navigator.userAgent')) await self.page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1') # noqa: E501 self.assertIn( 'Safari', await self.page.evaluate('navigator.userAgent')) class TestExtraHTTPHeader(BaseTestCase): @sync async def test_extra_http_header(self): await self.page.setExtraHTTPHeaders({'foo': 'bar'}) from tornado.web import RequestHandler requests = [] class HeaderFetcher(RequestHandler): def get(self): requests.append(self.request) self.write('') self.app.add_handlers('localhost', [('/header', HeaderFetcher)]) await self.page.goto(self.url + 'header') self.assertEqual(len(requests), 1) self.assertEqual(requests[0].headers['foo'], 'bar') @sync async def test_non_string_value(self): with self.assertRaises(TypeError) as e: await self.page.setExtraHTTPHeaders({'foo': 1}) self.assertIn( 'Expected value of header "foo" to be string', e.exception.args[0]) class TestAuthenticate(BaseTestCase): @sync async def test_auth(self): response = await self.page.goto(self.url + 'auth') self.assertEqual(response.status, 401) await self.page.authenticate({'username': 'user', 'password': 'pass'}) response = await self.page.goto(self.url + 'auth') self.assertEqual(response.status, 200) class TestAuthenticateFailed(BaseTestCase): @sync async def test_auth_fail(self): await self.page.authenticate({'username': 'foo', 'password': 'bar'}) response = await self.page.goto(self.url + 'auth') self.assertEqual(response.status, 401) class TestAuthenticateDisable(BaseTestCase): @sync async def test_disable_auth(self): await self.page.authenticate({'username': 'user', 'password': 'pass'}) response = await self.page.goto(self.url + 'auth') self.assertEqual(response.status, 200) await self.page.authenticate(None) response = await self.page.goto( 'http://127.0.0.1:{}/auth'.format(self.port)) self.assertEqual(response.status, 401) class TestSetContent(BaseTestCase): expectedOutput = '
hello
' @sync async def test_set_content(self): await self.page.setContent('
hello
') result = await self.page.content() self.assertEqual(result, self.expectedOutput) @sync async def test_with_doctype(self): doctype = '' await self.page.setContent(doctype + '
hello
') result = await self.page.content() self.assertEqual(result, doctype + self.expectedOutput) @sync async def test_with_html4_doctype(self): doctype = ('') await self.page.setContent(doctype + '
hello
') result = await self.page.content() self.assertEqual(result, doctype + self.expectedOutput) class TestSetBypassCSP(BaseTestCase): @sync async def test_bypass_csp_meta_tag(self): await self.page.goto(self.url + 'static/csp.html') with self.assertRaises(ElementHandleError): await self.page.addScriptTag(content='window.__injected = 42;') self.assertIsNone(await self.page.evaluate('window.__injected')) await self.page.setBypassCSP(True) await self.page.reload() await self.page.addScriptTag(content='window.__injected = 42;') self.assertEqual(await self.page.evaluate('window.__injected'), 42) @sync async def test_bypass_csp_header(self): await self.page.goto(self.url + 'csp') with self.assertRaises(ElementHandleError): await self.page.addScriptTag(content='window.__injected = 42;') self.assertIsNone(await self.page.evaluate('window.__injected')) await self.page.setBypassCSP(True) await self.page.reload() await self.page.addScriptTag(content='window.__injected = 42;') self.assertEqual(await self.page.evaluate('window.__injected'), 42) @sync async def test_bypass_scp_cross_process(self): await self.page.setBypassCSP(True) await self.page.goto(self.url + 'static/csp.html') await self.page.addScriptTag(content='window.__injected = 42;') self.assertEqual(await self.page.evaluate('window.__injected'), 42) await self.page.goto( 'http://127.0.0.1:{}/static/csp.html'.format(self.port)) await self.page.addScriptTag(content='window.__injected = 42;') self.assertEqual(await self.page.evaluate('window.__injected'), 42) class TestAddScriptTag(BaseTestCase): @sync async def test_script_tag_error(self): await self.page.goto(self.url + 'empty') with self.assertRaises(ValueError): await self.page.addScriptTag('/static/injectedfile.js') @sync async def test_script_tag_url(self): await self.page.goto(self.url + 'empty') scriptHandle = await self.page.addScriptTag( url='/static/injectedfile.js') self.assertIsNotNone(scriptHandle.asElement()) self.assertEqual(await self.page.evaluate('__injected'), 42) @sync async def test_script_tag_url_fail(self): await self.page.goto(self.url + 'empty') with self.assertRaises(PageError) as cm: await self.page.addScriptTag({'url': '/nonexistsfile.js'}) self.assertEqual(cm.exception.args[0], 'Loading script from /nonexistsfile.js failed') @sync async def test_script_tag_path(self): curdir = Path(__file__).parent path = str(curdir / 'static' / 'injectedfile.js') await self.page.goto(self.url + 'empty') scriptHanlde = await self.page.addScriptTag(path=path) self.assertIsNotNone(scriptHanlde.asElement()) self.assertEqual(await self.page.evaluate('__injected'), 42) @sync async def test_script_tag_path_source_map(self): curdir = Path(__file__).parent path = str(curdir / 'static' / 'injectedfile.js') await self.page.goto(self.url + 'empty') await self.page.addScriptTag(path=path) result = await self.page.evaluate('__injectedError.stack') self.assertIn(str(Path('static') / 'injectedfile.js'), result) @sync async def test_script_tag_content(self): await self.page.goto(self.url + 'empty') scriptHandle = await self.page.addScriptTag( content='window.__injected = 35;') self.assertIsNotNone(scriptHandle.asElement()) self.assertEqual(await self.page.evaluate('__injected'), 35) @sync async def test_scp_error_content(self): await self.page.goto(self.url + 'static/csp.html') with self.assertRaises(ElementHandleError): await self.page.addScriptTag(content='window.__injected = 35;') @sync async def test_scp_error_url(self): await self.page.goto(self.url + 'static/csp.html') with self.assertRaises(PageError): await self.page.addScriptTag( url='http://127.0.0.1:{}/static/injectedfile.js'.format(self.port) # noqa: E501 ) @sync async def test_module_url(self): await self.page.goto(self.url + 'empty') await self.page.addScriptTag( url='/static/es6/es6import.js', type='module') self.assertEqual(await self.page.evaluate('__es6injected'), 42) @sync async def test_module_path(self): await self.page.goto(self.url + 'empty') curdir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(curdir, 'static', 'es6', 'es6pathimport.js') await self.page.addScriptTag(path=path, type='module') await self.page.waitForFunction('window.__es6injected') self.assertEqual(await self.page.evaluate('__es6injected'), 42) @sync async def test_module_content(self): await self.page.goto(self.url + 'empty') content = ''' import num from '/static/es6/es6module.js'; window.__es6injected = num; ''' await self.page.addScriptTag(content=content, type='module') await self.page.waitForFunction('window.__es6injected') self.assertEqual(await self.page.evaluate('__es6injected'), 42) class TestAddStyleTag(BaseTestCase): @sync async def test_style_tag_error(self): await self.page.goto(self.url + 'empty') with self.assertRaises(ValueError): await self.page.addStyleTag('/static/injectedstyle.css') async def get_bgcolor(self): return await self.page.evaluate('() => window.getComputedStyle(document.querySelector("body")).getPropertyValue("background-color")') # noqa: E501 @sync async def test_style_tag_url(self): await self.page.goto(self.url + 'empty') self.assertEqual(await self.get_bgcolor(), 'rgba(0, 0, 0, 0)') styleHandle = await self.page.addStyleTag(url='/static/injectedstyle.css') # noqa: E501 self.assertIsNotNone(styleHandle.asElement()) self.assertEqual(await self.get_bgcolor(), 'rgb(255, 0, 0)') @sync async def test_style_tag_url_fail(self): await self.page.goto(self.url + 'empty') with self.assertRaises(PageError) as cm: await self.page.addStyleTag(url='/nonexistfile.css') self.assertEqual(cm.exception.args[0], 'Loading style from /nonexistfile.css failed') @sync async def test_style_tag_path(self): curdir = Path(__file__).parent path = str(curdir / 'static' / 'injectedstyle.css') await self.page.goto(self.url + 'empty') self.assertEqual(await self.get_bgcolor(), 'rgba(0, 0, 0, 0)') styleHandle = await self.page.addStyleTag(path=path) self.assertIsNotNone(styleHandle.asElement()) self.assertEqual(await self.get_bgcolor(), 'rgb(255, 0, 0)') @sync async def test_style_tag_path_source_map(self): curdir = Path(__file__).parent path = str(curdir / 'static' / 'injectedstyle.css') await self.page.goto(self.url + 'empty') await self.page.addStyleTag(path=str(path)) styleHandle = await self.page.J('style') styleContent = await self.page.evaluate( 'style => style.innerHTML', styleHandle) self.assertIn(str(Path('static') / 'injectedstyle.css'), styleContent) @sync async def test_style_tag_content(self): await self.page.goto(self.url + 'empty') self.assertEqual(await self.get_bgcolor(), 'rgba(0, 0, 0, 0)') styleHandle = await self.page.addStyleTag(content=' body {background-color: green;}') # noqa: E501 self.assertIsNotNone(styleHandle.asElement()) self.assertEqual(await self.get_bgcolor(), 'rgb(0, 128, 0)') @sync async def test_csp_error_content(self): await self.page.goto(self.url + 'static/csp.html') with self.assertRaises(ElementHandleError): await self.page.addStyleTag( content='body { background-color: green; }') @sync async def test_csp_error_url(self): await self.page.goto(self.url + 'static/csp.html') with self.assertRaises(PageError): await self.page.addStyleTag( url='http://127.0.0.1:{}/static/injectedstyle.css'.format(self.port) # noqa: E501 ) class TestUrl(BaseTestCase): @sync async def test_url(self): await self.page.goto('about:blank') self.assertEqual(self.page.url, 'about:blank') await self.page.goto(self.url + 'empty') self.assertEqual(self.page.url, self.url + 'empty') class TestViewport(BaseTestCase): iPhoneViewport = iPhone['viewport'] @sync async def test_viewport(self): self.assertEqual(self.page.viewport, {'width': 800, 'height': 600}) await self.page.setViewport({'width': 123, 'height': 456}) self.assertEqual(self.page.viewport, {'width': 123, 'height': 456}) @sync async def test_mobile_emulation(self): await self.page.goto(self.url + 'static/mobile.html') self.assertEqual(await self.page.evaluate('window.innerWidth'), 800) await self.page.setViewport(self.iPhoneViewport) self.assertEqual(await self.page.evaluate('window.innerWidth'), 375) await self.page.setViewport({'width': 400, 'height': 300}) self.assertEqual(await self.page.evaluate('window.innerWidth'), 400) @sync async def test_touch_emulation(self): await self.page.goto(self.url + 'static/mobile.html') self.assertFalse(await self.page.evaluate('"ontouchstart" in window')) await self.page.setViewport(self.iPhoneViewport) self.assertTrue(await self.page.evaluate('"ontouchstart" in window')) dispatchTouch = '''() => { let fulfill; const promise = new Promise(x => fulfill = x); window.ontouchstart = function(e) { fulfill('Received touch'); }; window.dispatchEvent(new Event('touchstart')); fulfill('Did not receive touch'); return promise; }''' self.assertEqual( await self.page.evaluate(dispatchTouch), 'Received touch') await self.page.setViewport({'width': 100, 'height': 100}) self.assertFalse(await self.page.evaluate('"ontouchstart" in window')) @sync async def test_detect_by_modernizr(self): await self.page.goto(self.url + 'static/detect-touch.html') self.assertEqual( await self.page.evaluate('document.body.textContent.trim()'), 'NO' ) await self.page.setViewport(self.iPhoneViewport) self.assertEqual( await self.page.evaluate('document.body.textContent.trim()'), 'YES' ) @sync async def test_detect_touch_viewport_touch(self): await self.page.setViewport({'width': 800, 'height': 600, 'hasTouch': True}) # noqa: E501 await self.page.addScriptTag({'url': self.url + 'static/modernizr.js'}) self.assertTrue(await self.page.evaluate('() => Modernizr.touchevents')) # noqa: E501 @sync async def test_landscape_emulation(self): await self.page.goto(self.url + 'static/mobile.html') self.assertEqual( await self.page.evaluate('screen.orientation.type'), 'portrait-primary', ) iPhoneLandscapeViewport = self.iPhoneViewport.copy() iPhoneLandscapeViewport['isLandscape'] = True await self.page.setViewport(iPhoneLandscapeViewport) self.assertEqual( await self.page.evaluate('screen.orientation.type'), 'landscape-primary', ) await self.page.setViewport({'width': 100, 'height': 100}) self.assertEqual( await self.page.evaluate('screen.orientation.type'), 'portrait-primary', ) class TestEmulate(BaseTestCase): @sync async def test_emulate(self): await self.page.goto(self.url + 'static/mobile.html') await self.page.emulate(iPhone) self.assertEqual(await self.page.evaluate('window.innerWidth'), 375) self.assertIn( 'Safari', await self.page.evaluate('navigator.userAgent')) @sync async def test_click(self): await self.page.emulate(iPhone) await self.page.goto(self.url + 'static/button.html') button = await self.page.J('button') await self.page.evaluate( 'button => button.style.marginTop = "200px"', button) await button.click() self.assertEqual(await self.page.evaluate('result'), 'Clicked') class TestEmulateMedia(BaseTestCase): @sync async def test_emulate_media(self): self.assertTrue( await self.page.evaluate('matchMedia("screen").matches')) self.assertFalse( await self.page.evaluate('matchMedia("print").matches')) await self.page.emulateMedia('print') self.assertFalse( await self.page.evaluate('matchMedia("screen").matches')) self.assertTrue( await self.page.evaluate('matchMedia("print").matches')) await self.page.emulateMedia(None) self.assertTrue( await self.page.evaluate('matchMedia("screen").matches')) self.assertFalse( await self.page.evaluate('matchMedia("print").matches')) @sync async def test_emulate_media_bad_arg(self): with self.assertRaises(ValueError) as cm: await self.page.emulateMedia('bad') self.assertEqual(cm.exception.args[0], 'Unsupported media type: bad') class TestJavaScriptEnabled(BaseTestCase): @sync async def test_set_javascript_enabled(self): await self.page.setJavaScriptEnabled(False) await self.page.goto( 'data:text/html, ') with self.assertRaises(ElementHandleError) as cm: await self.page.evaluate('something') self.assertIn('something is not defined', cm.exception.args[0]) await self.page.setJavaScriptEnabled(True) await self.page.goto( 'data:text/html, ') self.assertEqual(await self.page.evaluate('something'), 'forbidden') class TestEvaluateOnNewDocument(BaseTestCase): @sync async def test_evaluate_before_else_on_page(self): await self.page.evaluateOnNewDocument('() => window.injected = 123') await self.page.goto(self.url + 'static/temperable.html') self.assertEqual(await self.page.evaluate('window.result'), 123) @sync async def test_csp(self): await self.page.evaluateOnNewDocument('() => window.injected = 123') await self.page.goto(self.url + 'csp') self.assertEqual(await self.page.evaluate('window.injected'), 123) with self.assertRaises(ElementHandleError): await self.page.addScriptTag(content='window.e = 10;') self.assertIsNone(await self.page.evaluate('window.e')) class TestCacheEnabled(BaseTestCase): @sync async def test_cache_enable_disable(self): responses = {} def set_response(res): responses[res.url.split('/').pop()] = res self.page.on('response', set_response) await self.page.goto(self.url + 'static/cached/one-style.html', waitUntil='networkidle2') await self.page.reload(waitUntil='networkidle2') self.assertTrue(responses.get('one-style.css').fromCache) await self.page.setCacheEnabled(False) await self.page.reload(waitUntil='networkidle2') self.assertFalse(responses.get('one-style.css').fromCache) class TestPDF(BaseTestCase): @sync async def test_pdf(self): outfile = Path(__file__).parent / 'output.pdf' await self.page.pdf({'path': str(outfile)}) self.assertTrue(outfile.is_file()) with outfile.open('rb') as f: pdf = f.read() self.assertGreater(len(pdf), 0) outfile.unlink() class TestTitle(BaseTestCase): @sync async def test_title(self): await self.page.goto(self.url + 'static/button.html') self.assertEqual(await self.page.title(), 'Button test') class TestSelect(BaseTestCase): def setUp(self): super().setUp() sync(self.page.goto(self.url + 'static/select.html')) @sync async def test_select(self): value = await self.page.select('select', 'blue') self.assertEqual(value, ['blue']) _input = await self.page.evaluate('result.onInput') self.assertEqual(_input, ['blue']) change = await self.page.evaluate('result.onChange') self.assertEqual(change, ['blue']) _input = await self.page.evaluate('result.onBubblingInput') self.assertEqual(_input, ['blue']) change = await self.page.evaluate('result.onBubblingChange') self.assertEqual(change, ['blue']) @sync async def test_select_first_item(self): await self.page.select('select', 'blue', 'green', 'red') self.assertEqual(await self.page.evaluate('result.onInput'), ['blue']) self.assertEqual(await self.page.evaluate('result.onChange'), ['blue']) @sync async def test_select_multiple(self): await self.page.evaluate('makeMultiple();') values = await self.page.select('select', 'blue', 'green', 'red') self.assertEqual(values, ['blue', 'green', 'red']) _input = await self.page.evaluate('result.onInput') self.assertEqual(_input, ['blue', 'green', 'red']) change = await self.page.evaluate('result.onChange') self.assertEqual(change, ['blue', 'green', 'red']) @sync async def test_select_not_select_element(self): with self.assertRaises(ElementHandleError): await self.page.select('body', '') @sync async def test_select_no_match(self): values = await self.page.select('select', 'abc', 'def') self.assertEqual(values, []) @sync async def test_return_selected_elements(self): await self.page.evaluate('makeMultiple()') result = await self.page.select('select', 'blue', 'black', 'magenta') self.assertEqual(len(result), 3) self.assertEqual(set(result), {'blue', 'black', 'magenta'}) @sync async def test_select_not_multiple(self): values = await self.page.select('select', 'blue', 'green', 'red') self.assertEqual(len(values), 1) @sync async def test_select_no_value(self): values = await self.page.select('select') self.assertEqual(values, []) @sync async def test_select_deselect(self): await self.page.select('select', 'blue', 'green', 'red') await self.page.select('select') result = await self.page.Jeval( 'select', 'elm => Array.from(elm.options).every(option => !option.selected)' ) self.assertTrue(result) @sync async def test_select_deselect_multiple(self): await self.page.evaluate('makeMultiple();') await self.page.select('select', 'blue', 'green', 'red') await self.page.select('select') result = await self.page.Jeval( 'select', 'elm => Array.from(elm.options).every(option => !option.selected)' ) self.assertTrue(result) @sync async def test_select_nonstring(self): with self.assertRaises(TypeError): await self.page.select('select', 12) class TestCookie(BaseTestCase): @sync async def test_cookies(self): await self.page.goto(self.url) cookies = await self.page.cookies() self.assertEqual(cookies, []) await self.page.evaluate( 'document.cookie = "username=John Doe"' ) cookies = await self.page.cookies() self.assertEqual(cookies, [{ 'name': 'username', 'value': 'John Doe', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 16, 'httpOnly': False, 'secure': False, 'session': True, }]) await self.page.setCookie({'name': 'password', 'value': '123456'}) cookies = await self.page.evaluate( '() => document.cookie' ) self.assertEqual(cookies, 'username=John Doe; password=123456') cookies = await self.page.cookies() self.assertIn(cookies, [ [ { 'name': 'password', 'value': '123456', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 14, 'httpOnly': False, 'secure': False, 'session': True, }, { 'name': 'username', 'value': 'John Doe', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 16, 'httpOnly': False, 'secure': False, 'session': True, } ], [ { 'name': 'username', 'value': 'John Doe', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 16, 'httpOnly': False, 'secure': False, 'session': True, }, { 'name': 'password', 'value': '123456', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 14, 'httpOnly': False, 'secure': False, 'session': True, } ] ]) await self.page.deleteCookie({'name': 'username'}) cookies = await self.page.evaluate( '() => document.cookie' ) self.assertEqual(cookies, 'password=123456') cookies = await self.page.cookies() self.assertEqual(cookies, [{ 'name': 'password', 'value': '123456', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 14, 'httpOnly': False, 'secure': False, 'session': True, }]) @sync async def test_cookie_blank_page(self): await self.page.goto('about:blank') with self.assertRaises(NetworkError): await self.page.setCookie({'name': 'example-cookie', 'value': 'a'}) @sync async def test_cookie_blank_page2(self): with self.assertRaises(PageError): await self.page.setCookie( {'name': 'example-cookie', 'value': 'best'}, {'url': 'about:blank', 'name': 'example-cookie-blank', 'value': 'best'} ) @sync async def test_cookie_data_url_page(self): await self.page.goto('data:,hello') with self.assertRaises(NetworkError): await self.page.setCookie({'name': 'example-cookie', 'value': 'a'}) @sync async def test_cookie_data_url_page2(self): with self.assertRaises(PageError): await self.page.setCookie( {'name': 'example-cookie', 'value': 'best'}, {'url': 'data:,hello', 'name': 'example-cookie-blank', 'value': 'best'} ) class TestCookieWithPath(BaseTestCase): @sync async def test_set_cookie_with_path(self): await self.page.goto(self.url + 'static/grid.html') await self.page.setCookie({ 'name': 'gridcookie', 'value': 'GRID', 'path': '/static/grid.html', }) self.assertEqual(await self.page.cookies(), [{ 'name': 'gridcookie', 'value': 'GRID', 'path': '/static/grid.html', 'domain': 'localhost', 'expires': -1, 'size': 14, 'httpOnly': False, 'secure': False, 'session': True, }]) class TestCookieDelete(BaseTestCase): @sync async def test_delete_cookie(self): await self.page.goto(self.url) await self.page.setCookie({ 'name': 'cookie1', 'value': '1', }, { 'name': 'cookie2', 'value': '2', }, { 'name': 'cookie3', 'value': '3', }) self.assertEqual( await self.page.evaluate('document.cookie'), 'cookie1=1; cookie2=2; cookie3=3' ) await self.page.deleteCookie({'name': 'cookie2'}) self.assertEqual( await self.page.evaluate('document.cookie'), 'cookie1=1; cookie3=3' ) class TestCookieDomain(BaseTestCase): @sync async def test_different_domain(self): await self.page.goto(self.url + 'static/grid.html') await self.page.setCookie({ 'name': 'example-cookie', 'value': 'best', 'url': 'https://www.example.com', }) self.assertEqual(await self.page.evaluate('document.cookie'), '') self.assertEqual(await self.page.cookies(), []) self.assertEqual(await self.page.cookies('https://www.example.com'), [{ 'name': 'example-cookie', 'value': 'best', 'domain': 'www.example.com', 'path': '/', 'expires': -1, 'size': 18, 'httpOnly': False, 'secure': True, 'session': True, }]) class TestCookieFrames(BaseTestCase): @sync async def test_frame(self): await self.page.goto(self.url + 'static/grid.html') await self.page.setCookie({ 'name': 'localhost-cookie', 'value': 'best', }) url_127 = 'http://127.0.0.1:{}'.format(self.port) await self.page.evaluate('''src => { let fulfill; const promise = new Promise(x => fulfill = x); const iframe = document.createElement('iframe'); document.body.appendChild(iframe); iframe.onload = fulfill; iframe.src = src; return promise; }''', url_127) await self.page.setCookie({ 'name': '127-cookie', 'value': 'worst', 'url': url_127, }) self.assertEqual( await self.page.evaluate('document.cookie'), 'localhost-cookie=best', ) self.assertEqual( await self.page.frames[1].evaluate('document.cookie'), '127-cookie=worst', ) self.assertEqual(await self.page.cookies(), [{ 'name': 'localhost-cookie', 'value': 'best', 'domain': 'localhost', 'path': '/', 'expires': -1, 'size': 20, 'httpOnly': False, 'secure': False, 'session': True, }]) self.assertEqual(await self.page.cookies(url_127), [{ 'name': '127-cookie', 'value': 'worst', 'domain': '127.0.0.1', 'path': '/', 'expires': -1, 'size': 15, 'httpOnly': False, 'secure': False, 'session': True, }]) class TestEvents(BaseTestCase): @sync async def test_close_window_close(self): loop = asyncio.get_event_loop() newPagePromise = loop.create_future() async def page_created(target): page = await target.page() newPagePromise.set_result(page) self.context.once( 'targetcreated', lambda target: loop.create_task(page_created(target)), ) await self.page.evaluate( 'window["newPage"] = window.open("about:blank")') newPage = await newPagePromise closedPromise = loop.create_future() newPage.on('close', lambda: closedPromise.set_result(True)) await self.page.evaluate('window["newPage"].close()') await closedPromise @sync async def test_close_page_close(self): newPage = await self.context.newPage() closedPromise = asyncio.get_event_loop().create_future() newPage.on('close', lambda: closedPromise.set_result(True)) await newPage.close() await closedPromise class TestBrowser(BaseTestCase): @sync async def test_get_browser(self): self.assertIs(self.page.browser, self.browser) ================================================ FILE: tests/test_pyppeteer.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ test_pyppeteer ---------------------------------- Tests for `pyppeteer` module. """ import asyncio import logging from pathlib import Path from syncer import sync from .base import BaseTestCase class TestPyppeteer(BaseTestCase): @sync async def test_get_https(self): await self.page.goto('https://example.com/') self.assertEqual(self.page.url, 'https://example.com/') @sync async def test_get_facebook(self): await self.page.goto('https://www.facebook.com/') self.assertEqual(self.page.url, 'https://www.facebook.com/') @sync async def test_plain_text_depr(self): await self.page.goto(self.url) with self.assertLogs('pyppeteer', logging.WARN) as log: text = await self.page.plainText() self.assertIn('deprecated', log.records[0].msg) self.assertEqual(text.split(), ['Hello', 'link1', 'link2']) @sync async def test_inject_file(self): # deprecated tmp_file = Path('tmp.js') with tmp_file.open('w') as f: f.write(''' () => document.body.appendChild(document.createElement("section")) '''.strip()) with self.assertLogs('pyppeteer', logging.WARN) as log: await self.page.injectFile(str(tmp_file)) self.assertIn('deprecated', log.records[0].msg) await self.page.waitForSelector('section') self.assertIsNotNone(await self.page.J('section')) tmp_file.unlink() class TestScreenshot(BaseTestCase): def setUp(self): super().setUp() self.target_path = Path(__file__).resolve().parent / 'test.png' if self.target_path.exists(): self.target_path.unlink() def tearDown(self): if self.target_path.exists(): self.target_path.unlink() super().tearDown() @sync async def test_screenshot_large(self): page = await self.context.newPage() await page.setViewport({ 'width': 2000, 'height': 2000, }) await page.goto(self.url + 'static/huge-page.html') options = {'path': str(self.target_path)} self.assertFalse(self.target_path.exists()) await asyncio.wait_for(page.screenshot(options), 30) self.assertTrue(self.target_path.exists()) with self.target_path.open('rb') as fh: bytes = fh.read() self.assertGreater(len(bytes), 2**20) ================================================ FILE: tests/test_screenshot.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import base64 from pathlib import Path from unittest import TestCase from syncer import sync from pyppeteer import launch root_path = Path(__file__).resolve().parent blank_png_path = root_path / 'blank_800x600.png' blank_pdf_path = root_path / 'blank.pdf' class TestScreenShot(TestCase): def setUp(self): self.browser = sync(launch(args=['--no-sandbox'])) self.target_path = Path(__file__).resolve().parent / 'test.png' if self.target_path.exists(): self.target_path.unlink() def tearDown(self): if self.target_path.exists(): self.target_path.unlink() sync(self.browser.close()) @sync async def test_screenshot(self): page = await self.browser.newPage() await page.goto('about:blank') options = {'path': str(self.target_path)} self.assertFalse(self.target_path.exists()) await page.screenshot(options) self.assertTrue(self.target_path.exists()) with self.target_path.open('rb') as f: result = f.read() with blank_png_path.open('rb') as f: sample = f.read() self.assertEqual(result, sample) @sync async def test_screenshot_binary(self): page = await self.browser.newPage() await page.goto('about:blank') result = await page.screenshot() with blank_png_path.open('rb') as f: sample = f.read() self.assertEqual(result, sample) @sync async def test_screenshot_base64(self): page = await self.browser.newPage() await page.goto('about:blank') options = {'encoding': 'base64'} result = await page.screenshot(options) with blank_png_path.open('rb') as f: sample = f.read() self.assertEqual(base64.b64decode(result), sample) @sync async def test_screenshot_element(self): page = await self.browser.newPage() await page.goto('http://example.com') element = await page.J('h1') options = {'path': str(self.target_path)} self.assertFalse(self.target_path.exists()) await element.screenshot(options) self.assertTrue(self.target_path.exists()) @sync async def test_unresolved_mimetype(self): page = await self.browser.newPage() await page.goto('about:blank') options = {'path': 'example.unsupported'} with self.assertRaises(ValueError, msg='mime type: unsupported'): await page.screenshot(options) class TestPDF(TestCase): def setUp(self): self.browser = sync(launch(args=['--no-sandbox'])) self.target_path = Path(__file__).resolve().parent / 'test.pdf' if self.target_path.exists(): self.target_path.unlink() @sync async def test_pdf(self): page = await self.browser.newPage() await page.goto('about:blank') self.assertFalse(self.target_path.exists()) await page.pdf(path=str(self.target_path)) self.assertTrue(self.target_path.exists()) self.assertTrue(self.target_path.stat().st_size >= 800) def tearDown(self): if self.target_path.exists: self.target_path.unlink() sync(self.browser.close()) ================================================ FILE: tests/test_target.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio import unittest from syncer import sync from .base import BaseTestCase class TestTarget(BaseTestCase): @sync async def test_targets(self): targets = self.browser.targets() _list = [target for target in targets if target.type == 'page' and target.url == 'about:blank'] self.assertTrue(any(_list)) target_types = [t.type for t in targets] self.assertIn('browser', target_types) @sync async def test_return_all_pages(self): pages = await self.context.pages() self.assertEqual(len(pages), 1) self.assertIn(self.page, pages) @sync async def test_browser_target(self): targets = self.browser.targets() browserTarget = [t for t in targets if t.type == 'browser'] self.assertTrue(browserTarget) @sync async def test_default_page(self): pages = await self.browser.pages() page = [page for page in pages if page != self.page][0] self.assertEqual(await page.evaluate('["Hello", "world"].join(" ")'), 'Hello world') self.assertTrue(await page.J('body')) @sync async def test_report_new_page(self): otherPagePromise = asyncio.get_event_loop().create_future() self.context.once('targetcreated', lambda target: otherPagePromise.set_result(target)) await self.page.evaluate( 'url => window.open(url)', 'http://127.0.0.1:{}'.format(self.port)) otherPage = await (await otherPagePromise).page() self.assertIn('127.0.0.1', otherPage.url) self.assertEqual( await otherPage.evaluate('["Hello", "world"].join(" ")'), 'Hello world') self.assertTrue(await otherPage.J('body')) pages = await self.context.pages() self.assertIn(self.page, pages) self.assertIn(otherPage, pages) closePagePromise = asyncio.get_event_loop().create_future() async def get_close_page(target): page = await target.page() closePagePromise.set_result(page) self.context.once('targetdestroyed', lambda t: asyncio.ensure_future(get_close_page(t))) await otherPage.close() self.assertEqual(await closePagePromise, otherPage) pages = await self.context.pages() self.assertIn(self.page, pages) self.assertNotIn(otherPage, pages) @sync async def test_report_service_worker(self): await self.page.goto(self.url + 'empty') createdTargetPromise = asyncio.get_event_loop().create_future() self.context.once('targetcreated', lambda t: createdTargetPromise.set_result(t)) await self.page.goto(self.url + 'static/serviceworkers/empty/sw.html') createdTarget = await createdTargetPromise self.assertEqual(createdTarget.type, 'service_worker') self.assertEqual( createdTarget.url, self.url + 'static/serviceworkers/empty/sw.js') destroyedTargetPromise = asyncio.get_event_loop().create_future() self.context.once('targetdestroyed', lambda t: destroyedTargetPromise.set_result(t)) await self.page.evaluate( '() => window.registrationPromise.then(reg => reg.unregister())') destroyedTarget = await destroyedTargetPromise self.assertEqual(destroyedTarget, createdTarget) @sync async def test_url_change(self): await self.page.goto(self.url + 'empty') changedTargetPromise = asyncio.get_event_loop().create_future() self.context.once('targetchanged', lambda t: changedTargetPromise.set_result(t)) await self.page.goto('http://127.0.0.1:{}/'.format(self.port)) changedTarget = await changedTargetPromise self.assertEqual(changedTarget.url, 'http://127.0.0.1:{}/'.format(self.port)) changedTargetPromise = asyncio.get_event_loop().create_future() self.context.once('targetchanged', lambda t: changedTargetPromise.set_result(t)) await self.page.goto(self.url + 'empty') changedTarget = await changedTargetPromise self.assertEqual(changedTarget.url, self.url + 'empty') @sync async def test_not_report_uninitialized_page(self): changedTargets = [] def listener(target): changedTargets.append(target) self.context.on('targetchanged', listener) targetPromise = asyncio.get_event_loop().create_future() self.context.once('targetcreated', lambda t: targetPromise.set_result(t)) newPagePromise = asyncio.ensure_future(self.context.newPage()) target = await targetPromise self.assertEqual(target.url, 'about:blank') newPage = await newPagePromise targetPromise2 = asyncio.get_event_loop().create_future() self.context.once('targetcreated', lambda t: targetPromise2.set_result(t)) evaluatePromise = asyncio.ensure_future( newPage.evaluate('window.open("about:blank")')) target2 = await targetPromise2 self.assertEqual(target2.url, 'about:blank') await evaluatePromise await newPage.close() self.assertFalse(changedTargets) self.context.remove_listener('targetchanged', listener) # cleanup await (await target2.page()).close() @unittest.skip('Need server-side implementation') @sync async def test_crash_while_redirect(self): pass @sync async def test_opener(self): await self.page.goto(self.url + 'empty') targetPromise = asyncio.get_event_loop().create_future() self.context.once('targetcreated', lambda target: targetPromise.set_result(target)) await self.page.goto(self.url + 'static/popup/window-open.html') createdTarget = await targetPromise self.assertEqual( (await createdTarget.page()).url, self.url + 'static/popup/popup.html', ) self.assertEqual(createdTarget.opener, self.page.target) self.assertIsNone(self.page.target.opener) await (await createdTarget.page()).close() ================================================ FILE: tests/test_tracing.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import json from pathlib import Path import unittest from syncer import sync from pyppeteer.errors import NetworkError from .base import BaseTestCase class TestTracing(BaseTestCase): def setUp(self): self.outfile = Path(__file__).parent / 'trace.json' if self.outfile.is_file(): self.outfile.unlink() super().setUp() def tearDown(self): if self.outfile.is_file(): self.outfile.unlink() super().tearDown() @sync async def test_tracing(self): await self.page.tracing.start({ 'path': str(self.outfile) }) await self.page.goto(self.url) await self.page.tracing.stop() self.assertTrue(self.outfile.is_file()) @sync async def test_custom_categories(self): await self.page.tracing.start({ 'path': str(self.outfile), 'categories': ['disabled-by-default-v8.cpu_profiler.hires'], }) await self.page.tracing.stop() self.assertTrue(self.outfile.is_file()) with self.outfile.open() as f: trace_json = json.load(f) self.assertIn( 'disabled-by-default-v8.cpu_profiler.hires', trace_json['metadata']['trace-config'], ) @sync async def test_tracing_two_page_error(self): await self.page.tracing.start({'path': str(self.outfile)}) new_page = await self.browser.newPage() with self.assertRaises(NetworkError): await new_page.tracing.start({'path': str(self.outfile)}) await new_page.close() await self.page.tracing.stop() @sync async def test_return_buffer(self): await self.page.tracing.start(screenshots=True, path=str(self.outfile)) await self.page.goto(self.url + 'static/grid.html') trace = await self.page.tracing.stop() with self.outfile.open('r') as f: buf = f.read() self.assertEqual(trace, buf) @unittest.skip('Not implemented') @sync async def test_return_null_on_error(self): await self.page.tracing.start(screenshots=True) await self.page.goto(self.url + 'static/grid.html') @sync async def test_without_path(self): await self.page.tracing.start(screenshots=True) await self.page.goto(self.url + 'static/grid.html') trace = await self.page.tracing.stop() self.assertIn('screenshot', trace) ================================================ FILE: tests/test_worker.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio from syncer import sync from .base import BaseTestCase class TestWorker(BaseTestCase): @sync async def test_worker(self): await self.page.goto(self.url + 'static/worker/worker.html') await self.page.waitForFunction('() => !!worker') worker = self.page.workers[0] self.assertIn('worker.js', worker.url) executionContext = await worker.executionContext() self.assertEqual( await executionContext.evaluate('self.workerFunction()'), 'worker function result', ) @sync async def test_create_destroy_events(self): workerCreatedPromise = asyncio.get_event_loop().create_future() self.page.once('workercreated', lambda w: workerCreatedPromise.set_result(w)) workerObj = await self.page.evaluateHandle( '() => new Worker("data:text/javascript,1")') worker = await workerCreatedPromise workerDestroyedPromise = asyncio.get_event_loop().create_future() self.page.once('workerdestroyed', lambda w: workerDestroyedPromise.set_result(w)) await self.page.evaluate( 'workerObj => workerObj.terminate()', workerObj) self.assertEqual(await workerDestroyedPromise, worker) @sync async def test_report_console_logs(self): logPromise = asyncio.get_event_loop().create_future() self.page.once('console', lambda m: logPromise.set_result(m)) await self.page.evaluate( '() => new Worker("data:text/javascript,console.log(1)")' ) log = await logPromise self.assertEqual(log.text, '1') @sync async def test_jshandle_for_console_log(self): logPromise = asyncio.get_event_loop().create_future() self.page.on('console', lambda m: logPromise.set_result(m)) await self.page.evaluate( '() => new Worker("data:text/javascript,console.log(1,2,3,this)")') log = await logPromise self.assertEqual(log.text, '1 2 3 JSHandle@object') self.assertEqual(len(log.args), 4) self.assertEqual( await (await log.args[3].getProperty('origin')).jsonValue(), 'null', ) @sync async def test_execution_context(self): workerCreatedPromise = asyncio.get_event_loop().create_future() self.page.once('workercreated', lambda w: workerCreatedPromise.set_result(w)) await self.page.evaluate( '() => new Worker("data:text/javascript,console.log(1)")') worker = await workerCreatedPromise self.assertEqual( await (await worker.executionContext()).evaluate('1+1'), 2) self.assertEqual(await worker.evaluate('1+2'), 3) @sync async def test_report_error(self): errorPromise = asyncio.get_event_loop().create_future() self.page.on('pageerror', lambda x: errorPromise.set_result(x)) await self.page.evaluate('() => new Worker(`data:text/javascript, throw new Error("this is my error");`)') # noqa: E501 errorLog = await errorPromise self.assertIn('this is my error', errorLog.args[0]) ================================================ FILE: tests/utils.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import asyncio def waitEvent(emitter, event_name): fut = asyncio.get_event_loop().create_future() def set_done(arg=None): fut.set_result(arg) emitter.once(event_name, set_done) return fut ================================================ FILE: tox.ini ================================================ [tox] envlist = py3{6,7,8},flake8,mypy minversion = 3.4.0 isolated_build = True [testenv] passenv: PYTEST_ADDOPTS CI* whitelist_external = poetry deps = py3{6,7,8},mypy: poetry skip_install = true commands_pre: ; mypy config doesn't play well when checking a dir, have to install the package and check it instead py3{6,7,8},mypy: poetry install -vv py3{6,7,8}: poetry run pyppeteer-install commands = py3{6,7,8}: poetry run pytest {posargs} [testenv:flake8] deps = flake8 black commands = flake8 ./ black --check -S [testenv:mypy] passenv = MYPY_JUNIT_XML_PATH deps = mypy whitelist_externals: poetry commands = mypy pyppeteer --config-file ./tox.ini [flake8] max-line-length = 120 exclude = docs,.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,out max-complexity = 7 [pydocstyle] ignore = D105,D107,D203,D213,D402,D404 match_dir = (?!(tmp|docs|ja_docs|tests|\.)).* [pytest] ; default number of workers for testing ; to change parallelism setting, export the PYTEST_ADDOPTS env variable ; (no need to change the next line, it's overridden by PYTEST_ADDOPTS) addopts = -n 6 minversion = 5.0.0 ; nicely formatted junit output for CI builds (must be enabled with flag: --junitxml=PATH) junit_suite_name: pyppeteer tests junit_log_passing_tests: True junit_family = xunit2 ;mypy config ;only mypy will be paying attention to this ;(hence the different env var interpolation format) [mypy] ; this line is currently useless until this PR lands: https://github.com/python/mypy/pull/8479 junit_xml = $MYPY_JUNIT_XML_PATH strict_optional = true disallow_untyped_defs = true disallow_untyped_calls = true follow_imports = silent ignore_missing_imports = true mypy_path = out [mypy-pyppeteer.tests.*,pyppeteer.docs.*] ignore_errors = true