├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── contributing.md
└── workflows
│ └── tests.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin
└── console
├── composer.json
├── package.json
├── phpunit.xml
├── src
├── Command
│ └── GenerateDocumentationCommand.php
├── Puppeteer.php
├── PuppeteerConnectionDelegate.js
├── PuppeteerProcessDelegate.php
├── Resources
│ ├── Accessibility.php
│ ├── Browser.php
│ ├── BrowserContext.php
│ ├── BrowserFetcher.php
│ ├── CDPSession.php
│ ├── ConsoleMessage.php
│ ├── Coverage.php
│ ├── Dialog.php
│ ├── ElementHandle.php
│ ├── EventEmitter.php
│ ├── ExecutionContext.php
│ ├── FileChooser.php
│ ├── Frame.php
│ ├── HTTPRequest.php
│ ├── HTTPResponse.php
│ ├── JSHandle.php
│ ├── Keyboard.php
│ ├── Mouse.php
│ ├── Page.php
│ ├── SecurityDetails.php
│ ├── Target.php
│ ├── TimeoutError.php
│ ├── Touchscreen.php
│ ├── Tracing.php
│ └── WebWorker.php
├── Traits
│ ├── AliasesEvaluationMethods.php
│ └── AliasesSelectionMethods.php
├── doc-generator.ts
└── get-puppeteer-version.js
└── tests
├── DownloadTest.php
├── PuphpeteerTest.php
├── ResourceInstantiator.php
├── RiskyResource.php
├── TestCase.php
├── UntestableResource.php
└── resources
├── denys-barabanov-jKcFmXCfaQ8-unsplash.jpg
├── index.html
├── puphpeteer-logo.png
├── stylesheet.css
└── worker.js
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help improve PuPHPeteer
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **Reproducible example**
11 | ```php
12 | // Provide a full example reproducing the bug
13 | ```
14 |
15 | **Expected behavior**
16 | A clear and concise description of what you expected to happen.
17 |
18 | **Generated logs**
19 | Provide the logs generated by your example, see how to get them in the guidelines for contributing to this repository:
20 | https://github.com/nesk/puphpeteer/blob/master/.github/contributing.md#provide-logs-with-your-bug-report
21 |
22 | ```
23 | The generated logs
24 | ```
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Environment (please complete the following information):**
30 | - OS: [e.g. Windows 10, macOS 10.13.2, Ubuntu 18.04]
31 | - Node version: [e.g. 10.4.0]
32 | - PHP version [e.g. 7.2.6]
33 |
34 | My Node package manager is:
35 | - [x] NPM (specify the version)
36 | - [ ] Yarn (specify the version)
37 | - [ ] Other (specify the name and version)
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to PuPHPeteer
2 |
3 | Please, be sure to read this document _before_ creating an issue or a pull-request.
4 |
5 | ## Where to ask a question or report a bug
6 |
7 | First, be sure to differentiate the original project [Puppeteer ](https://github.com/GoogleChrome/puppeteer), developed by Google to interact with Chromium, from [PuPHPeteer ](https://github.com/nesk/puphpeteer), an unofficial PHP bridge to call Puppeteer methods in PHP.
8 |
9 | Here are some cases to help you choosing where to ask a question or report a bug. The first list item matching your case should be used:
10 |
11 | 1. You don't know how to do _[something]_ with **Puppeteer **: ask your question [on StackOverflow with the _puppeteer_ tag](https://stackoverflow.com/questions/tagged/puppeteer?sort=newest).
12 | 2. You don't know how to do _[something]_ with **PuPHPeteer **: ask your question [in PuPHPeteer's issues](https://github.com/nesk/puphpeteer/issues).
13 | 3. You encountered a bug:
14 | 1. It is reproducible with **Puppeteer **: report the bug [in Puppeteer's issues](https://github.com/GoogleChrome/puppeteer/issues).
15 | 2. it is reproducible only with **PuPHPeteer **: report the bug [in PuPHPeteer's issues](https://github.com/nesk/puphpeteer/issues).
16 |
17 | ## Provide logs with your bug report
18 |
19 | Bug reports should contain logs generated by your reproducible example. To get them you must provide a logger to your PuPHPeteer instance. Say you have the following code in your bug report:
20 |
21 | ```php
22 | use Nesk\Puphpeteer\Puppeteer;
23 |
24 | $puppeteer = new Puppeteer;
25 | $browser = $puppeteer->launch();
26 | $browser->newPage()->goto('https://example.com');
27 | $browser->close();
28 | ```
29 |
30 | Require Monolog with Composer:
31 |
32 | ```shell
33 | composer require monolog/monolog
34 | ```
35 |
36 | And provide a Monolog instance to the `Puppeteer` constructor:
37 |
38 | ```diff
39 | use Nesk\Puphpeteer\Puppeteer;
40 |
41 | - $puppeteer = new Puppeteer;
42 | + $logPath = 'path/to/your.log';
43 | +
44 | + $logger = new \Monolog\Logger('PuPHPeteer');
45 | + $logger->pushHandler(new \Monolog\Handler\StreamHandler($logPath, \Monolog\Logger::DEBUG));
46 | +
47 | + $puppeteer = new Puppeteer([
48 | + 'logger' => $logger,
49 | + 'log_node_console' => true,
50 | + 'log_browser_console' => true,
51 | + ]);
52 | +
53 | $browser = $puppeteer->launch();
54 | $browser->newPage()->goto('https://example.com');
55 | $browser->close();
56 | ```
57 |
58 | Execute your code and `path/to/your.log` will contain the generated logs, here's an example of what you can get and provide in your bug report:
59 |
60 | ```
61 | [2018-08-17 10:26:01] PuPHPeteer.INFO: Applying options... {"options":{"read_timeout":30,"log_browser_console":true,"logger":"[object] (Monolog\\Logger: {})","log_node_console":true}} []
62 | [2018-08-17 10:26:01] PuPHPeteer.DEBUG: Options applied and merged with defaults {"options":{"executable_path":"node","idle_timeout":60,"read_timeout":30,"stop_timeout":3,"logger":"[object] (Monolog\\Logger: {})","log_node_console":true,"debug":false,"log_browser_console":true}} []
63 | [2018-08-17 10:26:01] PuPHPeteer.INFO: Starting process with command line: 'node' '/Users/johann/Development/puphpeteer/node_modules/@nesk/rialto/src/node-process/serve.js' '/Users/johann/Development/puphpeteer/src/PuppeteerConnectionDelegate.js' '{"idle_timeout":60,"log_node_console":true,"log_browser_console":true}' {"commandline":"'node' '/Users/johann/Development/puphpeteer/node_modules/@nesk/rialto/src/node-process/serve.js' '/Users/johann/Development/puphpeteer/src/PuppeteerConnectionDelegate.js' '{\"idle_timeout\":60,\"log_node_console\":true,\"log_browser_console\":true}'"} []
64 | [2018-08-17 10:26:01] PuPHPeteer.INFO: Process started with PID 18153 {"pid":18153} []
65 | [2018-08-17 10:26:01] PuPHPeteer.DEBUG: Sending an instruction to the port 59621... {"pid":18153,"port":59621,"instruction":{"type":"call","name":"launch","catched":false,"value":[]}} []
66 | [2018-08-17 10:26:01] PuPHPeteer.DEBUG: Received data from the port 59621... {"pid":18153,"port":59621,"data":"[object] (Nesk\\Puphpeteer\\Resources\\Browser: {\"__rialto_resource__\":true,\"class_name\":\"Browser\",\"id\":\"1534501561533.8093\"})"} []
67 | ```
68 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push, pull_request]
3 | jobs:
4 | phpunit:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | php-version: [7.3, 7.4, 8.0]
9 | composer-flags: [null, --prefer-lowest]
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v1
13 | - uses: shivammathur/setup-php@v2
14 | with:
15 | php-version: ${{ matrix.php-version }}
16 | coverage: none
17 | - run: composer update ${{ matrix.composer-flags }} --no-interaction --no-progress --prefer-dist --ansi
18 | - run: npm install
19 | - run: ./vendor/bin/phpunit --color=always
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.build/
2 | /node_modules/
3 | /vendor/
4 | .phpunit.result.cache
5 | composer.lock
6 | package-lock.json
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7 |
8 | **Note:** PuPHPeteer is heavily based on [Rialto](https://github.com/nesk/rialto). For a complete overview of the changes, you might want to check out [Rialto's changelog](https://github.com/nesk/rialto/blob/master/CHANGELOG.md) too.
9 |
10 | ## [Unreleased]
11 | _In progress…_
12 |
13 | ## [2.0.0] - 2020-12-01
14 | ### Added
15 | - Support Puppeteer v5.5
16 | - Support PHP 8
17 | - Add documentation on all resources to provide autocompletion in IDEs
18 |
19 | ### Removed
20 | - Drop support for PHP 7.1 and 7.2
21 |
22 | ## [1.6.0] - 2019-07-01
23 | ### Added
24 | - Support Puppeteer v1.18
25 |
26 | ## [1.5.0] - 2019-03-17
27 | ### Added
28 | - Support Puppeteer v1.13
29 | - Make the `ElementHandle` resource extend the `JSHandle` one
30 |
31 | ### Fixed
32 | - Add missing `Accessibility` resource
33 |
34 | ## [1.4.1] - 2018-11-27
35 | ### Added
36 | - Support Puppeteer v1.10
37 |
38 | ## [1.4.0] - 2018-09-22
39 | ### Added
40 | - Support Puppeteer v1.8
41 |
42 | ### Changed
43 | - Detect resource types by using the constructor name
44 |
45 | ### Fixed
46 | - Logs of initial pages are now retrieved
47 |
48 | ## [1.3.0] - 2018-08-20
49 | ### Added
50 | - Add a `log_browser_console` option to log the output of Browser's console methods (`console.log`, `console.debug`, `console.table`, etc…) to the PHP logger
51 | - Support Puppeteer v1.7
52 |
53 | ## [1.2.0] - 2018-07-25
54 | ### Added
55 | - Support Puppeteer v1.6
56 |
57 | ### Changed
58 | - Upgrade to Rialto v1.1
59 |
60 | ## [1.1.0] - 2018-06-12
61 | ### Added
62 | - Support Puppeteer v1.5
63 | - Add aliases for evaluation methods to the `ElementHandle` resource
64 | - Support new Puppeteer v1.5 resources:
65 | - `BrowserContext`
66 | - `Worker`
67 |
68 | ### Fixed
69 | - Fix Travis tests
70 |
71 | ## [1.0.0] - 2018-06-05
72 | ### Changed
73 | - Change PHP's vendor name from `extractr-io` to `nesk`
74 | - Change NPM's scope name from `@extractr-io` to `@nesk`
75 | - Upgrade to Rialto v1
76 |
77 | ## [0.2.2] - 2018-04-20
78 | ### Added
79 | - Support Puppeteer v1.3
80 | - Test missing Puppeteer resources: `ConsoleMessage` and `Dialog`
81 | - Show a warning in logs if Puppeteer's version doesn't match requirements
82 |
83 | ## [0.2.1] - 2018-04-09
84 | ### Changed
85 | - Update Rialto version requirements
86 |
87 | ## [0.2.0] - 2018-02-19
88 | ### Added
89 | - Support new Puppeteer v1.1 resources:
90 | - `BrowserFetcher`
91 | - `CDPSession`
92 | - `Coverage`
93 | - `SecurityDetails`
94 | - Test Puppeteer resources
95 | - Support PHPUnit v7
96 | - Add Travis integration
97 |
98 | ### Changed
99 | - Lock Puppeteer's version to v1.1 to avoid issues with forward compatibility
100 |
101 | ## 0.1.0 - 2018-01-29
102 | First release
103 |
104 |
105 | [Unreleased]: https://github.com/nesk/puphpeteer/compare/2.0.0...HEAD
106 | [2.0.0]: https://github.com/nesk/puphpeteer/compare/1.6.0...2.0.0
107 | [1.6.0]: https://github.com/nesk/puphpeteer/compare/1.5.0...1.6.0
108 | [1.5.0]: https://github.com/nesk/puphpeteer/compare/1.4.1...1.5.0
109 | [1.4.1]: https://github.com/nesk/puphpeteer/compare/1.4.0...1.4.1
110 | [1.4.0]: https://github.com/nesk/puphpeteer/compare/1.3.0...1.4.0
111 | [1.3.0]: https://github.com/nesk/puphpeteer/compare/1.2.0...1.3.0
112 | [1.2.0]: https://github.com/nesk/puphpeteer/compare/1.1.0...1.2.0
113 | [1.1.0]: https://github.com/nesk/puphpeteer/compare/1.0.0...1.1.0
114 | [1.0.0]: https://github.com/nesk/puphpeteer/compare/0.2.2...1.0.0
115 | [0.2.2]: https://github.com/nesk/puphpeteer/compare/0.2.1...0.2.2
116 | [0.2.1]: https://github.com/nesk/puphpeteer/compare/0.2.0...0.2.1
117 | [0.2.0]: https://github.com/nesk/puphpeteer/compare/0.1.0...0.2.0
118 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Johann Pardanaud
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚨 This project is not maintained anymore
2 |
3 | As I write these lines, it's been nearly two years since the latest release of PuPHPeteer. Despite the enthusiasm around this project, I no longer have the motivation to support its development, mainly because it never really had any use to me. So its time to be honest with you, PuPHPeteer is no longer maintained.
4 |
5 | However, here's a list of forks maintained by the community:
6 |
7 | - [zoonru/puphpeteer](https://github.com/zoonru/puphpeteer)
8 | - [NigelCunningham/puphpeteer](https://github.com/NigelCunningham/puphpeteer)
9 |
10 | If you create a fork and plan to maintain it, let me know and I will link it here.
11 |
12 | # PuPHPeteer
13 |
14 |
15 |
16 | [](http://php.net/)
17 | [](https://packagist.org/packages/nesk/puphpeteer)
18 | [](https://nodejs.org/)
19 | [](https://www.npmjs.com/package/@nesk/puphpeteer)
20 | [](https://travis-ci.org/nesk/puphpeteer)
21 |
22 | A [Puppeteer](https://github.com/GoogleChrome/puppeteer/) bridge for PHP, supporting the entire API. Based on [Rialto](https://github.com/nesk/rialto/), a package to manage Node resources from PHP.
23 |
24 | Here are some examples [borrowed from Puppeteer's documentation](https://github.com/GoogleChrome/puppeteer/blob/master/README.md#usage) and adapted to PHP's syntax:
25 |
26 | **Example** - navigating to https://example.com and saving a screenshot as *example.png*:
27 |
28 | ```php
29 | use Nesk\Puphpeteer\Puppeteer;
30 |
31 | $puppeteer = new Puppeteer;
32 | $browser = $puppeteer->launch();
33 |
34 | $page = $browser->newPage();
35 | $page->goto('https://example.com');
36 | $page->screenshot(['path' => 'example.png']);
37 |
38 | $browser->close();
39 | ```
40 |
41 | **Example** - evaluate a script in the context of the page:
42 |
43 | ```php
44 | use Nesk\Puphpeteer\Puppeteer;
45 | use Nesk\Rialto\Data\JsFunction;
46 |
47 | $puppeteer = new Puppeteer;
48 |
49 | $browser = $puppeteer->launch();
50 | $page = $browser->newPage();
51 | $page->goto('https://example.com');
52 |
53 | // Get the "viewport" of the page, as reported by the page.
54 | $dimensions = $page->evaluate(JsFunction::createWithBody("
55 | return {
56 | width: document.documentElement.clientWidth,
57 | height: document.documentElement.clientHeight,
58 | deviceScaleFactor: window.devicePixelRatio
59 | };
60 | "));
61 |
62 | printf('Dimensions: %s', print_r($dimensions, true));
63 |
64 | $browser->close();
65 | ```
66 |
67 | ## Requirements and installation
68 |
69 | This package requires PHP >= 7.3 and Node >= 8.
70 |
71 | Install it with these two command lines:
72 |
73 | ```shell
74 | composer require nesk/puphpeteer
75 | npm install @nesk/puphpeteer
76 | ```
77 |
78 | ## Notable differences between PuPHPeteer and Puppeteer
79 |
80 | ### Puppeteer's class must be instantiated
81 |
82 | Instead of requiring Puppeteer:
83 |
84 | ```js
85 | const puppeteer = require('puppeteer');
86 | ```
87 |
88 | You have to instantiate the `Puppeteer` class:
89 |
90 | ```php
91 | $puppeteer = new Puppeteer;
92 | ```
93 |
94 | This will create a new Node process controlled by PHP.
95 |
96 | You can also pass some options to the constructor, see [Rialto's documentation](https://github.com/nesk/rialto/blob/master/docs/api.md#options). PuPHPeteer also extends these options:
97 |
98 | ```php
99 | [
100 | // Logs the output of Browser's console methods (console.log, console.debug, etc...) to the PHP logger
101 | 'log_browser_console' => false,
102 | ]
103 | ```
104 |
105 |
106 | ⏱ Want to use some timeouts higher than 30 seconds in Puppeteer's API?
107 |
108 | If you use some timeouts higher than 30 seconds, you will have to set a higher value for the `read_timeout` option (default: `35`):
109 |
110 | ```php
111 | $puppeteer = new Puppeteer([
112 | 'read_timeout' => 65, // In seconds
113 | ]);
114 |
115 | $puppeteer->launch()->newPage()->goto($url, [
116 | 'timeout' => 60000, // In milliseconds
117 | ]);
118 | ```
119 |
120 |
121 | ### No need to use the `await` keyword
122 |
123 | With PuPHPeteer, every method call or property getting/setting is synchronous.
124 |
125 | ### Some methods have been aliased
126 |
127 | The following methods have been aliased because PHP doesn't support the `$` character in method names:
128 |
129 | - `$` => `querySelector`
130 | - `$$` => `querySelectorAll`
131 | - `$x` => `querySelectorXPath`
132 | - `$eval` => `querySelectorEval`
133 | - `$$eval` => `querySelectorAllEval`
134 |
135 | Use these aliases just like you would have used the original methods:
136 |
137 | ```php
138 | $divs = $page->querySelectorAll('div');
139 | ```
140 |
141 | ### Evaluated functions must be created with `JsFunction`
142 |
143 | Functions evaluated in the context of the page must be written [with the `JsFunction` class](https://github.com/nesk/rialto/blob/master/docs/api.md#javascript-functions), the body of these functions must be written in JavaScript instead of PHP.
144 |
145 | ```php
146 | use Nesk\Rialto\Data\JsFunction;
147 |
148 | $pageFunction = JsFunction::createWithParameters(['element'])
149 | ->body("return element.textContent");
150 | ```
151 |
152 | ### Exceptions must be caught with `->tryCatch`
153 |
154 | If an error occurs in Node, a `Node\FatalException` will be thrown and the process closed, you will have to create a new instance of `Puppeteer`.
155 |
156 | To avoid that, you can ask Node to catch these errors by prepending your instruction with `->tryCatch`:
157 |
158 | ```php
159 | use Nesk\Rialto\Exceptions\Node;
160 |
161 | try {
162 | $page->tryCatch->goto('invalid_url');
163 | } catch (Node\Exception $exception) {
164 | // Handle the exception...
165 | }
166 | ```
167 |
168 | Instead, a `Node\Exception` will be thrown, the Node process will stay alive and usable.
169 |
170 | ## License
171 |
172 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
173 |
174 | ## Logo attribution
175 |
176 | PuPHPeteer's logo is composed of:
177 |
178 | - [Puppet](https://thenounproject.com/search/?q=puppet&i=52120) by Luis Prado from [the Noun Project](http://thenounproject.com/).
179 | - [Elephant](https://thenounproject.com/search/?q=elephant&i=954119) by Lluisa Iborra from [the Noun Project](http://thenounproject.com/).
180 |
181 | Thanks to [Laravel News](https://laravel-news.com/) for picking the icons and colors of the logo.
182 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new GenerateDocumentationCommand)
11 | ->getApplication()
12 | ->run();
13 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nesk/puphpeteer",
3 | "description": "A Puppeteer bridge for PHP, supporting the entire API.",
4 | "keywords": [
5 | "php",
6 | "puppeteer",
7 | "headless-chrome",
8 | "testing",
9 | "web",
10 | "developer-tools",
11 | "automation"
12 | ],
13 | "type": "library",
14 | "license": "MIT",
15 | "authors": [
16 | {
17 | "name": "Johann Pardanaud",
18 | "email": "pardanaud.j@gmail.com"
19 | }
20 | ],
21 | "require": {
22 | "php": ">=7.3",
23 | "nesk/rialto": "^1.2.0",
24 | "psr/log": "^1.0",
25 | "vierbergenlars/php-semver": "^3.0.2"
26 | },
27 | "require-dev": {
28 | "monolog/monolog": "^2.0",
29 | "phpunit/phpunit": "^9.0",
30 | "symfony/process": "^4.0|^5.0",
31 | "symfony/console": "^4.0|^5.0"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "Nesk\\Puphpeteer\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "Nesk\\Puphpeteer\\Tests\\": "tests/"
41 | }
42 | },
43 | "scripts": {
44 | "post-install-cmd": "npm install",
45 | "test": "./vendor/bin/phpunit"
46 | },
47 | "config": {
48 | "sort-packages": true
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nesk/puphpeteer",
3 | "version": "2.0.0",
4 | "description": "A Puppeteer bridge for PHP, supporting the entire API.",
5 | "keywords": [
6 | "php",
7 | "puppeteer",
8 | "headless-chrome",
9 | "testing",
10 | "web",
11 | "developer-tools",
12 | "automation"
13 | ],
14 | "author": {
15 | "name": "Johann Pardanaud",
16 | "email": "pardanaud.j@gmail.com",
17 | "url": "https://johann.pardanaud.com/"
18 | },
19 | "license": "MIT",
20 | "repository": "github:nesk/puphpeteer",
21 | "engines": {
22 | "node": ">=9.0.0"
23 | },
24 | "dependencies": {
25 | "@nesk/rialto": "^1.2.1",
26 | "puppeteer": "~5.5.0"
27 | },
28 | "devDependencies": {
29 | "@types/yargs": "^15.0.10",
30 | "typescript": "^4.1.2",
31 | "yargs": "^16.1.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | tests
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Command/GenerateDocumentationCommand.php:
--------------------------------------------------------------------------------
1 | addOption(
26 | 'puppeteerPath',
27 | null,
28 | InputOption::VALUE_OPTIONAL,
29 | 'The path where Puppeteer is installed.',
30 | self::NODE_MODULES_DIR.'/puppeteer'
31 | );
32 | }
33 |
34 | /**
35 | * Builds the documentation generator from TypeScript to JavaScript.
36 | */
37 | private static function buildDocumentationGenerator(): void
38 | {
39 | $process = new Process([
40 | self::NODE_MODULES_DIR.'/.bin/tsc',
41 | '--outDir',
42 | self::BUILD_DIR,
43 | __DIR__.'/../../src/'.self::DOC_FILE_NAME.'.ts',
44 | ]);
45 | $process->run();
46 | }
47 |
48 | /**
49 | * Gets the documentation from the TypeScript documentation generator.
50 | */
51 | private static function getDocumentation(string $puppeteerPath, array $resourceNames): array
52 | {
53 | self::buildDocumentationGenerator();
54 |
55 | $commonFiles = \glob("$puppeteerPath/lib/esm/puppeteer/common/*.d.ts");
56 | $nodeFiles = \glob("$puppeteerPath/lib/esm/puppeteer/node/*.d.ts");
57 |
58 | $process = new Process(
59 | \array_merge(
60 | ['node', self::BUILD_DIR.'/'.self::DOC_FILE_NAME.'.js', 'php'],
61 | $commonFiles,
62 | $nodeFiles,
63 | ['--resources-namespace', self::RESOURCES_NAMESPACE, '--resources'],
64 | $resourceNames
65 | )
66 | );
67 | $process->mustRun();
68 |
69 | return \json_decode($process->getOutput(), true);
70 | }
71 |
72 | private static function getResourceNames(): array
73 | {
74 | return array_map(function (string $filePath): string {
75 | return explode('.', \basename($filePath))[0];
76 | }, \glob(self::RESOURCES_DIR.'/*'));
77 | }
78 |
79 | private static function generatePhpDocWithDocumentation(array $classDocumentation): ?string
80 | {
81 | $properties = array_map(function (string $property): string {
82 | return "\n * @property $property";
83 | }, $classDocumentation['properties']);
84 | $properties = \implode('', $properties);
85 |
86 | $getters = array_map(function (string $getter): string {
87 | return "\n * @property-read $getter";
88 | }, $classDocumentation['getters']);
89 | $getters = \implode('', $getters);
90 |
91 | $methods = array_map(function (string $method): string {
92 | return "\n * @method $method";
93 | }, $classDocumentation['methods']);
94 | $methods = \implode('', $methods);
95 |
96 | if (\strlen($properties) > 0 || \strlen($getters) > 0 || \strlen($methods) > 0) {
97 | return "/**$properties$getters$methods\n */";
98 | }
99 |
100 | return null;
101 | }
102 |
103 | /**
104 | * Writes the doc comment in the PHP class.
105 | */
106 | private static function writePhpDoc(string $className, string $phpDoc): void
107 | {
108 | $reflectionClass = new \ReflectionClass($className);
109 |
110 | if (! $reflectionClass) {
111 | return;
112 | }
113 |
114 | $fileName = $reflectionClass->getFileName();
115 |
116 | $contents = file_get_contents($fileName);
117 |
118 | // If there already is a doc comment, replace it.
119 | if ($doc = $reflectionClass->getDocComment()) {
120 | $newContents = str_replace($doc, $phpDoc, $contents);
121 | } else {
122 | $startLine = $reflectionClass->getStartLine();
123 |
124 | $lines = explode("\n", $contents);
125 |
126 | $before = array_slice($lines, 0, $startLine - 1);
127 | $after = array_slice($lines, $startLine - 1);
128 |
129 | $newContents = implode("\n", array_merge($before, explode("\n", $phpDoc), $after));
130 | }
131 |
132 | file_put_contents($fileName, $newContents);
133 | }
134 |
135 | /**
136 | * Executes the current command.
137 | */
138 | protected function execute(InputInterface $input, OutputInterface $output): int
139 | {
140 | $io = new SymfonyStyle($input, $output);
141 |
142 | $resourceNames = self::getResourceNames();
143 | $documentation = self::getDocumentation($input->getOption('puppeteerPath'), $resourceNames);
144 |
145 | foreach ($resourceNames as $resourceName) {
146 | $classDocumentation = $documentation[$resourceName] ?? null;
147 |
148 | if ($classDocumentation !== null) {
149 | $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation);
150 | if ($phpDoc !== null) {
151 | $resourceClass = self::RESOURCES_NAMESPACE.'\\'.$resourceName;
152 | self::writePhpDoc($resourceClass, $phpDoc);
153 | }
154 | }
155 | }
156 |
157 | // Handle the specific Puppeteer class
158 | $classDocumentation = $documentation['Puppeteer'] ?? null;
159 | if ($classDocumentation !== null) {
160 | $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation);
161 | if ($phpDoc !== null) {
162 | self::writePhpDoc(Puppeteer::class, $phpDoc);
163 | }
164 | }
165 |
166 | $missingResources = \array_diff(\array_keys($documentation), $resourceNames);
167 | foreach ($missingResources as $resource) {
168 | $io->warning("The $resource class in Puppeteer doesn't have any equivalent in PuPHPeteer.");
169 | }
170 |
171 | $inexistantResources = \array_diff($resourceNames, \array_keys($documentation));
172 | foreach ($inexistantResources as $resource) {
173 | $io->error("The $resource resource doesn't have any equivalent in Puppeteer.");
174 | }
175 |
176 | return 0;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/Puppeteer.php:
--------------------------------------------------------------------------------
1 | $options)
14 | * @method void registerCustomQueryHandler(string $name, mixed $queryHandler)
15 | * @method void unregisterCustomQueryHandler(string $name)
16 | * @method string[] customQueryHandlerNames()
17 | * @method void clearCustomQueryHandlers()
18 | */
19 | class Puppeteer extends AbstractEntryPoint
20 | {
21 | /**
22 | * Default options.
23 | *
24 | * @var array
25 | */
26 | protected $options = [
27 | 'read_timeout' => 30,
28 |
29 | // Logs the output of Browser's console methods (console.log, console.debug, etc...) to the PHP logger
30 | 'log_browser_console' => false,
31 | ];
32 |
33 | /**
34 | * Instantiate Puppeteer's entry point.
35 | */
36 | public function __construct(array $userOptions = [])
37 | {
38 | if (!empty($userOptions['logger']) && $userOptions['logger'] instanceof LoggerInterface) {
39 | $this->checkPuppeteerVersion($userOptions['executable_path'] ?? 'node', $userOptions['logger']);
40 | }
41 |
42 | parent::__construct(
43 | __DIR__.'/PuppeteerConnectionDelegate.js',
44 | new PuppeteerProcessDelegate,
45 | $this->options,
46 | $userOptions
47 | );
48 | }
49 |
50 | private function checkPuppeteerVersion(string $nodePath, LoggerInterface $logger): void {
51 | $currentVersion = $this->currentPuppeteerVersion($nodePath);
52 | $acceptedVersions = $this->acceptedPuppeteerVersion();
53 |
54 | try {
55 | $semver = new version($currentVersion);
56 | $expression = new expression($acceptedVersions);
57 |
58 | if (!$semver->satisfies($expression)) {
59 | $logger->warning(
60 | "The installed version of Puppeteer (v$currentVersion) doesn't match the requirements"
61 | ." ($acceptedVersions), you may encounter issues."
62 | );
63 | }
64 | } catch (SemVerException $exception) {
65 | $logger->warning("Puppeteer doesn't seem to be installed.");
66 | }
67 | }
68 |
69 | private function currentPuppeteerVersion(string $nodePath): ?string {
70 | $process = new Process([$nodePath, __DIR__.'/get-puppeteer-version.js']);
71 | $process->mustRun();
72 |
73 | return json_decode($process->getOutput());
74 | }
75 |
76 | private function acceptedPuppeteerVersion(): string {
77 | $npmManifestPath = __DIR__.'/../package.json';
78 | $npmManifest = json_decode(file_get_contents($npmManifestPath));
79 |
80 | return $npmManifest->dependencies->puppeteer;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/PuppeteerConnectionDelegate.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const puppeteer = require('puppeteer'),
4 | {ConnectionDelegate} = require('@nesk/rialto'),
5 | Logger = require('@nesk/rialto/src/node-process/Logger'),
6 | ConsoleInterceptor = require('@nesk/rialto/src/node-process/NodeInterceptors/ConsoleInterceptor'),
7 | StandardStreamsInterceptor = require('@nesk/rialto/src/node-process/NodeInterceptors/StandardStreamsInterceptor');
8 |
9 | /**
10 | * Handle the requests of a connection to control Puppeteer.
11 | */
12 | class PuppeteerConnectionDelegate extends ConnectionDelegate
13 | {
14 | /**
15 | * Constructor.
16 | *
17 | * @param {Object} options
18 | */
19 | constructor(options) {
20 | super(options);
21 |
22 | this.browsers = new Set;
23 |
24 | this.addSignalEventListeners();
25 | }
26 |
27 | /**
28 | * @inheritdoc
29 | */
30 | async handleInstruction(instruction, responseHandler, errorHandler) {
31 | instruction.setDefaultResource(puppeteer);
32 |
33 | let value = null;
34 |
35 | try {
36 | value = await instruction.execute();
37 | } catch (error) {
38 | if (instruction.shouldCatchErrors()) {
39 | return errorHandler(error);
40 | }
41 |
42 | throw error;
43 | }
44 |
45 | if (this.isInstanceOf(value, 'Browser')) {
46 | this.browsers.add(value);
47 |
48 | if (this.options.log_browser_console === true) {
49 | const initialPages = await value.pages()
50 | initialPages.forEach(page => page.on('console', this.logConsoleMessage));
51 | }
52 | }
53 |
54 | if (this.options.log_browser_console === true && this.isInstanceOf(value, 'Page')) {
55 | value.on('console', this.logConsoleMessage);
56 | }
57 |
58 | responseHandler(value);
59 | }
60 |
61 | /**
62 | * Checks if a value is an instance of a class. The check must be done with the `[object].constructor.name`
63 | * property because relying on Puppeteer's constructors isn't viable since the exports aren't constrained by semver.
64 | *
65 | * @protected
66 | * @param {*} value
67 | * @param {string} className
68 | *
69 | * @see {@link https://github.com/GoogleChrome/puppeteer/issues/3067|Puppeteer's issue about semver on exports}
70 | */
71 | isInstanceOf(value, className) {
72 | const nonObjectValues = [undefined, null];
73 |
74 | return !nonObjectValues.includes(value)
75 | && !nonObjectValues.includes(value.constructor)
76 | && value.constructor.name === className;
77 | }
78 |
79 | /**
80 | * Log the console message.
81 | *
82 | * @param {ConsoleMessage} consoleMessage
83 | */
84 | async logConsoleMessage(consoleMessage) {
85 | const type = consoleMessage.type();
86 |
87 | if (!ConsoleInterceptor.typeIsSupported(type)) {
88 | return;
89 | }
90 |
91 | const level = ConsoleInterceptor.getLevelFromType(type);
92 | const args = await Promise.all(consoleMessage.args().map(arg => arg.jsonValue()));
93 |
94 | StandardStreamsInterceptor.startInterceptingStrings(message => {
95 | Logger.log('Browser', level, ConsoleInterceptor.formatMessage(message));
96 | });
97 |
98 | ConsoleInterceptor.originalConsole[type](...args);
99 |
100 | StandardStreamsInterceptor.stopInterceptingStrings();
101 | }
102 |
103 | /**
104 | * Listen for process signal events.
105 | *
106 | * @protected
107 | */
108 | addSignalEventListeners() {
109 | for (let eventName of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
110 | process.on(eventName, () => {
111 | this.closeAllBrowsers();
112 | process.exit();
113 | });
114 | }
115 | }
116 |
117 | /**
118 | * Close all the browser instances when the process exits.
119 | *
120 | * Calling this method before exiting Node is mandatory since Puppeteer doesn't seem to handle that properly.
121 | *
122 | * @protected
123 | */
124 | closeAllBrowsers() {
125 | for (let browser of this.browsers.values()) {
126 | browser.close();
127 | }
128 | }
129 | }
130 |
131 | module.exports = PuppeteerConnectionDelegate;
132 |
--------------------------------------------------------------------------------
/src/PuppeteerProcessDelegate.php:
--------------------------------------------------------------------------------
1 | $options = null)
9 | */
10 | class Accessibility extends BasicResource
11 | {
12 | //
13 | }
14 |
--------------------------------------------------------------------------------
/src/Resources/Browser.php:
--------------------------------------------------------------------------------
1 | $options = null)
15 | * @method \Nesk\Puphpeteer\Resources\Page[] pages()
16 | * @method string version()
17 | * @method string userAgent()
18 | * @method void close()
19 | * @method void disconnect()
20 | * @method bool isConnected()
21 | */
22 | class Browser extends EventEmitter
23 | {
24 | //
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/BrowserContext.php:
--------------------------------------------------------------------------------
1 | $options = null)
9 | * @method mixed[] stopJSCoverage()
10 | * @method void startCSSCoverage(array $options = null)
11 | * @method mixed[] stopCSSCoverage()
12 | */
13 | class Coverage extends BasicResource
14 | {
15 | //
16 | }
17 |
--------------------------------------------------------------------------------
/src/Resources/Dialog.php:
--------------------------------------------------------------------------------
1 | |null asElement()
10 | * @method \Nesk\Puphpeteer\Resources\Frame|null contentFrame()
11 | * @method void hover()
12 | * @method void click(array $options = null)
13 | * @method string[] select(string ...$values)
14 | * @method void uploadFile(string ...$filePaths)
15 | * @method void tap()
16 | * @method void focus()
17 | * @method void type(string $text, array{ delay: float } $options = null)
18 | * @method void press(mixed $key, array $options = null)
19 | * @method mixed|null boundingBox()
20 | * @method mixed|null boxModel()
21 | * @method string|mixed|null screenshot(array{ } $options = null)
22 | * @method bool isIntersectingViewport()
23 | */
24 | class ElementHandle extends JSHandle
25 | {
26 | use AliasesSelectionMethods, AliasesEvaluationMethods;
27 | }
28 |
--------------------------------------------------------------------------------
/src/Resources/EventEmitter.php:
--------------------------------------------------------------------------------
1 | $options)
23 | * @method \Nesk\Puphpeteer\Resources\ElementHandle addStyleTag(array $options)
24 | * @method void click(string $selector, array{ delay: float, button: mixed, clickCount: float } $options = null)
25 | * @method void focus(string $selector)
26 | * @method void hover(string $selector)
27 | * @method string[] select(string $selector, string ...$values)
28 | * @method void tap(string $selector)
29 | * @method void type(string $selector, string $text, array{ delay: float } $options = null)
30 | * @method \Nesk\Puphpeteer\Resources\JSHandle|null waitFor(string|float|callable $selectorOrFunctionOrTimeout, array $options = null, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
31 | * @method void waitForTimeout(float $milliseconds)
32 | * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForSelector(string $selector, array $options = null)
33 | * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForXPath(string $xpath, array $options = null)
34 | * @method \Nesk\Puphpeteer\Resources\JSHandle waitForFunction(callable|string $pageFunction, array $options = null, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
35 | * @method string title()
36 | */
37 | class Frame extends BasicResource
38 | {
39 | use AliasesSelectionMethods, AliasesEvaluationMethods;
40 | }
41 |
--------------------------------------------------------------------------------
/src/Resources/HTTPRequest.php:
--------------------------------------------------------------------------------
1 | headers()
13 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null response()
14 | * @method \Nesk\Puphpeteer\Resources\Frame|null frame()
15 | * @method bool isNavigationRequest()
16 | * @method \Nesk\Puphpeteer\Resources\HTTPRequest[] redirectChain()
17 | * @method array{ errorText: string }|null failure()
18 | * @method void continue(mixed $overrides = null)
19 | * @method void respond(mixed $response)
20 | * @method void abort(mixed $errorCode = null)
21 | */
22 | class HTTPRequest extends BasicResource
23 | {
24 | //
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/HTTPResponse.php:
--------------------------------------------------------------------------------
1 | headers()
14 | * @method \Nesk\Puphpeteer\Resources\SecurityDetails|null securityDetails()
15 | * @method mixed buffer()
16 | * @method string text()
17 | * @method mixed json()
18 | * @method \Nesk\Puphpeteer\Resources\HTTPRequest request()
19 | * @method bool fromCache()
20 | * @method bool fromServiceWorker()
21 | * @method \Nesk\Puphpeteer\Resources\Frame|null frame()
22 | */
23 | class HTTPResponse extends BasicResource
24 | {
25 | //
26 | }
27 |
--------------------------------------------------------------------------------
/src/Resources/JSHandle.php:
--------------------------------------------------------------------------------
1 | getProperties()
13 | * @method array jsonValue()
14 | * @method \Nesk\Puphpeteer\Resources\ElementHandle|null asElement()
15 | * @method void dispose()
16 | * @method string toString()
17 | */
18 | class JSHandle extends BasicResource
19 | {
20 | //
21 | }
22 |
--------------------------------------------------------------------------------
/src/Resources/Keyboard.php:
--------------------------------------------------------------------------------
1 | &array{ delay: float } $options = null)
10 | * @method void down(array $options = null)
11 | * @method void up(array $options = null)
12 | * @method void wheel(array $options = null)
13 | */
14 | class Mouse extends BasicResource
15 | {
16 | //
17 | }
18 |
--------------------------------------------------------------------------------
/src/Resources/Page.php:
--------------------------------------------------------------------------------
1 | $options = null)
17 | * @method void setGeolocation(array $options)
18 | * @method \Nesk\Puphpeteer\Resources\Target target()
19 | * @method \Nesk\Puphpeteer\Resources\Browser browser()
20 | * @method \Nesk\Puphpeteer\Resources\BrowserContext browserContext()
21 | * @method \Nesk\Puphpeteer\Resources\Frame mainFrame()
22 | * @method \Nesk\Puphpeteer\Resources\Frame[] frames()
23 | * @method \Nesk\Puphpeteer\Resources\WebWorker[] workers()
24 | * @method void setRequestInterception(bool $value)
25 | * @method void setOfflineMode(bool $enabled)
26 | * @method void setDefaultNavigationTimeout(float $timeout)
27 | * @method void setDefaultTimeout(float $timeout)
28 | * @method mixed evaluateHandle(callable|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
29 | * @method \Nesk\Puphpeteer\Resources\JSHandle queryObjects(\Nesk\Puphpeteer\Resources\JSHandle $prototypeHandle)
30 | * @method mixed[] cookies(string ...$urls)
31 | * @method void deleteCookie(mixed ...$cookies)
32 | * @method void setCookie(mixed ...$cookies)
33 | * @method \Nesk\Puphpeteer\Resources\ElementHandle addScriptTag(array{ url: string, path: string, content: string, type: string } $options)
34 | * @method \Nesk\Puphpeteer\Resources\ElementHandle addStyleTag(array{ url: string, path: string, content: string } $options)
35 | * @method void exposeFunction(string $name, callable $puppeteerFunction)
36 | * @method void authenticate(mixed $credentials)
37 | * @method void setExtraHTTPHeaders(array $headers)
38 | * @method void setUserAgent(string $userAgent)
39 | * @method mixed metrics()
40 | * @method string url()
41 | * @method string content()
42 | * @method void setContent(string $html, array $options = null)
43 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse goto(string $url, array&array{ referer: string } $options = null)
44 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null reload(array $options = null)
45 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = null)
46 | * @method \Nesk\Puphpeteer\Resources\HTTPRequest waitForRequest(string|callable $urlOrPredicate, array{ timeout: float } $options = null)
47 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse waitForResponse(string|callable $urlOrPredicate, array{ timeout: float } $options = null)
48 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goBack(array $options = null)
49 | * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goForward(array $options = null)
50 | * @method void bringToFront()
51 | * @method void emulate(array{ viewport: mixed, userAgent: string } $options)
52 | * @method void setJavaScriptEnabled(bool $enabled)
53 | * @method void setBypassCSP(bool $enabled)
54 | * @method void emulateMediaType(string $type = null)
55 | * @method void emulateMediaFeatures(mixed[] $features = null)
56 | * @method void emulateTimezone(string $timezoneId = null)
57 | * @method void emulateIdleState(array{ isUserActive: bool, isScreenUnlocked: bool } $overrides = null)
58 | * @method void emulateVisionDeficiency(mixed $type = null)
59 | * @method void setViewport(mixed $viewport)
60 | * @method mixed|null viewport()
61 | * @method mixed evaluate(mixed $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
62 | * @method void evaluateOnNewDocument(callable|string $pageFunction, mixed ...$args)
63 | * @method void setCacheEnabled(bool $enabled = null)
64 | * @method mixed|string|null screenshot(array $options = null)
65 | * @method mixed pdf(array $options = null)
66 | * @method string title()
67 | * @method void close(array{ runBeforeUnload: bool } $options = null)
68 | * @method bool isClosed()
69 | * @method void click(string $selector, array{ delay: float, button: mixed, clickCount: float } $options = null)
70 | * @method void focus(string $selector)
71 | * @method void hover(string $selector)
72 | * @method string[] select(string $selector, string ...$values)
73 | * @method void tap(string $selector)
74 | * @method void type(string $selector, string $text, array{ delay: float } $options = null)
75 | * @method \Nesk\Puphpeteer\Resources\JSHandle waitFor(string|float|callable $selectorOrFunctionOrTimeout, array{ visible: bool, hidden: bool, timeout: float, polling: string|float } $options = null, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
76 | * @method void waitForTimeout(float $milliseconds)
77 | * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForSelector(string $selector, array{ visible: bool, hidden: bool, timeout: float } $options = null)
78 | * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForXPath(string $xpath, array{ visible: bool, hidden: bool, timeout: float } $options = null)
79 | * @method \Nesk\Puphpeteer\Resources\JSHandle waitForFunction(callable|string $pageFunction, array{ timeout: float, polling: string|float } $options = null, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
80 | */
81 | class Page extends EventEmitter
82 | {
83 | use AliasesSelectionMethods, AliasesEvaluationMethods;
84 | }
85 |
--------------------------------------------------------------------------------
/src/Resources/SecurityDetails.php:
--------------------------------------------------------------------------------
1 | $options = null)
9 | * @method mixed stop()
10 | */
11 | class Tracing extends BasicResource
12 | {
13 | //
14 | }
15 |
--------------------------------------------------------------------------------
/src/Resources/WebWorker.php:
--------------------------------------------------------------------------------
1 | __call('$eval', $arguments);
16 | }
17 |
18 | public function querySelectorAllEval(...$arguments)
19 | {
20 | return $this->__call('$$eval', $arguments);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Traits/AliasesSelectionMethods.php:
--------------------------------------------------------------------------------
1 | querySelectorAll(string $selector)
8 | * @method array querySelectorXPath(string $expression)
9 | */
10 | trait AliasesSelectionMethods
11 | {
12 | public function querySelector(...$arguments)
13 | {
14 | return $this->__call('$', $arguments);
15 | }
16 |
17 | public function querySelectorAll(...$arguments)
18 | {
19 | return $this->__call('$$', $arguments);
20 | }
21 |
22 | public function querySelectorXPath(...$arguments)
23 | {
24 | return $this->__call('$x', $arguments);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/doc-generator.ts:
--------------------------------------------------------------------------------
1 | import * as ts from 'typescript';
2 | const yargs = require('yargs/yargs');
3 | const { hideBin } = require('yargs/helpers');
4 |
5 | type ObjectMemberAsJson = { [key: string]: string; }
6 |
7 | type ObjectMembersAsJson = {
8 | properties: ObjectMemberAsJson,
9 | getters: ObjectMemberAsJson,
10 | methods: ObjectMemberAsJson,
11 | }
12 |
13 | type ClassAsJson = { name: string } & ObjectMembersAsJson
14 | type MemberContext = 'class'|'literal'
15 | type TypeContext = 'methodReturn'
16 |
17 | class TypeNotSupportedError extends Error {
18 | constructor(message?: string) {
19 | super(message || 'This type is currently not supported.');
20 | }
21 | }
22 |
23 | interface SupportChecker {
24 | supportsMethodName(methodName: string): boolean;
25 | }
26 |
27 | class JsSupportChecker {
28 | supportsMethodName(methodName: string): boolean {
29 | return true;
30 | }
31 | }
32 |
33 | class PhpSupportChecker {
34 | supportsMethodName(methodName: string): boolean {
35 | return !methodName.includes('$');
36 | }
37 | }
38 |
39 | interface DocumentationFormatter {
40 | formatProperty(name: string, type: string, context: MemberContext): string
41 | formatGetter(name: string, type: string): string
42 | formatAnonymousFunction(parameters: string, returnType: string): string
43 | formatFunction(name: string, parameters: string, returnType: string): string
44 | formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string
45 | formatTypeAny(): string
46 | formatTypeUnknown(): string
47 | formatTypeVoid(): string
48 | formatTypeUndefined(): string
49 | formatTypeNull(): string
50 | formatTypeBoolean(): string
51 | formatTypeNumber(): string
52 | formatTypeString(): string
53 | formatTypeReference(type: string): string
54 | formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string
55 | formatQualifiedName(left: string, right: string): string
56 | formatIndexedAccessType(object: string, index: string): string
57 | formatLiteralType(value: string): string
58 | formatUnion(types: string[]): string
59 | formatIntersection(types: string[]): string
60 | formatObject(members: string[]): string
61 | formatArray(type: string): string
62 | }
63 |
64 | class JsDocumentationFormatter implements DocumentationFormatter {
65 | formatProperty(name: string, type: string, context: MemberContext): string {
66 | return `${name}: ${type}`;
67 | }
68 |
69 | formatGetter(name: string, type: string): string {
70 | return `${name}: ${type}`;
71 | }
72 |
73 | formatAnonymousFunction(parameters: string, returnType: string): string {
74 | return `(${parameters}) => ${returnType}`;
75 | }
76 |
77 | formatFunction(name: string, parameters: string, returnType: string): string {
78 | return `${name}(${parameters}): ${returnType}`;
79 | }
80 |
81 | formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string {
82 | return `${isVariadic ? '...' : ''}${name}${isOptional ? '?' : ''}: ${type}`;
83 | }
84 |
85 | formatTypeAny(): string {
86 | return 'any';
87 | }
88 |
89 | formatTypeUnknown(): string {
90 | return 'unknown';
91 | }
92 |
93 | formatTypeVoid(): string {
94 | return 'void';
95 | }
96 |
97 | formatTypeUndefined(): string {
98 | return 'undefined';
99 | }
100 |
101 | formatTypeNull(): string {
102 | return 'null';
103 | }
104 |
105 | formatTypeBoolean(): string {
106 | return 'boolean';
107 | }
108 |
109 | formatTypeNumber(): string {
110 | return 'number';
111 | }
112 |
113 | formatTypeString(): string {
114 | return 'string';
115 | }
116 |
117 | formatTypeReference(type: string): string {
118 | return type;
119 | }
120 |
121 | formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string {
122 | return `${parentType}<${argumentTypes.join(', ')}>`;
123 | }
124 |
125 | formatQualifiedName(left: string, right: string): string {
126 | return `${left}.${right}`;
127 | }
128 |
129 | formatIndexedAccessType(object: string, index: string): string {
130 | return `${object}[${index}]`;
131 | }
132 |
133 | formatLiteralType(value: string): string {
134 | return `'${value}'`;
135 | }
136 |
137 | formatUnion(types: string[]): string {
138 | return types.join(' | ');
139 | }
140 |
141 | formatIntersection(types: string[]): string {
142 | return types.join(' & ');
143 | }
144 |
145 | formatObject(members: string[]): string {
146 | return `{ ${members.join(', ')} }`;
147 | }
148 |
149 | formatArray(type: string): string {
150 | return `${type}[]`;
151 | }
152 | }
153 |
154 | class PhpDocumentationFormatter implements DocumentationFormatter {
155 | static readonly allowedJsClasses = ['Promise', 'Record', 'Map'];
156 |
157 | constructor(
158 | private readonly resourcesNamespace: string,
159 | private readonly resources: string[],
160 | ) {}
161 |
162 | formatProperty(name: string, type: string, context: MemberContext): string {
163 | return context === 'class'
164 | ? `${type} ${name}`
165 | : `${name}: ${type}`;
166 | }
167 |
168 | formatGetter(name: string, type: string): string {
169 | return `${type} ${name}`;
170 | }
171 |
172 | formatAnonymousFunction(parameters: string, returnType: string): string {
173 | return `callable(${parameters}): ${returnType}`;
174 | }
175 |
176 | formatFunction(name: string, parameters: string, returnType: string): string {
177 | return `${returnType} ${name}(${parameters})`;
178 | }
179 |
180 | formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string {
181 | if (isVariadic && type.endsWith('[]')) {
182 | type = type.slice(0, -2);
183 | }
184 |
185 | const defaultValue = isOptional ? ' = null' : '';
186 | return `${type} ${isVariadic ? '...' : ''}\$${name}${defaultValue}`;
187 | }
188 |
189 | formatTypeAny(): string {
190 | return 'mixed';
191 | }
192 |
193 | formatTypeUnknown(): string {
194 | return 'mixed';
195 | }
196 |
197 | formatTypeVoid(): string {
198 | return 'void';
199 | }
200 |
201 | formatTypeUndefined(): string {
202 | return 'null';
203 | }
204 |
205 | formatTypeNull(): string {
206 | return 'null';
207 | }
208 |
209 | formatTypeBoolean(): string {
210 | return 'bool';
211 | }
212 |
213 | formatTypeNumber(): string {
214 | return 'float';
215 | }
216 |
217 | formatTypeString(): string {
218 | return 'string';
219 | }
220 |
221 | formatTypeReference(type: string): string {
222 | // Allow some specific JS classes to be used in phpDoc
223 | if (PhpDocumentationFormatter.allowedJsClasses.includes(type)) {
224 | return type;
225 | }
226 |
227 | // Prefix PHP resources with their namespace
228 | if (this.resources.includes(type)) {
229 | return `\\${this.resourcesNamespace}\\${type}`;
230 | }
231 |
232 | // If the type ends with "options" then convert it to an associative array
233 | if (/options$/i.test(type)) {
234 | return 'array';
235 | }
236 |
237 | // Types ending with "Fn" are always callables or strings
238 | if (type.endsWith('Fn')) {
239 | return this.formatUnion(['callable', 'string']);
240 | }
241 |
242 | if (type === 'Function') {
243 | return 'callable';
244 | }
245 |
246 | if (type === 'PuppeteerLifeCycleEvent') {
247 | return 'string';
248 | }
249 |
250 | if (type === 'Serializable') {
251 | return this.formatUnion(['int', 'float', 'string', 'bool', 'null', 'array']);
252 | }
253 |
254 | if (type === 'SerializableOrJSHandle') {
255 | return this.formatUnion([this.formatTypeReference('Serializable'), this.formatTypeReference('JSHandle')]);
256 | }
257 |
258 | if (type === 'HandleType') {
259 | return this.formatUnion([this.formatTypeReference('JSHandle'), this.formatTypeReference('ElementHandle')]);
260 | }
261 |
262 | return 'mixed';
263 | }
264 |
265 | formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string {
266 | // Avoid generics with "mixed" as parent type
267 | if (parentType === 'mixed') {
268 | return 'mixed';
269 | }
270 |
271 | // Unwrap promises for method return types
272 | if (context === 'methodReturn' && parentType === 'Promise' && argumentTypes.length === 1) {
273 | return argumentTypes[0];
274 | }
275 |
276 | // Transform Record and Map types to associative arrays
277 | if (['Record', 'Map'].includes(parentType) && argumentTypes.length === 2) {
278 | parentType = 'array';
279 | }
280 |
281 | return `${parentType}<${argumentTypes.join(', ')}>`;
282 | }
283 |
284 | formatQualifiedName(left: string, right: string): string {
285 | return `mixed`;
286 | }
287 |
288 | formatIndexedAccessType(object: string, index: string): string {
289 | return `mixed`;
290 | }
291 |
292 | formatLiteralType(value: string): string {
293 | return `'${value}'`;
294 | }
295 |
296 | private prepareUnionOrIntersectionTypes(types: string[]): string[] {
297 | // Replace "void" type by "null"
298 | types = types.map(type => type === 'void' ? 'null' : type)
299 |
300 | // Remove duplicates
301 | const uniqueTypes = new Set(types);
302 | return Array.from(uniqueTypes.values());
303 | }
304 |
305 | formatUnion(types: string[]): string {
306 | const result = this.prepareUnionOrIntersectionTypes(types).join('|');
307 |
308 | // Convert enums to string type
309 | if (/^('\w+'\|)*'\w+'$/.test(result)) {
310 | return 'string';
311 | }
312 |
313 | return result;
314 | }
315 |
316 | formatIntersection(types: string[]): string {
317 | return this.prepareUnionOrIntersectionTypes(types).join('&');
318 | }
319 |
320 | formatObject(members: string[]): string {
321 | return `array{ ${members.join(', ')} }`;
322 | }
323 |
324 | formatArray(type: string): string {
325 | return `${type}[]`;
326 | }
327 | }
328 |
329 | class DocumentationGenerator {
330 | constructor(
331 | private readonly supportChecker: SupportChecker,
332 | private readonly formatter: DocumentationFormatter,
333 | ) {}
334 |
335 | private hasModifierForNode(
336 | node: ts.Node,
337 | modifier: ts.KeywordSyntaxKind
338 | ): boolean {
339 | if (!node.modifiers) {
340 | return false;
341 | }
342 |
343 | return node.modifiers.some((node) => node.kind === modifier);
344 | }
345 |
346 | private isNodeAccessible(node: ts.Node): boolean {
347 | // @ts-ignore
348 | if (node.name && this.getNamedDeclarationAsString(node).startsWith('_')) {
349 | return false;
350 | }
351 |
352 | return (
353 | this.hasModifierForNode(node, ts.SyntaxKind.PublicKeyword) ||
354 | (!this.hasModifierForNode(node, ts.SyntaxKind.ProtectedKeyword) &&
355 | !this.hasModifierForNode(node, ts.SyntaxKind.PrivateKeyword))
356 | );
357 | }
358 |
359 | private isNodeStatic(node: ts.Node): boolean {
360 | return this.hasModifierForNode(node, ts.SyntaxKind.StaticKeyword);
361 | }
362 |
363 | public getClassDeclarationAsJson(node: ts.ClassDeclaration): ClassAsJson {
364 | return Object.assign(
365 | { name: this.getNamedDeclarationAsString(node) },
366 | this.getMembersAsJson(node.members, 'class'),
367 | );
368 | }
369 |
370 | private getMembersAsJson(members: ts.NodeArray, context: MemberContext): ObjectMembersAsJson {
371 | const json: ObjectMembersAsJson = {
372 | properties: {},
373 | getters: {},
374 | methods: {},
375 | };
376 |
377 | for (const member of members) {
378 | if (!this.isNodeAccessible(member) || this.isNodeStatic(member)) {
379 | continue;
380 | }
381 |
382 | const name = member.name ? this.getNamedDeclarationAsString(member) : null;
383 |
384 | if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
385 | json.properties[name] = this.getPropertySignatureOrDeclarationAsString(member, context);
386 | } else if (ts.isGetAccessorDeclaration(member)) {
387 | json.getters[name] = this.getGetAccessorDeclarationAsString(member);
388 | } else if (ts.isMethodDeclaration(member)) {
389 | if (!this.supportChecker.supportsMethodName(name)) {
390 | continue;
391 | }
392 | json.methods[name] = this.getSignatureDeclarationBaseAsString(member);
393 | }
394 | }
395 |
396 | return json;
397 | }
398 |
399 | private getPropertySignatureOrDeclarationAsString(
400 | node: ts.PropertySignature | ts.PropertyDeclaration,
401 | context: MemberContext
402 | ): string {
403 | const type = this.getTypeNodeAsString(node.type);
404 | const name = this.getNamedDeclarationAsString(node);
405 | return this.formatter.formatProperty(name, type, context);
406 | }
407 |
408 | private getGetAccessorDeclarationAsString(
409 | node: ts.GetAccessorDeclaration
410 | ): string {
411 | const type = this.getTypeNodeAsString(node.type);
412 | const name = this.getNamedDeclarationAsString(node);
413 | return this.formatter.formatGetter(name, type);
414 | }
415 |
416 | private getSignatureDeclarationBaseAsString(
417 | node: ts.SignatureDeclarationBase
418 | ): string {
419 | const name = node.name && this.getNamedDeclarationAsString(node);
420 | const parameters = node.parameters
421 | .map(parameter => this.getParameterDeclarationAsString(parameter))
422 | .join(', ');
423 |
424 | const returnType = this.getTypeNodeAsString(node.type, name ? 'methodReturn' : undefined);
425 |
426 | return name
427 | ? this.formatter.formatFunction(name, parameters, returnType)
428 | : this.formatter.formatAnonymousFunction(parameters, returnType);
429 | }
430 |
431 | private getParameterDeclarationAsString(node: ts.ParameterDeclaration): string {
432 | const name = this.getNamedDeclarationAsString(node);
433 | const type = this.getTypeNodeAsString(node.type);
434 | const isVariadic = node.dotDotDotToken !== undefined;
435 | const isOptional = node.questionToken !== undefined;
436 | return this.formatter.formatParameter(name, type, isVariadic, isOptional);
437 | }
438 |
439 | private getTypeNodeAsString(node: ts.TypeNode, context?: TypeContext): string {
440 | if (node.kind === ts.SyntaxKind.AnyKeyword) {
441 | return this.formatter.formatTypeAny();
442 | } else if (node.kind === ts.SyntaxKind.UnknownKeyword) {
443 | return this.formatter.formatTypeUnknown();
444 | } else if (node.kind === ts.SyntaxKind.VoidKeyword) {
445 | return this.formatter.formatTypeVoid();
446 | } else if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
447 | return this.formatter.formatTypeUndefined();
448 | } else if (node.kind === ts.SyntaxKind.NullKeyword) {
449 | return this.formatter.formatTypeNull();
450 | } else if (node.kind === ts.SyntaxKind.BooleanKeyword) {
451 | return this.formatter.formatTypeBoolean();
452 | } else if (node.kind === ts.SyntaxKind.NumberKeyword) {
453 | return this.formatter.formatTypeNumber();
454 | } else if (node.kind === ts.SyntaxKind.StringKeyword) {
455 | return this.formatter.formatTypeString();
456 | } else if (ts.isTypeReferenceNode(node)) {
457 | return this.getTypeReferenceNodeAsString(node, context);
458 | } else if (ts.isIndexedAccessTypeNode(node)) {
459 | return this.getIndexedAccessTypeNodeAsString(node);
460 | } else if (ts.isLiteralTypeNode(node)) {
461 | return this.getLiteralTypeNodeAsString(node);
462 | } else if (ts.isUnionTypeNode(node)) {
463 | return this.getUnionTypeNodeAsString(node, context);
464 | } else if (ts.isIntersectionTypeNode(node)) {
465 | return this.getIntersectionTypeNodeAsString(node, context);
466 | } else if (ts.isTypeLiteralNode(node)) {
467 | return this.getTypeLiteralNodeAsString(node);
468 | } else if (ts.isArrayTypeNode(node)) {
469 | return this.getArrayTypeNodeAsString(node, context);
470 | } else if (ts.isFunctionTypeNode(node)) {
471 | return this.getSignatureDeclarationBaseAsString(node);
472 | } else {
473 | throw new TypeNotSupportedError();
474 | }
475 | }
476 |
477 | private getTypeReferenceNodeAsString(node: ts.TypeReferenceNode, context?: TypeContext): string {
478 | return this.getGenericTypeReferenceNodeAsString(node, context) || this.getSimpleTypeReferenceNodeAsString(node);
479 | }
480 |
481 | private getGenericTypeReferenceNodeAsString(node: ts.TypeReferenceNode, context?: TypeContext): string | null {
482 | if (!node.typeArguments || node.typeArguments.length === 0) {
483 | return null;
484 | }
485 |
486 | const parentType = this.getSimpleTypeReferenceNodeAsString(node);
487 | const argumentTypes = node.typeArguments.map((node) => this.getTypeNodeAsString(node));
488 | return this.formatter.formatGeneric(parentType, argumentTypes, context);
489 | }
490 |
491 | private getSimpleTypeReferenceNodeAsString(node: ts.TypeReferenceNode): string {
492 | return ts.isIdentifier(node.typeName)
493 | ? this.formatter.formatTypeReference(this.getIdentifierAsString(node.typeName))
494 | : this.getQualifiedNameAsString(node.typeName);
495 | }
496 |
497 | private getQualifiedNameAsString(node: ts.QualifiedName): string {
498 | const right = this.getIdentifierAsString(node.right);
499 | const left = ts.isIdentifier(node.left)
500 | ? this.getIdentifierAsString(node.left)
501 | : this.getQualifiedNameAsString(node.left);
502 |
503 | return this.formatter.formatQualifiedName(left, right);
504 | }
505 |
506 | private getIndexedAccessTypeNodeAsString(
507 | node: ts.IndexedAccessTypeNode
508 | ): string {
509 | const object = this.getTypeNodeAsString(node.objectType);
510 | const index = this.getTypeNodeAsString(node.indexType);
511 | return this.formatter.formatIndexedAccessType(object, index);
512 | }
513 |
514 | private getLiteralTypeNodeAsString(node: ts.LiteralTypeNode): string {
515 | if (node.literal.kind === ts.SyntaxKind.NullKeyword) {
516 | return this.formatter.formatTypeNull();
517 | } else if (node.literal.kind === ts.SyntaxKind.BooleanKeyword) {
518 | return this.formatter.formatTypeBoolean();
519 | } else if (ts.isLiteralExpression(node.literal)) {
520 | return this.formatter.formatLiteralType(node.literal.text);
521 | }
522 | throw new TypeNotSupportedError();
523 | }
524 |
525 | private getUnionTypeNodeAsString(node: ts.UnionTypeNode, context?: TypeContext): string {
526 | const types = node.types.map(typeNode => this.getTypeNodeAsString(typeNode, context));
527 | return this.formatter.formatUnion(types);
528 | }
529 |
530 | private getIntersectionTypeNodeAsString(node: ts.IntersectionTypeNode, context?: TypeContext): string {
531 | const types = node.types.map(typeNode => this.getTypeNodeAsString(typeNode, context));
532 | return this.formatter.formatIntersection(types);
533 | }
534 |
535 | private getTypeLiteralNodeAsString(node: ts.TypeLiteralNode): string {
536 | const members = this.getMembersAsJson(node.members, 'literal');
537 | const stringMembers = Object.values(members).map(Object.values);
538 | const flattenMembers = stringMembers.reduce((acc, val) => acc.concat(val), []);
539 | return this.formatter.formatObject(flattenMembers);
540 | }
541 |
542 | private getArrayTypeNodeAsString(node: ts.ArrayTypeNode, context?: TypeContext): string {
543 | const type = this.getTypeNodeAsString(node.elementType, context);
544 | return this.formatter.formatArray(type);
545 | }
546 |
547 | private getNamedDeclarationAsString(node: ts.NamedDeclaration): string {
548 | if (!ts.isIdentifier(node.name)) {
549 | throw new TypeNotSupportedError();
550 | }
551 | return this.getIdentifierAsString(node.name);
552 | }
553 |
554 | private getIdentifierAsString(node: ts.Identifier): string {
555 | return String(node.escapedText);
556 | }
557 | }
558 |
559 | const { argv } = yargs(hideBin(process.argv))
560 | .command('$0 ')
561 | .option('resources-namespace', { type: 'string', default: '' })
562 | .option('resources', { type: 'array', default: [] })
563 | .option('pretty', { type: 'boolean', default: false })
564 |
565 | let supportChecker, formatter;
566 | switch (argv.language.toUpperCase()) {
567 | case 'JS':
568 | supportChecker = new JsSupportChecker();
569 | formatter = new JsDocumentationFormatter();
570 | break;
571 | case 'PHP':
572 | supportChecker = new PhpSupportChecker();
573 | formatter = new PhpDocumentationFormatter(argv.resourcesNamespace, argv.resources);
574 | break;
575 | default:
576 | console.error(`Unsupported "${argv.language}" language.`);
577 | process.exit(1);
578 | }
579 |
580 | const docGenerator = new DocumentationGenerator(supportChecker, formatter);
581 | const program = ts.createProgram(argv.definitionFiles, {});
582 | const classes = {};
583 |
584 | for (const fileName of argv.definitionFiles) {
585 | const sourceFile = program.getSourceFile(fileName);
586 |
587 | ts.forEachChild(sourceFile, node => {
588 | if (ts.isClassDeclaration(node)) {
589 | const classAsJson = docGenerator.getClassDeclarationAsJson(node);
590 | classes[classAsJson.name] = classAsJson;
591 | }
592 | });
593 | }
594 |
595 | process.stdout.write(JSON.stringify(classes, null, argv.pretty ? 2 : null));
596 |
--------------------------------------------------------------------------------
/src/get-puppeteer-version.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function output(value) {
4 | process.stdout.write(JSON.stringify(value));
5 | }
6 |
7 | try {
8 | const manifest = require('puppeteer/package.json');
9 | output(manifest.version);
10 | } catch (exception) {
11 | output(null);
12 | }
13 |
--------------------------------------------------------------------------------
/tests/DownloadTest.php:
--------------------------------------------------------------------------------
1 | serveResources();
18 |
19 | // Launch the browser to run tests on.
20 | $this->launchBrowser();
21 | }
22 |
23 | /**
24 | * Downloads an image and checks string length.
25 | *
26 | * @test
27 | */
28 | public function download_image()
29 | {
30 | // Download the image
31 | $page = $this->browser
32 | ->newPage()
33 | ->goto($this->url . '/puphpeteer-logo.png');
34 |
35 | $base64 = $page->buffer()->toString('base64');
36 | $imageString = base64_decode($base64);
37 |
38 | // Get the reference image from resources
39 | $reference = file_get_contents('tests/resources/puphpeteer-logo.png');
40 |
41 | $this->assertTrue(
42 | mb_strlen($reference) === mb_strlen($imageString),
43 | 'Image is not the same length after download.'
44 | );
45 | }
46 |
47 | /**
48 | * Downloads an image and checks string length.
49 | *
50 | * @test
51 | */
52 | // public function download_large_image()
53 | // {
54 | // // Download the image
55 | // $page = $this->browser
56 | // ->newPage()
57 | // ->goto($this->url . '/denys-barabanov-jKcFmXCfaQ8-unsplash.jpg');
58 |
59 | // $base64 = $page->buffer()->toString('base64');
60 | // $imageString = base64_decode($base64);
61 |
62 | // // Get the reference image from resources
63 | // $reference = file_get_contents('tests/resources/denys-barabanov-jKcFmXCfaQ8-unsplash.jpg');
64 |
65 | // $this->assertTrue(
66 | // mb_strlen($reference) === mb_strlen($imageString),
67 | // 'Large image is not the same length after download.'
68 | // );
69 | // }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/PuphpeteerTest.php:
--------------------------------------------------------------------------------
1 | serveResources();
19 |
20 | // Launch the browser to run tests on.
21 | $this->launchBrowser();
22 | }
23 |
24 | /** @test */
25 | public function can_browse_website()
26 | {
27 | $response = $this->browser->newPage()->goto($this->url);
28 |
29 | $this->assertTrue($response->ok(), 'Failed asserting that the response is successful.');
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function can_use_method_aliases()
36 | {
37 | $page = $this->browser->newPage();
38 |
39 | $page->goto($this->url);
40 |
41 | $select = function($resource) {
42 | $elements = [
43 | $resource->querySelector('h1'),
44 | $resource->querySelectorAll('h1')[0],
45 | $resource->querySelectorXPath('/html/body/h1')[0],
46 | ];
47 |
48 | $this->assertContainsOnlyInstancesOf(ElementHandle::class, $elements);
49 | };
50 |
51 | $evaluate = function($resource) {
52 | $strings = [
53 | $resource->querySelectorEval('h1', JsFunction::createWithBody('return "Hello World!";')),
54 | $resource->querySelectorAllEval('h1', JsFunction::createWithBody('return "Hello World!";')),
55 | ];
56 |
57 | foreach ($strings as $string) {
58 | $this->assertEquals('Hello World!', $string);
59 | }
60 | };
61 |
62 | // Test method aliases for Page, Frame and ElementHandle classes
63 | $resources = [$page, $page->mainFrame(), $page->querySelector('body')];
64 | foreach ($resources as $resource) {
65 | $select($resource);
66 | $evaluate($resource);
67 | }
68 | }
69 |
70 | /** @test */
71 | public function can_evaluate_a_selection()
72 | {
73 | $page = $this->browser->newPage();
74 |
75 | $page->goto($this->url);
76 |
77 | $title = $page->querySelectorEval('h1', JsFunction::createWithParameters(['node'])
78 | ->body('return node.textContent;'));
79 |
80 | $titleCount = $page->querySelectorAllEval('h1', JsFunction::createWithParameters(['nodes'])
81 | ->body('return nodes.length;'));
82 |
83 | $this->assertEquals('Example Page', $title);
84 | $this->assertEquals(1, $titleCount);
85 | }
86 |
87 | /** @test */
88 | public function can_intercept_requests()
89 | {
90 | $page = $this->browser->newPage();
91 |
92 | $page->setRequestInterception(true);
93 |
94 | $page->on('request', JsFunction::createWithParameters(['request'])
95 | ->body('request.resourceType() === "stylesheet" ? request.abort() : request.continue()'));
96 |
97 | $page->goto($this->url);
98 |
99 | $backgroundColor = $page->querySelectorEval('h1', JsFunction::createWithParameters(['node'])
100 | ->body('return getComputedStyle(node).textTransform'));
101 |
102 | $this->assertNotEquals('lowercase', $backgroundColor);
103 | }
104 |
105 | /**
106 | * @test
107 | * @dataProvider resourceProvider
108 | * @dontPopulateProperties browser
109 | */
110 | public function check_all_resources_are_supported(string $name)
111 | {
112 | $incompleteTest = false;
113 | $resourceInstantiator = new ResourceInstantiator($this->browserOptions, $this->url);
114 | $resource = $resourceInstantiator->{$name}(new Puppeteer, $this->browserOptions);
115 |
116 | if ($resource instanceof UntestableResource) {
117 | $incompleteTest = true;
118 | } else if ($resource instanceof RiskyResource) {
119 | if (!empty($resource->exception())) {
120 | $incompleteTest = true;
121 | } else {
122 | try {
123 | $this->assertInstanceOf("Nesk\\Puphpeteer\\Resources\\$name", $resource->value());
124 | } catch (ExpectationFailedException $exception) {
125 | $incompleteTest = true;
126 | }
127 | }
128 | } else {
129 | $this->assertInstanceOf("Nesk\\Puphpeteer\\Resources\\$name", $resource);
130 | }
131 |
132 | if (!$incompleteTest) return;
133 |
134 | $reason = "The \"$name\" resource has not been tested properly, probably"
135 | ." for a good reason but you might want to have a look: \n\n ";
136 |
137 | if ($resource instanceof UntestableResource) {
138 | $reason .= "\e[33mMarked as untestable.\e[0m";
139 | } else {
140 | if (!empty($exception = $resource->exception())) {
141 | $reason .= "\e[31mMarked as risky because of a Node error: {$exception->getMessage()}\e[0m";
142 | } else {
143 | $value = print_r($resource->value(), true);
144 | $reason .= "\e[31mMarked as risky because of an unexpected value: $value\e[0m";
145 | }
146 | }
147 |
148 | $this->markTestIncomplete($reason);
149 | }
150 |
151 | public function resourceProvider(): \Generator
152 | {
153 | $resourceNames = (new ResourceInstantiator([], ''))->getResourceNames();
154 |
155 | foreach ($resourceNames as $name) {
156 | yield [$name];
157 | }
158 | }
159 |
160 | private function createBrowserLogger(callable $onBrowserLog): LoggerInterface
161 | {
162 | $logger = $this->createMock(LoggerInterface::class);
163 | $logger->expects(self::atLeastOnce())
164 | ->method('log')
165 | ->willReturn(self::returnCallback(function (string $level, string $message) use ($onBrowserLog) {
166 | if (\strpos($message, "Received a Browser log:") === 0) {
167 | $onBrowserLog();
168 | }
169 |
170 | return null;
171 | }));
172 |
173 | return $logger;
174 | }
175 |
176 | /**
177 | * @test
178 | * @dontPopulateProperties browser
179 | */
180 | public function browser_console_calls_are_logged_if_enabled()
181 | {
182 | $browserLogOccured = false;
183 | $logger = $this->createBrowserLogger(function () use (&$browserLogOccured) {
184 | $browserLogOccured = true;
185 | });
186 |
187 | $puppeteer = new Puppeteer([
188 | 'log_browser_console' => true,
189 | 'logger' => $logger,
190 | ]);
191 |
192 | $this->browser = $puppeteer->launch($this->browserOptions);
193 | $this->browser->pages()[0]->goto($this->url);
194 |
195 | static::assertTrue($browserLogOccured);
196 | }
197 |
198 | /**
199 | * @test
200 | * @dontPopulateProperties browser
201 | */
202 | public function browser_console_calls_are_not_logged_if_disabled()
203 | {
204 | $browserLogOccured = false;
205 | $logger = $this->createBrowserLogger(function () use (&$browserLogOccured) {
206 | $browserLogOccured = true;
207 | });
208 |
209 | $puppeteer = new Puppeteer([
210 | 'log_browser_console' => false,
211 | 'logger' => $logger,
212 | ]);
213 |
214 | $this->browser = $puppeteer->launch($this->browserOptions);
215 | $this->browser->pages()[0]->goto($this->url);
216 |
217 | static::assertFalse($browserLogOccured);
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/tests/ResourceInstantiator.php:
--------------------------------------------------------------------------------
1 | browserOptions = $browserOptions;
13 | $this->url = $url;
14 |
15 | $this->resources = [
16 | 'Accessibility' => function ($puppeteer) {
17 | return $this->Page($puppeteer)->accessibility;
18 | },
19 | 'Browser' => function ($puppeteer) {
20 | return $puppeteer->launch($this->browserOptions);
21 | },
22 | 'BrowserContext' => function ($puppeteer) {
23 | return $this->Browser($puppeteer)->createIncognitoBrowserContext();
24 | },
25 | 'BrowserFetcher' => function ($puppeteer) {
26 | return $puppeteer->createBrowserFetcher();
27 | },
28 | 'CDPSession' => function ($puppeteer) {
29 | return $this->Target($puppeteer)->createCDPSession();
30 | },
31 | 'ConsoleMessage' => function () {
32 | return new UntestableResource;
33 | },
34 | 'Coverage' => function ($puppeteer) {
35 | return $this->Page($puppeteer)->coverage;
36 | },
37 | 'Dialog' => function () {
38 | return new UntestableResource;
39 | },
40 | 'ElementHandle' => function ($puppeteer) {
41 | return $this->Page($puppeteer)->querySelector('body');
42 | },
43 | 'EventEmitter' => function ($puppeteer) {
44 | return $puppeteer->launch($this->browserOptions);
45 | },
46 | 'ExecutionContext' => function ($puppeteer) {
47 | return $this->Frame($puppeteer)->executionContext();
48 | },
49 | 'FileChooser' => function () {
50 | return new UntestableResource;
51 | },
52 | 'Frame' => function ($puppeteer) {
53 | return $this->Page($puppeteer)->mainFrame();
54 | },
55 | 'HTTPRequest' => function ($puppeteer) {
56 | return $this->HTTPResponse($puppeteer)->request();
57 | },
58 | 'HTTPResponse' => function ($puppeteer) {
59 | return $this->Page($puppeteer)->goto($this->url);
60 | },
61 | 'JSHandle' => function ($puppeteer) {
62 | return $this->Page($puppeteer)->evaluateHandle(JsFunction::createWithBody('window'));
63 | },
64 | 'Keyboard' => function ($puppeteer) {
65 | return $this->Page($puppeteer)->keyboard;
66 | },
67 | 'Mouse' => function ($puppeteer) {
68 | return $this->Page($puppeteer)->mouse;
69 | },
70 | 'Page' => function ($puppeteer) {
71 | return $this->Browser($puppeteer)->newPage();
72 | },
73 | 'SecurityDetails' => function ($puppeteer) {
74 | return new RiskyResource(function () use ($puppeteer) {
75 | return $this->Page($puppeteer)->goto('https://example.com')->securityDetails();
76 | });
77 | },
78 | 'Target' => function ($puppeteer) {
79 | return $this->Page($puppeteer)->target();
80 | },
81 | 'TimeoutError' => function () {
82 | return new UntestableResource;
83 | },
84 | 'Touchscreen' => function ($puppeteer) {
85 | return $this->Page($puppeteer)->touchscreen;
86 | },
87 | 'Tracing' => function ($puppeteer) {
88 | return $this->Page($puppeteer)->tracing;
89 | },
90 | 'WebWorker' => function ($puppeteer) {
91 | $page = $this->Page($puppeteer);
92 | $page->goto($this->url, ['waitUntil' => 'networkidle0']);
93 | return $page->workers()[0];
94 | },
95 | ];
96 | }
97 |
98 | public function getResourceNames(): array
99 | {
100 | return array_keys($this->resources);
101 | }
102 |
103 | public function __call(string $name, array $arguments)
104 | {
105 | if (!isset($this->resources[$name])) {
106 | throw new \InvalidArgumentException("The $name resource is not supported.");
107 | }
108 |
109 | return $this->resources[$name](...$arguments);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/tests/RiskyResource.php:
--------------------------------------------------------------------------------
1 | value = $resourceRetriever();
15 | } catch (NodeFatalException $exception) {
16 | $this->exception = $exception;
17 | }
18 | }
19 |
20 | public function value() {
21 | return $this->value;
22 | }
23 |
24 | public function exception(): ?NodeFatalException {
25 | return $this->exception;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | getName())[0] ?? '';
23 | $testMethod = new \ReflectionMethod($this, $methodName);
24 | $docComment = $testMethod->getDocComment();
25 |
26 | if (preg_match('/@dontPopulateProperties (.*)/', $docComment, $matches)) {
27 | $this->dontPopulateProperties = array_values(array_filter(explode(' ', $matches[1])));
28 | }
29 | }
30 |
31 | /**
32 | * Stops the browser and local server
33 | */
34 | public function tearDown(): void
35 | {
36 | // Close the browser.
37 | if (isset($this->browser)) {
38 | $this->browser->close();
39 | }
40 |
41 | // Shutdown the local server
42 | if (isset($this->servingProcess)) {
43 | $this->servingProcess->stop(0);
44 | }
45 | }
46 |
47 | /**
48 | * Serves the resources folder locally on port 8089
49 | */
50 | protected function serveResources(): void
51 | {
52 | // Spin up a local server to deliver the resources.
53 | $this->host = '127.0.0.1:8089';
54 | $this->url = "http://{$this->host}";
55 | $this->serverDir = __DIR__.'/resources';
56 |
57 | $this->servingProcess = new Process(['php', '-S', $this->host, '-t', $this->serverDir]);
58 | $this->servingProcess->start();
59 | }
60 |
61 | /**
62 | * Launches the PuPHPeteer-controlled browser
63 | */
64 | protected function launchBrowser(): void
65 | {
66 | /**
67 | * Chrome doesn't support Linux sandbox on many CI environments
68 | *
69 | * @see: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-fails-due-to-sandbox-issues
70 | */
71 | $this->browserOptions = [
72 | 'args' => ['--no-sandbox', '--disable-setuid-sandbox'],
73 | ];
74 |
75 | if ($this->canPopulateProperty('browser')) {
76 | $this->browser = (new Puppeteer)->launch($this->browserOptions);
77 | }
78 | }
79 |
80 | public function canPopulateProperty(string $propertyName): bool
81 | {
82 | return !in_array($propertyName, $this->dontPopulateProperties);
83 | }
84 |
85 | public function isLogLevel(): Callback {
86 | $psrLogLevels = (new ReflectionClass(LogLevel::class))->getConstants();
87 | $monologLevels = (new ReflectionClass(Logger::class))->getConstants();
88 | $monologLevels = array_intersect_key($monologLevels, $psrLogLevels);
89 |
90 | return $this->callback(function ($level) use ($psrLogLevels, $monologLevels) {
91 | if (is_string($level)) {
92 | return in_array($level, $psrLogLevels, true);
93 | } else if (is_int($level)) {
94 | return in_array($level, $monologLevels, true);
95 | }
96 |
97 | return false;
98 | });
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/UntestableResource.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Document
5 |
6 |
7 |
8 |
9 |
10 | Example Page
11 |
12 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/resources/puphpeteer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rialto-php/puphpeteer/ac3705e876ec4a6fb844c423be89638df35fa78e/tests/resources/puphpeteer-logo.png
--------------------------------------------------------------------------------
/tests/resources/stylesheet.css:
--------------------------------------------------------------------------------
1 | h1 {
2 | text-transform: lowercase;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/resources/worker.js:
--------------------------------------------------------------------------------
1 | // There's nothing to do, just wait.
2 |
--------------------------------------------------------------------------------