Repository: zoonru/puphpeteer Branch: zoon Commit: 1c35dc91fc6b Files: 64 Total size: 122.6 KB Directory structure: gitextract_n_rqg4xg/ ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin/ │ └── console ├── composer.json ├── examples/ │ ├── 01_page_open.php │ ├── 02_page_screenshot.php │ └── 03_browserless.php ├── package.json ├── phpstan.neon ├── phpunit.xml ├── src/ │ ├── Command/ │ │ └── GenerateDocumentationCommand.php │ ├── Puppeteer.php │ ├── PuppeteerConnectionDelegate.js │ ├── PuppeteerProcessDelegate.php │ ├── Resources/ │ │ ├── Accessibility.php │ │ ├── Browser.php │ │ ├── BrowserContext.php │ │ ├── BrowserLauncher.php │ │ ├── BrowserWebSocketTransport.php │ │ ├── Buffer.php │ │ ├── CDPSession.php │ │ ├── ConsoleMessage.php │ │ ├── Coverage.php │ │ ├── DevToolsTarget.php │ │ ├── Dialog.php │ │ ├── ElementHandle.php │ │ ├── EventEmitter.php │ │ ├── FileChooser.php │ │ ├── Frame.php │ │ ├── HTTPRequest.php │ │ ├── HTTPResponse.php │ │ ├── JSHandle.php │ │ ├── Keyboard.php │ │ ├── Mouse.php │ │ ├── OtherTarget.php │ │ ├── Page.php │ │ ├── PageTarget.php │ │ ├── Realm.php │ │ ├── ScreenRecorder.php │ │ ├── SecurityDetails.php │ │ ├── Target.php │ │ ├── TaskManager.php │ │ ├── TimeoutError.php │ │ ├── TimeoutSettings.php │ │ ├── Touchscreen.php │ │ ├── Tracing.php │ │ ├── Uint8Array.php │ │ ├── WaitTask.php │ │ ├── WebWorker.php │ │ └── WorkerTarget.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/ ├── index.html ├── stylesheet.css └── worker.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). **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. ## [Unreleased] _In progress…_ ## [2.0.0] - 2020-12-01 ### Added - Support Puppeteer v5.5 - Support PHP 8 - Add documentation on all resources to provide autocompletion in IDEs ### Removed - Drop support for PHP 7.1 and 7.2 ## [1.6.0] - 2019-07-01 ### Added - Support Puppeteer v1.18 ## [1.5.0] - 2019-03-17 ### Added - Support Puppeteer v1.13 - Make the `ElementHandle` resource extend the `JSHandle` one ### Fixed - Add missing `Accessibility` resource ## [1.4.1] - 2018-11-27 ### Added - Support Puppeteer v1.10 ## [1.4.0] - 2018-09-22 ### Added - Support Puppeteer v1.8 ### Changed - Detect resource types by using the constructor name ### Fixed - Logs of initial pages are now retrieved ## [1.3.0] - 2018-08-20 ### Added - 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 - Support Puppeteer v1.7 ## [1.2.0] - 2018-07-25 ### Added - Support Puppeteer v1.6 ### Changed - Upgrade to Rialto v1.1 ## [1.1.0] - 2018-06-12 ### Added - Support Puppeteer v1.5 - Add aliases for evaluation methods to the `ElementHandle` resource - Support new Puppeteer v1.5 resources: - `BrowserContext` - `Worker` ### Fixed - Fix Travis tests ## [1.0.0] - 2018-06-05 ### Changed - Change PHP's vendor name from `extractr-io` to `nesk` - Change NPM's scope name from `@extractr-io` to `@nesk` - Upgrade to Rialto v1 ## [0.2.2] - 2018-04-20 ### Added - Support Puppeteer v1.3 - Test missing Puppeteer resources: `ConsoleMessage` and `Dialog` - Show a warning in logs if Puppeteer's version doesn't match requirements ## [0.2.1] - 2018-04-09 ### Changed - Update Rialto version requirements ## [0.2.0] - 2018-02-19 ### Added - Support new Puppeteer v1.1 resources: - `BrowserFetcher` - `CDPSession` - `Coverage` - `SecurityDetails` - Test Puppeteer resources - Support PHPUnit v7 - Add Travis integration ### Changed - Lock Puppeteer's version to v1.1 to avoid issues with forward compatibility ## 0.1.0 - 2018-01-29 First release [Unreleased]: https://github.com/nesk/puphpeteer/compare/2.0.0...HEAD [2.0.0]: https://github.com/nesk/puphpeteer/compare/1.6.0...2.0.0 [1.6.0]: https://github.com/nesk/puphpeteer/compare/1.5.0...1.6.0 [1.5.0]: https://github.com/nesk/puphpeteer/compare/1.4.1...1.5.0 [1.4.1]: https://github.com/nesk/puphpeteer/compare/1.4.0...1.4.1 [1.4.0]: https://github.com/nesk/puphpeteer/compare/1.3.0...1.4.0 [1.3.0]: https://github.com/nesk/puphpeteer/compare/1.2.0...1.3.0 [1.2.0]: https://github.com/nesk/puphpeteer/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/nesk/puphpeteer/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/nesk/puphpeteer/compare/0.2.2...1.0.0 [0.2.2]: https://github.com/nesk/puphpeteer/compare/0.2.1...0.2.2 [0.2.1]: https://github.com/nesk/puphpeteer/compare/0.2.0...0.2.1 [0.2.0]: https://github.com/nesk/puphpeteer/compare/0.1.0...0.2.0 ================================================ FILE: LICENSE ================================================ Copyright (c) Johann Pardanaud 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. ================================================ FILE: README.md ================================================ # PuPHPeteer [![PHP Version](https://img.shields.io/packagist/php-v/zoon/puphpeteer.svg?style=flat-square)](http://php.net/) [![Composer Version](https://img.shields.io/packagist/v/zoon/puphpeteer.svg?style=flat-square&label=Composer)](https://packagist.org/packages/zoon/puphpeteer) A [Puppeteer](https://github.com/GoogleChrome/puppeteer/) bridge for PHP, supporting the entire API. Based on [Rialto](https://github.com/zoonru/rialto/), a package to manage Node resources from PHP. 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: **Example** - navigating to https://example.com and saving a screenshot as *example.png*: ```php use Nesk\Puphpeteer\Puppeteer; $puppeteer = new Puppeteer; $browser = $puppeteer->launch(); $page = $browser->newPage(); $page->goto('https://example.com'); $page->screenshot(['path' => 'example.png']); $browser->close(); ``` **Example** - evaluate a script in the context of the page: ```php use Nesk\Puphpeteer\Puppeteer; use Nesk\Rialto\Data\JsFunction; $puppeteer = new Puppeteer; $browser = $puppeteer->launch(); $page = $browser->newPage(); $page->goto('https://example.com'); // Get the "viewport" of the page, as reported by the page. $dimensions = $page->evaluate(JsFunction::createWithBody(" return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio }; ")); printf('Dimensions: %s', print_r($dimensions, true)); $browser->close(); ``` ## Requirements and installation This package requires PHP >= 7.3 and Node >= 8. Install it with these two command lines: ```shell composer require zoon/puphpeteer npm install git+https://git@github.com/zoonru/puphpeteer.git#zoon ``` ## Use with browserless ```shell docker run --rm -p 3000:3000 ghcr.io/browserless/chrome ``` ```php $puppeteer = new Nesk\Puphpeteer\Puppeteer; $options = [ 'headless' => false, 'stealth'=> true, 'timeout'=> 5000, 'args' => [ '--window-size=1366,768', ], ]; $browser = $puppeteer->connect(['browserWSEndpoint' => 'ws://127.0.0.1:3000/chrome?launch='.urlencode(json_encode($options, JSON_UNESCAPED_UNICODE))]); $page = $browser->newPage(); $page->goto('https://www.example.com'); $page->screenshot(['path' => 'example.png']); $browser->close(); ``` ## Notable differences between PuPHPeteer and Puppeteer ### Puppeteer's class must be instantiated Instead of requiring Puppeteer: ```js const puppeteer = require('puppeteer'); ``` You have to instantiate the `Puppeteer` class: ```php $puppeteer = new Puppeteer; ``` This will create a new Node process controlled by PHP. 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: ```php [ // Logs the output of Browser's console methods (console.log, console.debug, etc...) to the PHP logger 'log_browser_console' => false, ] ```
⏱ Want to use some timeouts higher than 30 seconds in Puppeteer's API?
If you use some timeouts higher than 30 seconds, you will have to set a higher value for the `read_timeout` option (default: `35`): ```php $puppeteer = new Puppeteer([ 'read_timeout' => 65, // In seconds ]); $puppeteer->launch()->newPage()->goto($url, [ 'timeout' => 60000, // In milliseconds ]); ```
### No need to use the `await` keyword With PuPHPeteer, every method call or property getting/setting is synchronous. ### Some methods have been aliased The following methods have been aliased because PHP doesn't support the `$` character in method names: - `$` => `querySelector` - `$$` => `querySelectorAll` - `$eval` => `querySelectorEval` - `$$eval` => `querySelectorAllEval` Use these aliases just like you would have used the original methods: ```php $divs = $page->querySelectorAll('div'); // Runs the `//h2` as the XPath expression. $xpath = $page->querySelectorAll('::-p-xpath(//h2)'); // div element that has Checkout as the inner text. $text = $page->querySelector('div ::-p-text(Checkout)'); ``` ### Evaluated functions must be created with `JsFunction` 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. ```php use Nesk\Rialto\Data\JsFunction; $pageFunction = JsFunction::createWithParameters(['element']) ->body("return element.textContent"); ``` ### Exceptions must be caught with `->tryCatch` 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`. To avoid that, you can ask Node to catch these errors by prepending your instruction with `->tryCatch`: ```php use Nesk\Rialto\Exceptions\Node; try { $page->tryCatch->goto('invalid_url'); } catch (Node\Exception $exception) { // Handle the exception... } ``` Instead, a `Node\Exception` will be thrown, the Node process will stay alive and usable. ### Puppeteer plugins Puppeteer-extra and puppeteer-extra-plugin-stealth plugins already added in npm requirements. To use them, override js inclusion with js_extra option ```php $puppeteer = new Puppeteer([ 'js_extra' => /** @lang JavaScript */ " const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); instruction.setDefaultResource(puppeteer); " ]); ``` ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. ## Logo attribution PuPHPeteer's logo is composed of: - [Puppet](https://thenounproject.com/search/?q=puppet&i=52120) by Luis Prado from [the Noun Project](http://thenounproject.com/). - [Elephant](https://thenounproject.com/search/?q=elephant&i=954119) by Lluisa Iborra from [the Noun Project](http://thenounproject.com/). Thanks to [Laravel News](https://laravel-news.com/) for picking the icons and colors of the logo. ================================================ FILE: bin/console ================================================ #!/usr/bin/env php add(new GenerateDocumentationCommand) ->getApplication() ->run(); ================================================ FILE: composer.json ================================================ { "name": "zoon/puphpeteer", "description": "A Puppeteer bridge for PHP, supporting the entire API.", "keywords": [ "php", "puppeteer", "headless-chrome", "testing", "web", "developer-tools", "automation" ], "type": "library", "license": "MIT", "authors": [ { "name": "Johann Pardanaud", "email": "pardanaud.j@gmail.com" } ], "require": { "php": "^8.1", "ext-json": "*", "composer/semver": "^3.0", "psr/log": "^3.0", "zoon/rialto": "^1.6" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "monolog/monolog": "^3.0", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11", "symfony/console": "^7", "symfony/filesystem": "^7", "symfony/process": "^7", "symfony/var-dumper": "^7" }, "autoload": { "psr-4": { "Nesk\\Puphpeteer\\": "src/" } }, "autoload-dev": { "psr-4": { "Nesk\\Puphpeteer\\Tests\\": "tests/" } }, "scripts": { "post-install-cmd": "npm install", "test": "./vendor/bin/phpunit", "update-docs": "php bin/console doc:generate", "stan": "vendor/bin/phpstan analyze src", "format": "vendor/bin/php-cs-fixer fix src" }, "config": { "sort-packages": true } } ================================================ FILE: examples/01_page_open.php ================================================ launch(); $page = $browser->newPage(); $page->goto('https://example.com'); // Get the "viewport" of the page, as reported by the page. $dimensions = $page->evaluate(JsFunction::createWithBody(/** @lang JavaScript */" return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio }; ")); printf('Dimensions: %s', print_r($dimensions, true)); $browser->close(); ================================================ FILE: examples/02_page_screenshot.php ================================================ launch(); $page = $browser->newPage(); $page->setViewport(['width' => 1366, 'height' => 768]); $page->goto('https://example.com'); $page->screenshot(['path' => 'example.png']); $browser->close(); ================================================ FILE: examples/03_browserless.php ================================================ /** @lang JavaScript */ " const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); instruction.setDefaultResource(puppeteer); ", 'idle_timeout' => 90, 'read_timeout' => 120, 'log_browser_console' => true, 'log_node_console' => true, 'logger' => new Symfony\Component\Console\Logger\ConsoleLogger(new ConsoleOutput(\Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE)), ]); $options = [ 'timeout' => 120000, 'token' => 'abc', 'launch' => json_encode([ 'headless' => false, 'stealth'=> true, 'blockAds' => false, 'timeout'=> 600_000, 'args' => [ '--window-size=1366,768', '--lang=ru-RU', '--incognito', '--0', ], ], JSON_UNESCAPED_UNICODE), ]; $browser = $puppeteer->connect([ 'browserWSEndpoint' => sprintf( 'ws://127.0.0.1:3000/chrome?%s', http_build_query($options) ), 'ignoreHTTPSErrors' => true, 'ignoreDefaultArgs' => true, ]); $page = $browser->newPage(); $page->setViewport(['width' => 1366, 'height' => 768]); $page->goto('https://bot.sannysoft.com', ['timeout' => 10_000, 'waitUntil' => 'domcontentloaded']); $page->waitForNetworkIdle(['idleTime' => 1_000, 'concurrency' => 1]); // Get the "viewport" of the page, as reported by the page. $dimensions = $page->evaluate(JsFunction::createWithBody(/** @lang JavaScript */" return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio }; ")); printf('Dimensions: %s', print_r($dimensions, true)); $page->screenshot(['path' => 'example_browserless.png', "fullPage" => true]); $browser->close(); } finally { `docker stop $dockerId`; } ================================================ FILE: package.json ================================================ { "name": "@zoon/puphpeteer", "version": "2.4.3", "description": "A Puppeteer bridge for PHP, supporting the entire API.", "keywords": [ "php", "puppeteer", "headless-chrome", "testing", "web", "developer-tools", "automation" ], "author": { "name": "Johann Pardanaud", "email": "pardanaud.j@gmail.com", "url": "https://johann.pardanaud.com/" }, "license": "MIT", "repository": "git+https://git@github.com/zoonru/puphpeteer.git#zoon", "engines": { "node": ">=9.0.0" }, "dependencies": { "@zoon/rialto": "git+https://git@github.com/zoonru/rialto.git#zoon", "puppeteer": "24.36.1", "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "devDependencies": { "@types/yargs": "^15.0.10", "typescript": "^5", "yargs": "^16.1.1" } } ================================================ FILE: phpstan.neon ================================================ parameters: level: 4 fileExtensions: - php ================================================ FILE: phpunit.xml ================================================ tests ================================================ FILE: src/Command/GenerateDocumentationCommand.php ================================================ addOption( 'puppeteerPath', null, InputOption::VALUE_OPTIONAL, 'The path where Puppeteer is installed.', self::NODE_MODULES_DIR.'/puppeteer-core' ); } /** * Builds the documentation generator from TypeScript to JavaScript. */ private static function buildDocumentationGenerator(): void { self::rmdirRecursive(self::BUILD_DIR); $process = new Process([ self::NODE_MODULES_DIR.'/.bin/tsc', '--outDir', self::BUILD_DIR, __DIR__.'/../../src/'.self::DOC_FILE_NAME.'.ts', ]); $process->run(); } /** * Gets the documentation from the TypeScript documentation generator. */ private static function getDocumentation(string $puppeteerPath, array $resourceNames): array { self::buildDocumentationGenerator(); $files = \glob($puppeteerPath . '/lib/esm/puppeteer/{common,node,api,cdp}/*.d.ts', GLOB_BRACE); if ($files === false) { $error = \error_get_last(); throw new \ErrorException($error['message'] ?? 'An error occurred', 0, $error['type'] ?? 1); } $result = []; foreach (self::DOC_FORMATS as $format) { $process = new Process( array_merge( ['node', self::BUILD_DIR.'/'.self::DOC_FILE_NAME.'.js', $format], $files, ['--resources-namespace', self::RESOURCES_NAMESPACE, '--resources'], $resourceNames ) ); $process->mustRun(); echo $process->getErrorOutput().\PHP_EOL; $data = \json_decode($process->getOutput(), true); if (JSON_ERROR_NONE !== \json_last_error()) { throw new \JsonException(json_last_error_msg(), json_last_error()); } foreach ($data as &$class) { $result[$class['name']]['name'] = $class['name']; $result[$class['name']][$format] = [ 'properties' => $class['properties'], 'getters' => $class['getters'], 'methods' => $class['methods'], ]; } } return $result; } private static function getResourceNames(): array { return array_map(static function (string $filePath): string { return explode('.', basename($filePath))[0]; }, glob(self::RESOURCES_DIR.'/*')); } private static function generatePhpDocWithDocumentation(array $classDocumentation): ?string { $properties = array_map(function (string $property): string { return "\n * @property {$property}"; }, $classDocumentation[self::DOC_FORMAT_PHP]['properties']); $properties = implode('', $properties); $getters = array_map(function (string $getter): string { return "\n * @property-read {$getter}"; }, $classDocumentation[self::DOC_FORMAT_PHP]['getters']); $getters = implode('', $getters); $methods = ''; foreach ($classDocumentation[self::DOC_FORMAT_PHP]['methods'] as $pos => $method) { $methods .= "\n * @method {$method}"; $phpStanMethod = $classDocumentation[self::DOC_FORMAT_PHPSTAN]['methods'][$pos]; // phpStorm works incorrectly if @phpstan-method is used. // Using non-standard method-extended phpDoc: $methods .= "\n * @method-extended {$phpStanMethod}"; } if ('' !== $properties || '' !== $getters || '' !== $methods) { return "/**{$properties}{$getters}{$methods}\n */"; } return null; } /** * Writes the doc comment in the PHP class. */ private static function writePhpDoc(string $className, string $phpDoc): void { $reflectionClass = new \ReflectionClass($className); if (! $reflectionClass) { return; } $fileName = $reflectionClass->getFileName(); $contents = file_get_contents($fileName); // If there already is a doc comment, replace it. if ($doc = $reflectionClass->getDocComment()) { $newContents = str_replace($doc, $phpDoc, $contents); } else { $startLine = $reflectionClass->getStartLine(); $lines = explode("\n", $contents); $before = \array_slice($lines, 0, $startLine - 1); $after = \array_slice($lines, $startLine - 1); $newContents = implode("\n", array_merge($before, explode("\n", $phpDoc), $after)); } file_put_contents($fileName, $newContents); } /** * Executes the current command. */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $resourceNames = self::getResourceNames(); $documentation = self::getDocumentation($input->getOption('puppeteerPath'), $resourceNames); foreach ($resourceNames as $resourceName) { $classDocumentation = $documentation[$resourceName] ?? null; if (null !== $classDocumentation) { $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation); if (null !== $phpDoc) { $resourceClass = self::RESOURCES_NAMESPACE.'\\'.$resourceName; self::writePhpDoc($resourceClass, $phpDoc); } } } // Handle the specific Puppeteer class $classDocumentation = array_replace_recursive($documentation['Puppeteer'], $documentation['PuppeteerNode']); unset($documentation['Puppeteer'], $documentation['PuppeteerNode'], $resourceNames[array_search('Puppeteer', $resourceNames, true)]); if (null !== $classDocumentation) { $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation); if (null !== $phpDoc) { self::writePhpDoc(Puppeteer::class, $phpDoc); } } $missingResources = array_diff(array_keys($documentation), $resourceNames); foreach ($missingResources as $resource) { $io->warning("The {$resource} class in Puppeteer doesn't have any equivalent in PuPHPeteer."); } $inexistantResources = array_diff($resourceNames, array_keys($documentation)); foreach ($inexistantResources as $resource) { $io->error("The {$resource} resource doesn't have any equivalent in Puppeteer."); } return 0; } private static function rmdirRecursive(string $dir): bool { $files = scandir($dir); if (! \is_array($files)) { return false; } $files = array_diff($files, ['.', '..']); foreach ($files as $file) { (is_dir("{$dir}/{$file}")) ? self::rmdirRecursive("{$dir}/{$file}") : unlink("{$dir}/{$file}"); } return rmdir($dir); } } ================================================ FILE: src/Puppeteer.php ================================================ $options) * @method \Nesk\Puphpeteer\Resources\Browser launch(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\Browser launch(array $options = null) * @method string executablePath() * @method-extended string executablePath() * @method string[] defaultArgs(array $options = []) * @method-extended string[] defaultArgs(array $options = null) * @method void trimCache() * @method-extended void trimCache() */ class Puppeteer extends AbstractEntryPoint { /** * Default options. * * @var array */ protected $options = [ 'read_timeout' => 30, // Logs the output of Browser's console methods (console.log, console.debug, etc...) to the PHP logger 'log_browser_console' => false, // Custom js to load puppeteer-extra plugins 'js_extra' => '', ]; /** * Instantiate Puppeteer's entry point. */ public function __construct(array $userOptions = []) { if (! empty($userOptions['logger']) && $userOptions['logger'] instanceof LoggerInterface) { $this->checkPuppeteerVersion($userOptions['executable_path'] ?? 'node', $userOptions['logger']); } parent::__construct( __DIR__.'/PuppeteerConnectionDelegate.js', new PuppeteerProcessDelegate(), $this->options, $userOptions ); } private function checkPuppeteerVersion(string $nodePath, LoggerInterface $logger): void { $currentVersion = $this->currentPuppeteerVersion($nodePath); $acceptedVersions = $this->acceptedPuppeteerVersion(); if (null === $currentVersion) { $logger->warning("Puppeteer doesn't seem to be installed."); return; } if (! Semver::satisfies($currentVersion, $acceptedVersions)) { $logger->warning( "The installed version of Puppeteer (v{$currentVersion}) doesn't match the requirements" ." ({$acceptedVersions}), you may encounter issues." ); } } private function currentPuppeteerVersion(string $nodePath): ?string { $process = new Process([$nodePath, __DIR__.'/get-puppeteer-version.js']); $process->mustRun(); return json_decode($process->getOutput(), true, 10, JSON_THROW_ON_ERROR); } private function acceptedPuppeteerVersion(): string { $npmManifestPath = __DIR__.'/../package.json'; $contents = file_get_contents($npmManifestPath) ?: throw new Exception('Cant load file'); $npmManifest = json_decode($contents, false, 10, JSON_THROW_ON_ERROR); return $npmManifest->dependencies->puppeteer; } } ================================================ FILE: src/PuppeteerConnectionDelegate.js ================================================ "use strict"; const { ConnectionDelegate } = require("@zoon/rialto"), Logger = require("@zoon/rialto/src/node-process/Logger"), ConsoleInterceptor = require("@zoon/rialto/src/node-process/NodeInterceptors/ConsoleInterceptor"), StandardStreamsInterceptor = require("@zoon/rialto/src/node-process/NodeInterceptors/StandardStreamsInterceptor"); /** * Handle the requests of a connection to control Puppeteer. */ class PuppeteerConnectionDelegate extends ConnectionDelegate { /** * Constructor. * * @param {Object} options */ constructor(options) { super(options); this.browsers = new Set(); this.addSignalEventListeners(); } /** * @inheritdoc */ async handleInstruction(instruction, responseHandler, errorHandler) { if (this.options.js_extra) { eval(this.options.js_extra); } else { const puppeteer = require("puppeteer"); instruction.setDefaultResource(puppeteer); } let value = null; try { value = await instruction.execute(); } catch (error) { if (instruction.shouldCatchErrors()) { return errorHandler(error); } throw error; } if (this.isInstanceOf(value, "Browser")) { this.browsers.add(value); if (this.options.log_browser_console === true) { const initialPages = await value.pages(); initialPages.forEach((page) => page.on("console", this.logConsoleMessage) ); } } if ( this.options.log_browser_console === true && this.isInstanceOf(value, "Page") ) { value.on("console", this.logConsoleMessage); } if (this.isInstanceOf(value, Uint8Array.name)) { value = Buffer.from(value) } responseHandler(value); } /** * Checks if a value is an instance of a class. The check must be done with the `[object].constructor.name` * property because relying on Puppeteer's constructors isn't viable since the exports aren't constrained by semver. * * @protected * @param {*} value * @param {string} className * * @see {@link https://github.com/GoogleChrome/puppeteer/issues/3067|Puppeteer's issue about semver on exports} */ isInstanceOf(value, className) { const nonObjectValues = [undefined, null]; return ( !nonObjectValues.includes(value) && !nonObjectValues.includes(value.constructor) && ( value.constructor.name === className || value.constructor.name === "Cdp" + className || value.constructor.name === "CDP" + className ) ); } /** * Log the console message. * * @param {ConsoleMessage} consoleMessage */ async logConsoleMessage(consoleMessage) { const type = consoleMessage.type(); if (!ConsoleInterceptor.typeIsSupported(type)) { return; } const level = ConsoleInterceptor.getLevelFromType(type); const args = await Promise.all( consoleMessage.args().map((arg) => arg.jsonValue()) ); StandardStreamsInterceptor.startInterceptingStrings((message) => { Logger.log("Browser", level, ConsoleInterceptor.formatMessage(message)); }); ConsoleInterceptor.originalConsole[type](...args); StandardStreamsInterceptor.stopInterceptingStrings(); } /** * Listen for process signal events. * * @protected */ addSignalEventListeners() { for (let eventName of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.on(eventName, () => { this.closeAllBrowsers(); process.exit(); }); } } /** * Close all the browser instances when the process exits. * * Calling this method before exiting Node is mandatory since Puppeteer doesn't seem to handle that properly. * * @protected */ closeAllBrowsers() { for (let browser of this.browsers.values()) { browser.close(); } } } module.exports = PuppeteerConnectionDelegate; ================================================ FILE: src/PuppeteerProcessDelegate.php ================================================ $options = null) */ class Accessibility extends BasicResource { } ================================================ FILE: src/Resources/Browser.php ================================================ $options = null) * @method \Nesk\Puphpeteer\Resources\BrowserContext[] browserContexts() * @method-extended \Nesk\Puphpeteer\Resources\BrowserContext[] browserContexts() * @method \Nesk\Puphpeteer\Resources\BrowserContext defaultBrowserContext() * @method-extended \Nesk\Puphpeteer\Resources\BrowserContext defaultBrowserContext() * @method string wsEndpoint() * @method-extended string wsEndpoint() * @method \Nesk\Puphpeteer\Resources\Page newPage() * @method-extended \Nesk\Puphpeteer\Resources\Page newPage() * @method \Nesk\Puphpeteer\Resources\Target[] targets() * @method-extended \Nesk\Puphpeteer\Resources\Target[] targets() * @method \Nesk\Puphpeteer\Resources\Target target() * @method-extended \Nesk\Puphpeteer\Resources\Target target() * @method \Nesk\Puphpeteer\Resources\Target waitForTarget(\Nesk\Rialto\Data\JsFunction $predicate, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\Target waitForTarget(callable(\Nesk\Puphpeteer\Resources\Target $x): bool|Promise|bool[]|\Nesk\Rialto\Data\JsFunction $predicate, array $options = null) * @method \Nesk\Puphpeteer\Resources\Page[] pages() * @method-extended \Nesk\Puphpeteer\Resources\Page[] pages() * @method string version() * @method-extended string version() * @method string userAgent() * @method-extended string userAgent() * @method void close() * @method-extended void close() * @method void disconnect() * @method-extended void disconnect() * @method mixed[] cookies() * @method-extended mixed[] cookies() * @method void setCookie(mixed ...$cookies) * @method-extended void setCookie(mixed ...$cookies) * @method void deleteCookie(mixed ...$cookies) * @method-extended void deleteCookie(mixed ...$cookies) * @method void deleteMatchingCookies(mixed ...$filters) * @method-extended void deleteMatchingCookies(mixed ...$filters) * @method string installExtension(string $path) * @method-extended string installExtension(string $path) * @method void uninstallExtension(string $id) * @method-extended void uninstallExtension(string $id) * @method bool isConnected() * @method-extended bool isConnected() * @method bool isNetworkEnabled() * @method-extended bool isNetworkEnabled() */ class Browser extends EventEmitter { } ================================================ FILE: src/Resources/BrowserContext.php ================================================ $options = null) * @method \Nesk\Puphpeteer\Resources\Page[] pages() * @method-extended \Nesk\Puphpeteer\Resources\Page[] pages() * @method void overridePermissions(string $origin, mixed[] $permissions) * @method-extended void overridePermissions(string $origin, mixed[] $permissions) * @method void clearPermissionOverrides() * @method-extended void clearPermissionOverrides() * @method \Nesk\Puphpeteer\Resources\Page newPage() * @method-extended \Nesk\Puphpeteer\Resources\Page newPage() * @method \Nesk\Puphpeteer\Resources\Browser browser() * @method-extended \Nesk\Puphpeteer\Resources\Browser browser() * @method void close() * @method-extended void close() * @method mixed[] cookies() * @method-extended mixed[] cookies() * @method void setCookie(mixed ...$cookies) * @method-extended void setCookie(mixed ...$cookies) * @method void deleteCookie(mixed ...$cookies) * @method-extended void deleteCookie(mixed ...$cookies) * @method void deleteMatchingCookies(mixed ...$filters) * @method-extended void deleteMatchingCookies(mixed ...$filters) */ class BrowserContext extends EventEmitter { } ================================================ FILE: src/Resources/BrowserLauncher.php ================================================ $options = null) * @method string executablePath(mixed $channel = null, bool $validatePath = null) * @method-extended string executablePath(mixed $channel = null, bool $validatePath = null) * @method string[] defaultArgs(array $object) * @method-extended string[] defaultArgs(array $object) * @method string resolveExecutablePath(bool|'shell' $headless = null, bool $validatePath = null) * @method-extended string resolveExecutablePath(bool|'shell' $headless = null, bool $validatePath = null) */ class BrowserLauncher { } ================================================ FILE: src/Resources/BrowserWebSocketTransport.php ================================================ $options = null) * @method void detach() * @method-extended void detach() * @method string id() * @method-extended string id() */ class CDPSession extends EventEmitter { } ================================================ FILE: src/Resources/ConsoleMessage.php ================================================ $options = null) * @method mixed[] stopJSCoverage() * @method-extended mixed[] stopJSCoverage() * @method void startCSSCoverage(array $options = []) * @method-extended void startCSSCoverage(array $options = null) * @method mixed[] stopCSSCoverage() * @method-extended mixed[] stopCSSCoverage() */ class Coverage extends BasicResource { } ================================================ FILE: src/Resources/DevToolsTarget.php ================================================ $options = null) * @method bool isVisible() * @method-extended bool isVisible() * @method bool isHidden() * @method-extended bool isHidden() * @method mixed toElement(mixed $tagName) * @method-extended mixed toElement(mixed $tagName) * @method \Nesk\Puphpeteer\Resources\Frame|null contentFrame() * @method-extended \Nesk\Puphpeteer\Resources\Frame|null contentFrame() * @method mixed clickablePoint(mixed $offset = null) * @method-extended mixed clickablePoint(mixed $offset = null) * @method void hover(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended void hover(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method void click(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $options = null) * @method-extended void click(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $options = null) * @method mixed|null drag(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed|\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $target) * @method-extended mixed|null drag(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed|\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $target) * @method void dragEnter(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method-extended void dragEnter(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method void dragOver(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method-extended void dragOver(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method void drop(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method-extended void drop(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $data = null) * @method void dragAndDrop(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $target, array $options = []) * @method-extended void dragAndDrop(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $target, array{ $delay: float } $options = null) * @method string[] select(string ...$values) * @method-extended string[] select(string ...$values) * @method void uploadFile(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, string ...$paths) * @method-extended void uploadFile(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, string ...$paths) * @method mixed queryAXTree(string $name = null, string $role = null) * @method-extended mixed queryAXTree(string $name = null, string $role = null) * @method void tap(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended void tap(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method mixed touchStart(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended mixed touchStart(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method void touchMove(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $touch = null) * @method-extended void touchMove(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, mixed $touch = null) * @method void touchEnd(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended void touchEnd(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method void focus() * @method-extended void focus() * @method void type(string $text, mixed $options = null) * @method-extended void type(string $text, mixed $options = null) * @method void press(mixed $key, mixed $options = null) * @method-extended void press(mixed $key, mixed $options = null) * @method mixed|null boundingBox() * @method-extended mixed|null boundingBox() * @method mixed|null boxModel() * @method-extended mixed|null boxModel() * @method \Nesk\Puphpeteer\Resources\Uint8Array screenshot(mixed $options = null) * @method-extended \Nesk\Puphpeteer\Resources\Uint8Array screenshot(mixed $options = null) * @method bool isIntersectingViewport(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, array $options = []) * @method-extended bool isIntersectingViewport(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector, array{ $threshold: float } $options = null) * @method void scrollIntoView(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended void scrollIntoView(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method mixed asLocator(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method-extended mixed asLocator(\Nesk\Puphpeteer\Resources\ElementHandle|mixed[] $selector) * @method void autofill(mixed $data) * @method-extended void autofill(mixed $data) * @method float backendNodeId() * @method-extended float backendNodeId() */ class ElementHandle extends JSHandle { use AliasesEvaluationMethods; use AliasesSelectionMethods; } ================================================ FILE: src/Resources/EventEmitter.php ================================================ $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = null) * @method \Nesk\Puphpeteer\Resources\Realm mainRealm() * @method-extended \Nesk\Puphpeteer\Resources\Realm mainRealm() * @method \Nesk\Puphpeteer\Resources\Realm isolatedRealm() * @method-extended \Nesk\Puphpeteer\Resources\Realm isolatedRealm() * @method void clearDocumentHandle() * @method-extended void clearDocumentHandle() * @method mixed|null frameElement() * @method-extended mixed|null frameElement() * @method mixed evaluateHandle(\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method-extended mixed evaluateHandle(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method mixed evaluate(\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method-extended mixed evaluate(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method mixed locator(\Nesk\Rialto\Data\JsFunction $func) * @method-extended mixed locator(callable(): mixed|\Nesk\Rialto\Data\JsFunction $func) * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[]|null waitForSelector(mixed $selector, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[]|null waitForSelector(mixed $selector, array $options = null) * @method mixed waitForFunction(\Nesk\Rialto\Data\JsFunction $pageFunction, array $options = [], mixed ...$args) * @method-extended mixed waitForFunction(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, array $options = null, mixed ...$args) * @method string content() * @method-extended string content() * @method void setContent(string $html, array $options = []) * @method-extended void setContent(string $html, array $options = null) * @method void setFrameContent(string $content) * @method-extended void setFrameContent(string $content) * @method string name() * @method-extended string name() * @method string url() * @method-extended string url() * @method \Nesk\Puphpeteer\Resources\Frame|null parentFrame() * @method-extended \Nesk\Puphpeteer\Resources\Frame|null parentFrame() * @method \Nesk\Puphpeteer\Resources\Frame[] childFrames() * @method-extended \Nesk\Puphpeteer\Resources\Frame[] childFrames() * @method bool isDetached() * @method-extended bool isDetached() * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addScriptTag(array $options) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addScriptTag(array $options) * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addStyleTag(array $options) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addStyleTag(array $options) * @method void click(string $selector, mixed $options = null) * @method-extended void click(string $selector, mixed $options = null) * @method void focus(string $selector) * @method-extended void focus(string $selector) * @method void hover(string $selector) * @method-extended void hover(string $selector) * @method string[] select(string $selector, string ...$values) * @method-extended string[] select(string $selector, string ...$values) * @method void tap(string $selector) * @method-extended void tap(string $selector) * @method void type(string $selector, string $text, mixed $options = null) * @method-extended void type(string $selector, string $text, mixed $options = null) * @method string title() * @method-extended string title() * @method mixed waitForDevicePrompt(array $options = []) * @method-extended mixed waitForDevicePrompt(array $options = null) */ class Frame extends BasicResource { use AliasesEvaluationMethods; use AliasesSelectionMethods; } ================================================ FILE: src/Resources/HTTPRequest.php ================================================ $options = null) * @method void setGeolocation(array $options) * @method-extended void setGeolocation(array $options) * @method \Nesk\Puphpeteer\Resources\Target target() * @method-extended \Nesk\Puphpeteer\Resources\Target target() * @method \Nesk\Puphpeteer\Resources\Browser browser() * @method-extended \Nesk\Puphpeteer\Resources\Browser browser() * @method \Nesk\Puphpeteer\Resources\BrowserContext browserContext() * @method-extended \Nesk\Puphpeteer\Resources\BrowserContext browserContext() * @method \Nesk\Puphpeteer\Resources\Frame mainFrame() * @method-extended \Nesk\Puphpeteer\Resources\Frame mainFrame() * @method \Nesk\Puphpeteer\Resources\CDPSession createCDPSession() * @method-extended \Nesk\Puphpeteer\Resources\CDPSession createCDPSession() * @method \Nesk\Puphpeteer\Resources\Frame[] frames() * @method-extended \Nesk\Puphpeteer\Resources\Frame[] frames() * @method \Nesk\Puphpeteer\Resources\WebWorker[] workers() * @method-extended \Nesk\Puphpeteer\Resources\WebWorker[] workers() * @method void setRequestInterception(bool $value) * @method-extended void setRequestInterception(bool $value) * @method void setBypassServiceWorker(bool $bypass) * @method-extended void setBypassServiceWorker(bool $bypass) * @method void setDragInterception(bool $enabled) * @method-extended void setDragInterception(bool $enabled) * @method void setOfflineMode(bool $enabled) * @method-extended void setOfflineMode(bool $enabled) * @method void emulateNetworkConditions(mixed|null $networkConditions) * @method-extended void emulateNetworkConditions(mixed|null $networkConditions) * @method void setDefaultNavigationTimeout(float $timeout) * @method-extended void setDefaultNavigationTimeout(float $timeout) * @method void setDefaultTimeout(float $timeout) * @method-extended void setDefaultTimeout(float $timeout) * @method float getDefaultTimeout() * @method-extended float getDefaultTimeout() * @method float getDefaultNavigationTimeout() * @method-extended float getDefaultNavigationTimeout() * @method mixed locator(\Nesk\Rialto\Data\JsFunction $func) * @method-extended mixed locator(callable(): mixed|\Nesk\Rialto\Data\JsFunction $func) * @method mixed locatorRace(mixed $locators) * @method-extended mixed locatorRace(mixed $locators) * @method mixed evaluateHandle(\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method-extended mixed evaluateHandle(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method \Nesk\Puphpeteer\Resources\JSHandle|mixed[][] queryObjects(\Nesk\Puphpeteer\Resources\JSHandle|mixed[] $prototypeHandle) * @method-extended \Nesk\Puphpeteer\Resources\JSHandle|mixed[][] queryObjects(\Nesk\Puphpeteer\Resources\JSHandle|mixed[] $prototypeHandle) * @method mixed[] cookies(string ...$urls) * @method-extended mixed[] cookies(string ...$urls) * @method void deleteCookie(mixed ...$cookies) * @method-extended void deleteCookie(mixed ...$cookies) * @method void setCookie(mixed ...$cookies) * @method-extended void setCookie(mixed ...$cookies) * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addScriptTag(array $options) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addScriptTag(array $options) * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addStyleTag(array $options) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[] addStyleTag(array $options) * @method void exposeFunction(string $name, \Nesk\Rialto\Data\JsFunction|array $pptrFunction) * @method-extended void exposeFunction(string $name, callable|\Nesk\Rialto\Data\JsFunction|array{ $default: callable|\Nesk\Rialto\Data\JsFunction } $pptrFunction) * @method void removeExposedFunction(string $name) * @method-extended void removeExposedFunction(string $name) * @method void authenticate(mixed|null $credentials) * @method-extended void authenticate(mixed|null $credentials) * @method void setExtraHTTPHeaders(array|string[]|string[] $headers) * @method-extended void setExtraHTTPHeaders(array|string[]|string[] $headers) * @method void setUserAgent(array $options) * @method-extended void setUserAgent(array{ $userAgent: string, $userAgentMetadata: mixed, $platform: string } $options) * @method mixed metrics() * @method-extended mixed metrics() * @method string url() * @method-extended string url() * @method string content() * @method-extended string content() * @method void setContent(string $html, array $options = []) * @method-extended void setContent(string $html, array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goto(string $url, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null goto(string $url, array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null reload(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null reload(array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPRequest waitForRequest(string|mixed $urlOrPredicate, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPRequest waitForRequest(string|mixed $urlOrPredicate, array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse waitForResponse(string|mixed $urlOrPredicate, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse waitForResponse(string|mixed $urlOrPredicate, array $options = null) * @method void waitForNetworkIdle(array $options = []) * @method-extended void waitForNetworkIdle(array $options = null) * @method \Nesk\Puphpeteer\Resources\Frame waitForFrame(string|\Nesk\Rialto\Data\JsFunction $urlOrPredicate, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\Frame waitForFrame(string|callable(callable(\Nesk\Puphpeteer\Resources\Frame $frame): mixed|\Nesk\Rialto\Data\JsFunction): |\Nesk\Rialto\Data\JsFunction $urlOrPredicate, array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goBack(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null goBack(array $options = null) * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goForward(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\HTTPResponse|null goForward(array $options = null) * @method void bringToFront() * @method-extended void bringToFront() * @method void emulate(mixed $device) * @method-extended void emulate(mixed $device) * @method void setJavaScriptEnabled(bool $enabled) * @method-extended void setJavaScriptEnabled(bool $enabled) * @method void setBypassCSP(bool $enabled) * @method-extended void setBypassCSP(bool $enabled) * @method void emulateMediaType(string $type = null) * @method-extended void emulateMediaType(string $type = null) * @method void emulateCPUThrottling(float|null $factor) * @method-extended void emulateCPUThrottling(float|null $factor) * @method void emulateMediaFeatures(mixed[] $features = null) * @method-extended void emulateMediaFeatures(mixed[] $features = null) * @method void emulateTimezone(string $timezoneId = null) * @method-extended void emulateTimezone(string $timezoneId = null) * @method void emulateIdleState(array $overrides = []) * @method-extended void emulateIdleState(array{ $isUserActive: bool, $isScreenUnlocked: bool } $overrides = null) * @method void emulateVisionDeficiency(mixed $type = null) * @method-extended void emulateVisionDeficiency(mixed $type = null) * @method void setViewport(mixed|null $viewport) * @method-extended void setViewport(mixed|null $viewport) * @method mixed|null viewport() * @method-extended mixed|null viewport() * @method mixed evaluate(\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method-extended mixed evaluate(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method mixed evaluateOnNewDocument(\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method-extended mixed evaluateOnNewDocument(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, mixed ...$args) * @method void removeScriptToEvaluateOnNewDocument(string $identifier) * @method-extended void removeScriptToEvaluateOnNewDocument(string $identifier) * @method void setCacheEnabled(bool $enabled = null) * @method-extended void setCacheEnabled(bool $enabled = null) * @method \Nesk\Puphpeteer\Resources\ScreenRecorder screencast(mixed $options = null) * @method-extended \Nesk\Puphpeteer\Resources\ScreenRecorder screencast(mixed $options = null) * @method \Nesk\Puphpeteer\Resources\Uint8Array screenshot(mixed $options = null) * @method-extended \Nesk\Puphpeteer\Resources\Uint8Array screenshot(mixed $options = null) * @method mixed createPDFStream(array $options = []) * @method-extended mixed createPDFStream(array $options = null) * @method \Nesk\Puphpeteer\Resources\Uint8Array pdf(array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\Uint8Array pdf(array $options = null) * @method string title() * @method-extended string title() * @method void close(array $options = []) * @method-extended void close(array{ $runBeforeUnload: bool } $options = null) * @method bool isClosed() * @method-extended bool isClosed() * @method void click(string $selector, mixed $options = null) * @method-extended void click(string $selector, mixed $options = null) * @method void focus(string $selector) * @method-extended void focus(string $selector) * @method void hover(string $selector) * @method-extended void hover(string $selector) * @method string[] select(string $selector, string ...$values) * @method-extended string[] select(string $selector, string ...$values) * @method void tap(string $selector) * @method-extended void tap(string $selector) * @method void type(string $selector, string $text, mixed $options = null) * @method-extended void type(string $selector, string $text, mixed $options = null) * @method \Nesk\Puphpeteer\Resources\ElementHandle|mixed[]|null waitForSelector(mixed $selector, array $options = []) * @method-extended \Nesk\Puphpeteer\Resources\ElementHandle|mixed[]|null waitForSelector(mixed $selector, array $options = null) * @method mixed waitForFunction(\Nesk\Rialto\Data\JsFunction $pageFunction, array $options = [], mixed ...$args) * @method-extended mixed waitForFunction(callable|\Nesk\Rialto\Data\JsFunction $pageFunction, array $options = null, mixed ...$args) * @method mixed waitForDevicePrompt(array $options = []) * @method-extended mixed waitForDevicePrompt(array $options = null) * @method void resize(array $params) * @method-extended void resize(array{ $contentWidth: float, $contentHeight: float } $params) */ class Page extends EventEmitter { use AliasesEvaluationMethods; use AliasesSelectionMethods; } ================================================ FILE: src/Resources/PageTarget.php ================================================ $options = null) * @method \Nesk\Puphpeteer\Resources\Uint8Array|null stop() * @method-extended \Nesk\Puphpeteer\Resources\Uint8Array|null stop() */ class Tracing extends BasicResource { } ================================================ FILE: src/Resources/Uint8Array.php ================================================ __call('$eval', $arguments); } public function querySelectorAllEval(...$arguments) { return $this->__call('$$eval', $arguments); } } ================================================ FILE: src/Traits/AliasesSelectionMethods.php ================================================ __call('$', $arguments); } public function querySelectorAll(...$arguments) { return $this->__call('$$', $arguments); } } ================================================ FILE: src/doc-generator.ts ================================================ import * as ts from 'typescript'; const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const callbackClass = '\\Nesk\\Rialto\\Data\\JsFunction'; type ObjectMemberAsJson = { [key: string]: string; } type ObjectMembersAsJson = { properties: ObjectMemberAsJson, getters: ObjectMemberAsJson, methods: ObjectMemberAsJson, } type ClassAsJson = { name: string } & ObjectMembersAsJson type MemberContext = 'class'|'literal' type TypeContext = 'methodReturn' class TypeNotSupportedError extends Error { constructor(message?: string) { super('This type is currently not supported: ' + message); } } interface SupportChecker { supportsMethodName(methodName: string): boolean; } class JsSupportChecker { supportsMethodName(methodName: string): boolean { return true; } } class PhpSupportChecker { supportsMethodName(methodName: string): boolean { return !methodName.includes('$'); } } interface DocumentationFormatter { formatProperty(name: string, type: string, context: MemberContext): string formatGetter(name: string, type: string): string formatAnonymousFunction(parameters: string, returnType: string): string formatFunction(name: string, parameters: string, returnType: string): string formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string formatTypeAny(): string formatTypeUnknown(): string formatTypeVoid(): string formatTypeUndefined(): string formatTypeNull(): string formatTypeBoolean(): string formatTypeNumber(): string formatTypeString(): string formatTypeReference(type: string): string formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string formatQualifiedName(left: string, right: string): string formatIndexedAccessType(object: string, index: string): string formatLiteralType(value: string): string formatUnion(types: string[]): string formatIntersection(types: string[]): string formatObject(members: string[]): string formatArray(type: string): string } class JsDocumentationFormatter implements DocumentationFormatter { formatProperty(name: string, type: string, context: MemberContext): string { return `${name}: ${type}`; } formatGetter(name: string, type: string): string { return `${name}: ${type}`; } formatAnonymousFunction(parameters: string, returnType: string): string { return `(${parameters}) => ${returnType}`; } formatFunction(name: string, parameters: string, returnType: string): string { return `${name}(${parameters}): ${returnType}`; } formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string { return `${isVariadic ? '...' : ''}${name}${isOptional ? '?' : ''}: ${type}`; } formatTypeAny(): string { return 'any'; } formatTypeUnknown(): string { return 'unknown'; } formatTypeVoid(): string { return 'void'; } formatTypeUndefined(): string { return 'undefined'; } formatTypeNull(): string { return 'null'; } formatTypeBoolean(): string { return 'boolean'; } formatTypeNumber(): string { return 'number'; } formatTypeString(): string { return 'string'; } formatTypeReference(type: string): string { return type; } formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string { return `${parentType}<${argumentTypes.join(', ')}>`; } formatQualifiedName(left: string, right: string): string { return `${left}.${right}`; } formatIndexedAccessType(object: string, index: string): string { return `${object}[${index}]`; } formatLiteralType(value: string): string { return `'${value}'`; } formatUnion(types: string[]): string { return types.join(' | '); } formatIntersection(types: string[]): string { return types.join(' & '); } formatObject(members: string[]): string { return `{ ${members.join(', ')} }`; } formatArray(type: string): string { return `${type}[]`; } } class PhpDocumentationFormatter implements DocumentationFormatter { static readonly allowedJsClasses = ['Promise', 'Record', 'Map']; constructor( protected readonly resourcesNamespace: string, protected readonly resources: string[], ) {} formatProperty(name: string, type: string, context: MemberContext): string { return context === 'class' ? `${type} \$${name}` : `\$${name}: ${type}`; } formatGetter(name: string, type: string): string { return `${type} \$${name}`; } formatAnonymousFunction(parameters: string, returnType: string): string { return callbackClass; } formatFunction(name: string, parameters: string, returnType: string): string { return `${returnType} ${name}(${parameters})`; } formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string { if (name === 'this') { name = 'selector' } if (isVariadic && type.endsWith('[]')) { type = type.slice(0, -2); } let defaultValue; switch (type) { case 'array' : defaultValue = isOptional ? ' = []' : ''; break; default: defaultValue = isOptional ? ' = null' : ''; break; } return `${type} ${isVariadic ? '...' : ''}\$${name}${defaultValue}`; } formatTypeAny(): string { return 'mixed'; } formatTypeUnknown(): string { return 'mixed'; } formatTypeVoid(): string { return 'void'; } formatTypeUndefined(): string { return 'null'; } formatTypeNull(): string { return 'null'; } formatTypeBoolean(): string { return 'bool'; } formatTypeNumber(): string { return 'float'; } formatTypeString(): string { return 'string'; } formatTypeReference(type: string): string { // Allow some specific JS classes to be used in phpDoc if (PhpDocumentationFormatter.allowedJsClasses.includes(type)) { return type; } // Prefix PHP resources with their namespace if (this.resources.includes(type)) { return this.resourcesNamespace ? `\\${this.resourcesNamespace}\\${type}` : type; } // If the type ends with "options" then convert it to an associative array if (/options$/i.test(type)) { return 'array'; } // Types ending with "Fn" are always callables or strings if (type.endsWith('Fn')) { return this.formatUnion([callbackClass, 'string']); } if (type === 'Function') { return callbackClass; } if (type === 'PuppeteerLifeCycleEvent') { return 'string'; } if (type === 'Serializable') { return this.formatUnion(['int', 'float', 'string', 'bool', 'null', 'array']); } if (type === 'SerializableOrJSHandle') { return this.formatUnion([this.formatTypeReference('Serializable'), this.formatTypeReference('JSHandle')]); } if (type === 'HandleType') { return this.formatUnion([this.formatTypeReference('JSHandle'), this.formatTypeReference('ElementHandle')]); } return 'mixed'; } formatGeneric(parentType: string, argumentTypes: string[], context?: TypeContext): string { // Avoid generics with "mixed" as parent type if (parentType === 'mixed') { return 'mixed'; } // Unwrap promises for method return types if (context === 'methodReturn' && parentType === 'Promise' && argumentTypes.length === 1) { return argumentTypes[0]; } // Transform Record and Map types to associative arrays if (['Record', 'Map'].includes(parentType) && argumentTypes.length === 2) { parentType = 'array'; } return `${parentType}|${argumentTypes.join('[]|')}[]`; } formatQualifiedName(left: string, right: string): string { return `mixed`; } formatIndexedAccessType(object: string, index: string): string { return `mixed`; } formatLiteralType(value: string): string { return `'${value}'`; } private prepareUnionOrIntersectionTypes(types: string[]): string[] { // Replace "void" type by "null" types = types.map(type => type === 'void' ? 'null' : type) // Remove duplicates const uniqueTypes = new Set(types); return Array.from(uniqueTypes.values()); } formatUnion(types: string[]): string { const result = this.prepareUnionOrIntersectionTypes(types).join('|'); // Convert enums to string type if (/^('\w+'\|)*'\w+'$/.test(result)) { return 'string'; } return result; } formatIntersection(types: string[]): string { return this.prepareUnionOrIntersectionTypes(types).join('&'); } formatObject(members: string[]): string { return `array`; } formatArray(type: string): string { return `${type}[]`; } } class PhpStanDocumentationFormatter extends PhpDocumentationFormatter { formatAnonymousFunction(parameters: string, returnType: string): string { return `callable(${parameters}): ${returnType}|` + callbackClass; } formatTypeReference(type: string): string { // Allow some specific JS classes to be used in phpDoc if (PhpDocumentationFormatter.allowedJsClasses.includes(type)) { return type; } // Prefix PHP resources with their namespace if (this.resources.includes(type)) { return this.resourcesNamespace ? `\\${this.resourcesNamespace}\\${type}` : type; } // If the type ends with "options" then convert it to an associative array if (/options$/i.test(type)) { return 'array'; } // Types ending with "Fn" are always callables or strings if (type.endsWith('Fn')) { return this.formatUnion([callbackClass, 'callable', 'string']); } if (type === 'Function') { return this.formatUnion(['callable', callbackClass]); } if (type === 'PuppeteerLifeCycleEvent') { return 'string'; } if (type === 'Serializable') { return this.formatUnion(['int', 'float', 'string', 'bool', 'null', 'array']); } if (type === 'SerializableOrJSHandle') { return this.formatUnion([this.formatTypeReference('Serializable'), this.formatTypeReference('JSHandle')]); } if (type === 'HandleType') { return this.formatUnion([this.formatTypeReference('JSHandle'), this.formatTypeReference('ElementHandle')]); } return 'mixed'; } formatObject(members: string[]): string { return `array{ ${members.join(', ')} }`; } } class DocumentationGenerator { constructor( private readonly supportChecker: SupportChecker, private readonly formatter: DocumentationFormatter, ) {} private hasModifierForNode( node: ts.Node & { modifiers?: ts.Modifier[] }, modifier: ts.KeywordSyntaxKind, ): boolean { if (!node.modifiers) { return false; } return node.modifiers.some((node) => node.kind === modifier); } private isNodeAccessible(node: ts.Node): boolean { // @ts-ignore if (node.name && (this.getNamedDeclarationAsString(node).startsWith('_') || this.getNamedDeclarationAsString(node).startsWith('#'))) { return false; } return ( this.hasModifierForNode(node, ts.SyntaxKind.PublicKeyword) || (!this.hasModifierForNode(node, ts.SyntaxKind.ProtectedKeyword) && !this.hasModifierForNode(node, ts.SyntaxKind.PrivateKeyword)) ); } private isNodeStatic(node: ts.Node): boolean { return this.hasModifierForNode(node, ts.SyntaxKind.StaticKeyword); } public getClassDeclarationAsJson(node: ts.ClassDeclaration): ClassAsJson { return Object.assign( { name: this.getNamedDeclarationAsString(node) }, this.getMembersAsJson(node.members, 'class'), ); } private getMembersAsJson(members: ts.NodeArray, context: MemberContext): ObjectMembersAsJson { const json: ObjectMembersAsJson = { properties: {}, getters: {}, methods: {}, }; for (const member of members) { if (!this.isNodeAccessible(member) || this.isNodeStatic(member)) { continue; } const name = member.name ? this.getNamedDeclarationAsString(member) : null; if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) { json.properties[name] = this.getPropertySignatureOrDeclarationAsString(member, context); } else if (ts.isGetAccessorDeclaration(member)) { json.getters[name] = this.getGetAccessorDeclarationAsString(member); } else if (ts.isMethodDeclaration(member)) { if (!this.supportChecker.supportsMethodName(name)) { continue; } json.methods[name] = this.getSignatureDeclarationBaseAsString(member); } } return json; } private getPropertySignatureOrDeclarationAsString( node: ts.PropertySignature | ts.PropertyDeclaration, context: MemberContext ): string { const type = this.getTypeNodeAsString(node.type); const name = this.getNamedDeclarationAsString(node); return this.formatter.formatProperty(name, type, context); } private getGetAccessorDeclarationAsString( node: ts.GetAccessorDeclaration ): string { const type = this.getTypeNodeAsString(node.type); const name = this.getNamedDeclarationAsString(node); return this.formatter.formatGetter(name, type); } private getSignatureDeclarationBaseAsString( node: ts.SignatureDeclarationBase ): string { const name = node.name && this.getNamedDeclarationAsString(node); const parameters = node.parameters .map(parameter => this.getParameterDeclarationAsString(parameter)) .join(', '); const returnType = this.getTypeNodeAsString(node.type, name ? 'methodReturn' : undefined); return name ? this.formatter.formatFunction(name, parameters, returnType) : this.formatter.formatAnonymousFunction(parameters, returnType); } private getEmptyFunctionSignatureAsString( node: ts.ParenthesizedTypeNode ): string { return this.formatter.formatAnonymousFunction(this.getTypeNodeAsString(node.type), ''); } private getParameterDeclarationAsString(node: ts.ParameterDeclaration): string { const name = this.getNamedDeclarationAsString(node); let type = this.getTypeNodeAsString(node.type); const isVariadic = node.dotDotDotToken !== undefined; const isOptional = node.questionToken !== undefined; //fix missing argument type in evaluate* methods. if (name.includes('Function') && type.includes('mixed')) { type = this.formatter.formatTypeReference('Function'); } return this.formatter.formatParameter(name, type, isVariadic, isOptional); } private getTypeNodeAsString(node: ts.TypeNode, context?: TypeContext): string { if (!node) { return ''; } if (node.kind === ts.SyntaxKind.AnyKeyword) { return this.formatter.formatTypeAny(); } else if (node.kind === ts.SyntaxKind.UnknownKeyword) { return this.formatter.formatTypeUnknown(); } else if (node.kind === ts.SyntaxKind.VoidKeyword) { return this.formatter.formatTypeVoid(); } else if (node.kind === ts.SyntaxKind.UndefinedKeyword) { return this.formatter.formatTypeUndefined(); } else if (node.kind === ts.SyntaxKind.NullKeyword) { return this.formatter.formatTypeNull(); } else if (node.kind === ts.SyntaxKind.BooleanKeyword) { return this.formatter.formatTypeBoolean(); } else if (node.kind === ts.SyntaxKind.NumberKeyword) { return this.formatter.formatTypeNumber(); } else if (node.kind === ts.SyntaxKind.StringKeyword) { return this.formatter.formatTypeString(); } else if (ts.isTypeReferenceNode(node)) { return this.getTypeReferenceNodeAsString(node, context); } else if (ts.isIndexedAccessTypeNode(node)) { return this.getIndexedAccessTypeNodeAsString(node); } else if (ts.isLiteralTypeNode(node)) { return this.getLiteralTypeNodeAsString(node); } else if (ts.isUnionTypeNode(node)) { return this.getUnionTypeNodeAsString(node, context); } else if (ts.isIntersectionTypeNode(node)) { return this.getIntersectionTypeNodeAsString(node, context); } else if (ts.isTypeLiteralNode(node)) { return this.getTypeLiteralNodeAsString(node); } else if (ts.isArrayTypeNode(node)) { return this.getArrayTypeNodeAsString(node, context); } else if (ts.isFunctionTypeNode(node)) { return this.getSignatureDeclarationBaseAsString(node); } else if (ts.isParenthesizedTypeNode(node)) { return this.getEmptyFunctionSignatureAsString(node); } else { console.error('Unknown type: ' + ts.SyntaxKind[node.kind]) return this.formatter.formatTypeAny(); } } private getTypeReferenceNodeAsString(node: ts.TypeReferenceNode, context?: TypeContext): string { return this.getGenericTypeReferenceNodeAsString(node, context) || this.getSimpleTypeReferenceNodeAsString(node); } private getGenericTypeReferenceNodeAsString(node: ts.TypeReferenceNode, context?: TypeContext): string | null { if (!node.typeArguments || node.typeArguments.length === 0) { return null; } const parentType = this.getSimpleTypeReferenceNodeAsString(node); const argumentTypes = node.typeArguments.map((node) => this.getTypeNodeAsString(node)); return this.formatter.formatGeneric(parentType, argumentTypes, context); } private getSimpleTypeReferenceNodeAsString(node: ts.TypeReferenceNode): string { return ts.isIdentifier(node.typeName) ? this.formatter.formatTypeReference(this.getIdentifierAsString(node.typeName)) : this.getQualifiedNameAsString(node.typeName); } private getQualifiedNameAsString(node: ts.QualifiedName): string { const right = this.getIdentifierAsString(node.right); const left = ts.isIdentifier(node.left) ? this.getIdentifierAsString(node.left) : this.getQualifiedNameAsString(node.left); return this.formatter.formatQualifiedName(left, right); } private getIndexedAccessTypeNodeAsString( node: ts.IndexedAccessTypeNode ): string { const object = this.getTypeNodeAsString(node.objectType); const index = this.getTypeNodeAsString(node.indexType); return this.formatter.formatIndexedAccessType(object, index); } private getLiteralTypeNodeAsString(node: ts.LiteralTypeNode): string { if (node.literal.kind === ts.SyntaxKind.NullKeyword) { return this.formatter.formatTypeNull(); } else if (node.literal.kind === ts.SyntaxKind.BooleanKeyword) { return this.formatter.formatTypeBoolean(); } else if (ts.isLiteralExpression(node.literal)) { return this.formatter.formatLiteralType(node.literal.text); } throw new TypeNotSupportedError(); } private getUnionTypeNodeAsString(node: ts.UnionTypeNode, context?: TypeContext): string { const types = node.types.map(typeNode => this.getTypeNodeAsString(typeNode, context)); return this.formatter.formatUnion(types); } private getIntersectionTypeNodeAsString(node: ts.IntersectionTypeNode, context?: TypeContext): string { const types = node.types.map(typeNode => this.getTypeNodeAsString(typeNode, context)); return this.formatter.formatIntersection(types); } private getTypeLiteralNodeAsString(node: ts.TypeLiteralNode): string { const members = this.getMembersAsJson(node.members, 'literal'); const stringMembers = Object.values(members).map(Object.values); const flattenMembers = stringMembers.reduce((acc, val) => acc.concat(val), []); return this.formatter.formatObject(flattenMembers); } private getArrayTypeNodeAsString(node: ts.ArrayTypeNode, context?: TypeContext): string { const type = this.getTypeNodeAsString(node.elementType, context); return this.formatter.formatArray(type); } private getNamedDeclarationAsString(node: ts.NamedDeclaration): string { if (!ts.isIdentifier(node.name) && !ts.isPrivateIdentifier(node.name)) { console.warn('Unknown type: ' + ts.SyntaxKind[node.kind]); return '#'; } return this.getIdentifierAsString(node.name); } private getIdentifierAsString(node: ts.Identifier|ts.PrivateIdentifier): string { return String(node.escapedText); } } const { argv } = yargs(hideBin(process.argv)) .command('$0 ') .option('resources-namespace', { type: 'string', default: '' }) .option('resources', { type: 'array', default: [] }) .option('pretty', { type: 'boolean', default: false }) let supportChecker, formatter; switch (argv.language.toUpperCase()) { case 'JS': supportChecker = new JsSupportChecker(); formatter = new JsDocumentationFormatter(); break; case 'PHP': supportChecker = new PhpSupportChecker(); formatter = new PhpDocumentationFormatter(argv.resourcesNamespace, argv.resources); break; case 'PHPSTAN': supportChecker = new PhpSupportChecker(); formatter = new PhpStanDocumentationFormatter(argv.resourcesNamespace, argv.resources); break; default: console.error(`Unsupported "${argv.language}" language.`); process.exit(1); } const docGenerator = new DocumentationGenerator(supportChecker, formatter); const program = ts.createProgram(argv.definitionFiles, {}); const classes = {}; for (const fileName of argv.definitionFiles) { const sourceFile = program.getSourceFile(fileName); ts.forEachChild(sourceFile, node => { if (ts.isClassDeclaration(node)) { const classAsJson = docGenerator.getClassDeclarationAsJson(node); classes[classAsJson.name] = classAsJson; } }); } process.stdout.write(JSON.stringify(classes, null, argv.pretty ? 2 : null)); ================================================ FILE: src/get-puppeteer-version.js ================================================ 'use strict'; function output(value) { process.stdout.write(JSON.stringify(value)); } try { const manifest = require('puppeteer/package.json'); output(manifest.version); } catch (exception) { output(null); } ================================================ FILE: tests/DownloadTest.php ================================================ serveResources(); // Launch the browser to run tests on. $this->launchBrowser(); } /** * Downloads an image and checks string length. * */ public function testDownloadImage() { // Download the image $page = $this->browser ->newPage() ->goto($this->url . '/puphpeteer-logo.png'); $base64 = $page->buffer()->toString('base64'); $imageString = base64_decode($base64); // Get the reference image from resources $reference = file_get_contents('tests/resources/puphpeteer-logo.png'); $this->assertTrue( mb_strlen($reference) === mb_strlen($imageString), 'Image is not the same length after download.' ); } public function testDownloadPdf() { $page = $this->browser->newPage(); $page->goto($this->url); $base64 = $page->pdf()->toString('base64'); $pdfBytes = base64_decode($base64); $reference = file_get_contents('tests/resources/example.pdf'); $this->assertTrue( mb_strlen($reference) === mb_strlen($pdfBytes), 'Pdf of the main page is not the same length after generation.' ); } /** * Downloads an image and checks string length. * * @test */ // public function download_large_image() // { // // Download the image // $page = $this->browser // ->newPage() // ->goto($this->url . '/denys-barabanov-jKcFmXCfaQ8-unsplash.jpg'); // $base64 = $page->buffer()->toString('base64'); // $imageString = base64_decode($base64); // // Get the reference image from resources // $reference = file_get_contents('tests/resources/denys-barabanov-jKcFmXCfaQ8-unsplash.jpg'); // $this->assertTrue( // mb_strlen($reference) === mb_strlen($imageString), // 'Large image is not the same length after download.' // ); // } } ================================================ FILE: tests/PuphpeteerTest.php ================================================ serveResources(); // Launch the browser to run tests on. $this->launchBrowser(); } /** @test */ public function can_browse_website() { $response = $this->browser->newPage()->goto($this->url); $this->assertTrue($response->ok(), 'Failed asserting that the response is successful.'); } /** * @test */ public function can_use_method_aliases() { $page = $this->browser->newPage(); $page->goto($this->url); $select = function(Page | Frame | ElementHandle $resource) { $elements = [ $resource->querySelector('h1'), $resource->querySelectorAll('h1')[0], $resource->querySelectorAll('::-p-xpath(/html/body/h1)')[0], ]; $this->assertContainsOnlyInstancesOf(ElementHandle::class, $elements); }; $evaluate = function(Page | Frame | ElementHandle $resource) { $strings = [ $resource->querySelectorEval('h1', JsFunction::createWithBody('return "Hello World!";')), $resource->querySelectorAllEval('h1', JsFunction::createWithBody('return "Hello World!";')), ]; foreach ($strings as $string) { $this->assertEquals('Hello World!', $string); } }; // Test method aliases for Page, Frame and ElementHandle classes $resources = [$page, $page->mainFrame(), $page->querySelector('body')]; foreach ($resources as $resource) { $select($resource); $evaluate($resource); } } /** @test */ public function can_evaluate_a_selection() { $page = $this->browser->newPage(); $page->goto($this->url); $title = $page->querySelectorEval('h1', JsFunction::createWithParameters(['node']) ->body('return node.textContent;')); $titleCount = $page->querySelectorAllEval('h1', JsFunction::createWithParameters(['nodes']) ->body('return nodes.length;')); $this->assertEquals('Example Page', $title); $this->assertEquals(1, $titleCount); } /** @test */ public function can_intercept_requests() { $page = $this->browser->newPage(); $page->setRequestInterception(true); $page->on('request', JsFunction::createWithParameters(['request']) ->body('request.resourceType() === "stylesheet" ? request.abort() : request.continue()')); $page->goto($this->url); $backgroundColor = $page->querySelectorEval('h1', JsFunction::createWithParameters(['node']) ->body('return getComputedStyle(node).textTransform')); $this->assertNotEquals('lowercase', $backgroundColor); } /** * @test * @dataProvider resourceProvider * @dontPopulateProperties browser */ public function check_all_resources_are_supported(string $name) { $incompleteTest = false; $resourceInstantiator = new ResourceInstantiator($this->browserOptions, $this->url); $resource = $resourceInstantiator->{$name}(new Puppeteer, $this->browserOptions); if ($resource instanceof UntestableResource) { $incompleteTest = true; } else if ($resource instanceof RiskyResource) { if (!empty($resource->exception())) { $incompleteTest = true; } else { try { $this->assertInstanceOf("Nesk\\Puphpeteer\\Resources\\$name", $resource->value()); } catch (ExpectationFailedException $exception) { $incompleteTest = true; } } } else { $this->assertInstanceOf("\\Nesk\\Puphpeteer\\Resources\\$name", $resource, json_encode($resource)); } if (!$incompleteTest) return; $reason = "The \"$name\" resource has not been tested properly, probably" ." for a good reason but you might want to have a look: \n\n "; if ($resource instanceof UntestableResource) { $reason .= "\e[33mMarked as untestable.\e[0m"; } else { if (!empty($exception = $resource->exception())) { $reason .= "\e[31mMarked as risky because of a Node error: {$exception->getMessage()}\e[0m"; } else { $value = print_r($resource->value(), true); $reason .= "\e[31mMarked as risky because of an unexpected value: $value\e[0m"; } } $this->markTestIncomplete($reason); } public static function resourceProvider(): \Generator { $resourceNames = (new ResourceInstantiator([], ''))->getResourceNames(); foreach ($resourceNames as $name) { yield [$name]; } } private function createBrowserLogger(callable $onBrowserLog): LoggerInterface { $logger = $this->createMock(LoggerInterface::class); $logger->expects(self::atLeastOnce()) ->method('log') ->willReturnCallback(function (string $level, string $message) use ($onBrowserLog) { if (\strpos($message, "Received a Browser log:") === 0) { $onBrowserLog(); } return null; }); return $logger; } /** * @test * @dontPopulateProperties browser */ public function browser_console_calls_are_logged_if_enabled() { $browserLogOccured = false; $logger = $this->createBrowserLogger(function () use (&$browserLogOccured) { $browserLogOccured = true; }); $puppeteer = new Puppeteer([ 'log_browser_console' => true, 'logger' => $logger, ]); $this->browser = $puppeteer->launch($this->browserOptions); $this->browser->pages()[0]->goto($this->url); static::assertTrue($browserLogOccured); } /** * @test * @dontPopulateProperties browser */ public function browser_console_calls_are_not_logged_if_disabled() { $browserLogOccured = false; $logger = $this->createBrowserLogger(function () use (&$browserLogOccured) { $browserLogOccured = true; }); $puppeteer = new Puppeteer([ 'log_browser_console' => false, 'logger' => $logger, ]); $this->browser = $puppeteer->launch($this->browserOptions); $this->browser->pages()[0]->goto($this->url); static::assertFalse($browserLogOccured); } } ================================================ FILE: tests/ResourceInstantiator.php ================================================ resources = [ 'Accessibility' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->accessibility; }, 'Browser' => function (Puppeteer $puppeteer): Browser { return $puppeteer->launch($this->browserOptions); }, 'BrowserContext' => function (Puppeteer $puppeteer) { return $this->Browser($puppeteer)->createBrowserContext(); }, 'CDPSession' => function (Puppeteer $puppeteer) { return $this->Target($puppeteer)->createCDPSession(); }, 'ConsoleMessage' => function () { return new UntestableResource; }, 'Coverage' => function ($puppeteer) { return $this->Page($puppeteer)->coverage; }, 'Dialog' => function () { return new UntestableResource; }, 'ElementHandle' => function ($puppeteer) { return $this->Page($puppeteer)->querySelector('body'); }, 'EventEmitter' => function (Puppeteer $puppeteer) { return $puppeteer->launch($this->browserOptions); }, 'FileChooser' => function () { return new UntestableResource; }, 'Frame' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->mainFrame(); }, 'HTTPRequest' => function (Puppeteer $puppeteer) { return $this->HTTPResponse($puppeteer)->request(); }, 'HTTPResponse' => function (Puppeteer $puppeteer): HTTPResponse { return $this->Page($puppeteer)->goto($this->url); }, 'JSHandle' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->evaluateHandle(JsFunction::createWithBody('window')); }, 'Keyboard' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->keyboard; }, 'Mouse' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->mouse; }, 'Page' => function (Puppeteer $puppeteer): Page { return $this->Browser($puppeteer)->newPage(); }, 'SecurityDetails' => function (Puppeteer $puppeteer) { return new RiskyResource(function () use ($puppeteer) { return $this->Page($puppeteer)->goto('https://example.com')->securityDetails(); }); }, 'Target' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->target(); }, 'TimeoutError' => function () { return new UntestableResource; }, 'Touchscreen' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->touchscreen; }, 'Tracing' => function (Puppeteer $puppeteer) { return $this->Page($puppeteer)->tracing; }, 'WebWorker' => function (Puppeteer $puppeteer) { $page = $this->Page($puppeteer); $page->goto($this->url, ['waitUntil' => 'networkidle0']); return $page->workers()[0]; }, ]; } public function getResourceNames(): array { return array_keys($this->resources); } public function __call(string $name, array $arguments) { if (!isset($this->resources[$name])) { throw new \InvalidArgumentException("The $name resource is not supported."); } return $this->resources[$name](...$arguments); } } ================================================ FILE: tests/RiskyResource.php ================================================ value = $resourceRetriever(); } catch (NodeFatalException $exception) { $this->exception = $exception; } } public function value() { return $this->value; } public function exception(): ?NodeFatalException { return $this->exception; } } ================================================ FILE: tests/TestCase.php ================================================ name())[0] ?? ''; $testMethod = new \ReflectionMethod($this, $methodName); $docComment = $testMethod->getDocComment(); if (preg_match('/@dontPopulateProperties (.*)/', $docComment, $matches)) { $this->dontPopulateProperties = array_values(array_filter(explode(' ', $matches[1]))); } } /** * Stops the browser and local server */ public function tearDown(): void { // Close the browser. if (isset($this->browser)) { $this->browser->close(); } // Shutdown the local server if (isset($this->servingProcess)) { $this->servingProcess->stop(0); } } /** * Serves the resources folder locally on port 8089 */ protected function serveResources(): void { // Spin up a local server to deliver the resources. $this->host = '127.0.0.1:8089'; $this->url = "http://{$this->host}"; $this->serverDir = __DIR__.'/resources'; $this->servingProcess = new Process(['php', '-S', $this->host, '-t', $this->serverDir]); $this->servingProcess->start(); } /** * Launches the PuPHPeteer-controlled browser */ protected function launchBrowser(): void { /** * Chrome doesn't support Linux sandbox on many CI environments * * @see: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-fails-due-to-sandbox-issues */ $this->browserOptions = [ 'args' => ['--no-sandbox', '--disable-setuid-sandbox'], 'headless' => true, ]; if ($this->canPopulateProperty('browser')) { $this->browser = (new Puppeteer)->launch($this->browserOptions); } } public function canPopulateProperty(string $propertyName): bool { return !in_array($propertyName, $this->dontPopulateProperties); } public function isLogLevel(): Callback { $psrLogLevels = (new ReflectionClass(LogLevel::class))->getConstants(); $monologLevels = (new ReflectionClass(Logger::class))->getConstants(); $monologLevels = array_intersect_key($monologLevels, $psrLogLevels); return $this->callback(function ($level) use ($psrLogLevels, $monologLevels) { if (is_string($level)) { return in_array($level, $psrLogLevels, true); } else if (is_int($level)) { return in_array($level, $monologLevels, true); } return false; }); } } ================================================ FILE: tests/UntestableResource.php ================================================ Document

Example Page

================================================ FILE: tests/resources/stylesheet.css ================================================ h1 { text-transform: lowercase; } ================================================ FILE: tests/resources/worker.js ================================================ // There's nothing to do, just wait.