diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 33b6d75..e990317 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- php-version: [7.1, 7.2, 7.3]
+ php-version: [7.3, 7.4, 8.0]
composer-flags: [null, --prefer-lowest]
steps:
- uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index 7dc1fb2..ca31087 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+/.build/
/node_modules/
/vendor/
+.phpunit.result.cache
composer.lock
package-lock.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14ae6b3..bb7b2da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
**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…_
+### 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
diff --git a/README.md b/README.md
index 319dce6..a3d763c 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ $browser->close();
## Requirements and installation
-This package requires PHP >= 7.1 and Node >= 8.
+This package requires PHP >= 7.3 and Node >= 8.
Install it with these two command lines:
diff --git a/bin/console b/bin/console
new file mode 100755
index 0000000..eb3a20e
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,12 @@
+#!/usr/bin/env php
+add(new GenerateDocumentationCommand)
+ ->getApplication()
+ ->run();
diff --git a/composer.json b/composer.json
index f9e5ea6..440800d 100644
--- a/composer.json
+++ b/composer.json
@@ -19,14 +19,14 @@
}
],
"require": {
- "php": ">=7.1",
+ "php": ">=7.3",
"nesk/rialto": "^1.2.0",
"psr/log": "^1.0",
"vierbergenlars/php-semver": "^3.0.2"
},
"require-dev": {
- "monolog/monolog": "^1.23",
- "phpunit/phpunit": "^6.5|^7.0",
+ "monolog/monolog": "^2.0",
+ "phpunit/phpunit": "^9.0",
"symfony/process": "^4.0|^5.0"
},
"autoload": {
diff --git a/package.json b/package.json
index 781b308..b2bd255 100644
--- a/package.json
+++ b/package.json
@@ -19,10 +19,15 @@
"license": "MIT",
"repository": "github:nesk/puphpeteer",
"engines": {
- "node": ">=8.0.0"
+ "node": ">=9.0.0"
},
"dependencies": {
"@nesk/rialto": "^1.2.1",
- "puppeteer": "~1.18.0"
+ "puppeteer": "~5.5.0"
+ },
+ "devDependencies": {
+ "@types/yargs": "^15.0.10",
+ "typescript": "^4.1.2",
+ "yargs": "^16.1.1"
}
}
diff --git a/phpunit.xml b/phpunit.xml
index 045036e..7534809 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,20 +1,9 @@
-
- tests
-
-
-
-
- src
-
-
+ colors="true">
+
+ tests
+
diff --git a/src/Command/GenerateDocumentationCommand.php b/src/Command/GenerateDocumentationCommand.php
new file mode 100644
index 0000000..9d46d3b
--- /dev/null
+++ b/src/Command/GenerateDocumentationCommand.php
@@ -0,0 +1,178 @@
+addOption(
+ 'puppeteerPath',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The path where Puppeteer is installed.',
+ self::NODE_MODULES_DIR.'/puppeteer'
+ );
+ }
+
+ /**
+ * Builds the documentation generator from TypeScript to JavaScript.
+ */
+ private static function buildDocumentationGenerator(): void
+ {
+ $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();
+
+ $commonFiles = \glob("$puppeteerPath/lib/esm/puppeteer/common/*.d.ts");
+ $nodeFiles = \glob("$puppeteerPath/lib/esm/puppeteer/node/*.d.ts");
+
+ $process = new Process(
+ \array_merge(
+ ['node', self::BUILD_DIR.'/'.self::DOC_FILE_NAME.'.js', 'php'],
+ $commonFiles,
+ $nodeFiles,
+ ['--resources-namespace', self::RESOURCES_NAMESPACE, '--resources'],
+ $resourceNames
+ )
+ );
+ $process->mustRun();
+
+ return \json_decode($process->getOutput(), true);
+ }
+
+ private static function getResourceNames(): array
+ {
+ return array_map(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['properties']);
+ $properties = \implode('', $properties);
+
+ $getters = array_map(function (string $getter): string {
+ return "\n * @property-read $getter";
+ }, $classDocumentation['getters']);
+ $getters = \implode('', $getters);
+
+ $methods = array_map(function (string $method): string {
+ return "\n * @method $method";
+ }, $classDocumentation['methods']);
+ $methods = \implode('', $methods);
+
+ if (\strlen($properties) > 0 || \strlen($getters) > 0 || \strlen($methods) > 0) {
+ 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 ($classDocumentation !== null) {
+ $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation);
+ if ($phpDoc !== null) {
+ $resourceClass = self::RESOURCES_NAMESPACE.'\\'.$resourceName;
+ self::writePhpDoc($resourceClass, $phpDoc);
+ }
+ }
+ }
+
+ // Handle the specific Puppeteer class
+ $classDocumentation = $documentation['Puppeteer'] ?? null;
+ if ($classDocumentation !== null) {
+ $phpDoc = self::generatePhpDocWithDocumentation($classDocumentation);
+ if ($phpDoc !== null) {
+ 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;
+ }
+}
diff --git a/src/Puppeteer.php b/src/Puppeteer.php
index de79aaa..ed8bac3 100644
--- a/src/Puppeteer.php
+++ b/src/Puppeteer.php
@@ -7,6 +7,15 @@
use Nesk\Rialto\AbstractEntryPoint;
use vierbergenlars\SemVer\{version, expression, SemVerException};
+/**
+ * @property-read mixed devices
+ * @property-read mixed errors
+ * @method \Nesk\Puphpeteer\Resources\Browser connect(array $options)
+ * @method void registerCustomQueryHandler(string $name, mixed $queryHandler)
+ * @method void unregisterCustomQueryHandler(string $name)
+ * @method string[] customQueryHandlerNames()
+ * @method void clearCustomQueryHandlers()
+ */
class Puppeteer extends AbstractEntryPoint
{
/**
diff --git a/src/Resources/Accessibility.php b/src/Resources/Accessibility.php
index dc1ce8a..34526d9 100644
--- a/src/Resources/Accessibility.php
+++ b/src/Resources/Accessibility.php
@@ -4,6 +4,9 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method mixed snapshot(array $options = null)
+ */
class Accessibility extends BasicResource
{
//
diff --git a/src/Resources/Browser.php b/src/Resources/Browser.php
index 284e178..a01b14a 100644
--- a/src/Resources/Browser.php
+++ b/src/Resources/Browser.php
@@ -2,9 +2,24 @@
namespace Nesk\Puphpeteer\Resources;
-use Nesk\Rialto\Data\BasicResource;
-
-class Browser extends BasicResource
+/**
+ * @method mixed|null process()
+ * @method \Nesk\Puphpeteer\Resources\BrowserContext createIncognitoBrowserContext()
+ * @method \Nesk\Puphpeteer\Resources\BrowserContext[] browserContexts()
+ * @method \Nesk\Puphpeteer\Resources\BrowserContext defaultBrowserContext()
+ * @method string wsEndpoint()
+ * @method \Nesk\Puphpeteer\Resources\Page newPage()
+ * @method \Nesk\Puphpeteer\Resources\Target[] targets()
+ * @method \Nesk\Puphpeteer\Resources\Target target()
+ * @method \Nesk\Puphpeteer\Resources\Target waitForTarget(callable(\Nesk\Puphpeteer\Resources\Target $x): bool $predicate, array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\Page[] pages()
+ * @method string version()
+ * @method string userAgent()
+ * @method void close()
+ * @method void disconnect()
+ * @method bool isConnected()
+ */
+class Browser extends EventEmitter
{
//
}
diff --git a/src/Resources/BrowserContext.php b/src/Resources/BrowserContext.php
index 5c99647..faca430 100644
--- a/src/Resources/BrowserContext.php
+++ b/src/Resources/BrowserContext.php
@@ -2,9 +2,18 @@
namespace Nesk\Puphpeteer\Resources;
-use Nesk\Rialto\Data\BasicResource;
-
-class BrowserContext extends BasicResource
+/**
+ * @method \Nesk\Puphpeteer\Resources\Target[] targets()
+ * @method \Nesk\Puphpeteer\Resources\Target waitForTarget(callable(\Nesk\Puphpeteer\Resources\Target $x): bool $predicate, array{ timeout: float } $options = null)
+ * @method \Nesk\Puphpeteer\Resources\Page[] pages()
+ * @method bool isIncognito()
+ * @method void overridePermissions(string $origin, string[] $permissions)
+ * @method void clearPermissionOverrides()
+ * @method \Nesk\Puphpeteer\Resources\Page newPage()
+ * @method \Nesk\Puphpeteer\Resources\Browser browser()
+ * @method void close()
+ */
+class BrowserContext extends EventEmitter
{
//
}
diff --git a/src/Resources/BrowserFetcher.php b/src/Resources/BrowserFetcher.php
index 87ef1b3..19f67cd 100644
--- a/src/Resources/BrowserFetcher.php
+++ b/src/Resources/BrowserFetcher.php
@@ -4,6 +4,16 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method mixed platform()
+ * @method mixed product()
+ * @method string host()
+ * @method bool canDownload(string $revision)
+ * @method mixed download(string $revision, callable(float $x, float $y): void $progressCallback = null)
+ * @method string[] localRevisions()
+ * @method void remove(string $revision)
+ * @method mixed revisionInfo(string $revision)
+ */
class BrowserFetcher extends BasicResource
{
//
diff --git a/src/Resources/CDPSession.php b/src/Resources/CDPSession.php
index db4b8eb..4983013 100644
--- a/src/Resources/CDPSession.php
+++ b/src/Resources/CDPSession.php
@@ -2,9 +2,11 @@
namespace Nesk\Puphpeteer\Resources;
-use Nesk\Rialto\Data\BasicResource;
-
-class CDPSession extends BasicResource
+/**
+ * @method mixed send(mixed $method, mixed ...$paramArgs)
+ * @method void detach()
+ */
+class CDPSession extends EventEmitter
{
//
}
diff --git a/src/Resources/ConsoleMessage.php b/src/Resources/ConsoleMessage.php
index 53d716d..86b0b48 100644
--- a/src/Resources/ConsoleMessage.php
+++ b/src/Resources/ConsoleMessage.php
@@ -4,6 +4,13 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method mixed type()
+ * @method string text()
+ * @method \Nesk\Puphpeteer\Resources\JSHandle[] args()
+ * @method mixed location()
+ * @method mixed[] stackTrace()
+ */
class ConsoleMessage extends BasicResource
{
//
diff --git a/src/Resources/Coverage.php b/src/Resources/Coverage.php
index ec106bc..33b8391 100644
--- a/src/Resources/Coverage.php
+++ b/src/Resources/Coverage.php
@@ -4,6 +4,12 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method void startJSCoverage(array $options = null)
+ * @method mixed[] stopJSCoverage()
+ * @method void startCSSCoverage(array $options = null)
+ * @method mixed[] stopCSSCoverage()
+ */
class Coverage extends BasicResource
{
//
diff --git a/src/Resources/Dialog.php b/src/Resources/Dialog.php
index 3fd4138..59eb603 100644
--- a/src/Resources/Dialog.php
+++ b/src/Resources/Dialog.php
@@ -4,6 +4,13 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method mixed type()
+ * @method string message()
+ * @method string defaultValue()
+ * @method void accept(string $promptText = null)
+ * @method void dismiss()
+ */
class Dialog extends BasicResource
{
//
diff --git a/src/Resources/ElementHandle.php b/src/Resources/ElementHandle.php
index 0e96062..a7618b9 100644
--- a/src/Resources/ElementHandle.php
+++ b/src/Resources/ElementHandle.php
@@ -5,6 +5,22 @@
use Nesk\Puphpeteer\Traits\AliasesSelectionMethods;
use Nesk\Puphpeteer\Traits\AliasesEvaluationMethods;
+/**
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null asElement()
+ * @method \Nesk\Puphpeteer\Resources\Frame|null contentFrame()
+ * @method void hover()
+ * @method void click(array $options = null)
+ * @method string[] select(string ...$values)
+ * @method void uploadFile(string ...$filePaths)
+ * @method void tap()
+ * @method void focus()
+ * @method void type(string $text, array{ delay: float } $options = null)
+ * @method void press(mixed $key, array $options = null)
+ * @method mixed|null boundingBox()
+ * @method mixed|null boxModel()
+ * @method string|mixed|null screenshot(array{ } $options = null)
+ * @method bool isIntersectingViewport()
+ */
class ElementHandle extends JSHandle
{
use AliasesSelectionMethods, AliasesEvaluationMethods;
diff --git a/src/Resources/EventEmitter.php b/src/Resources/EventEmitter.php
new file mode 100644
index 0000000..180460c
--- /dev/null
+++ b/src/Resources/EventEmitter.php
@@ -0,0 +1,20 @@
+ $options)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle addStyleTag(array $options)
+ * @method void click(string $selector, array{ delay: float, button: mixed, clickCount: float } $options = null)
+ * @method void focus(string $selector)
+ * @method void hover(string $selector)
+ * @method string[] select(string $selector, string ...$values)
+ * @method void tap(string $selector)
+ * @method void type(string $selector, string $text, array{ delay: float } $options = null)
+ * @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)
+ * @method void waitForTimeout(float $milliseconds)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForSelector(string $selector, array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForXPath(string $xpath, array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\JSHandle waitForFunction(callable|string $pageFunction, array $options = null, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
+ * @method string title()
+ */
class Frame extends BasicResource
{
use AliasesSelectionMethods, AliasesEvaluationMethods;
diff --git a/src/Resources/HTTPRequest.php b/src/Resources/HTTPRequest.php
new file mode 100644
index 0000000..7ce8558
--- /dev/null
+++ b/src/Resources/HTTPRequest.php
@@ -0,0 +1,25 @@
+ headers()
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null response()
+ * @method \Nesk\Puphpeteer\Resources\Frame|null frame()
+ * @method bool isNavigationRequest()
+ * @method \Nesk\Puphpeteer\Resources\HTTPRequest[] redirectChain()
+ * @method array{ errorText: string }|null failure()
+ * @method void continue(mixed $overrides = null)
+ * @method void respond(mixed $response)
+ * @method void abort(mixed $errorCode = null)
+ */
+class HTTPRequest extends BasicResource
+{
+ //
+}
diff --git a/src/Resources/HTTPResponse.php b/src/Resources/HTTPResponse.php
new file mode 100644
index 0000000..d8b6ead
--- /dev/null
+++ b/src/Resources/HTTPResponse.php
@@ -0,0 +1,26 @@
+ headers()
+ * @method \Nesk\Puphpeteer\Resources\SecurityDetails|null securityDetails()
+ * @method mixed buffer()
+ * @method string text()
+ * @method mixed json()
+ * @method \Nesk\Puphpeteer\Resources\HTTPRequest request()
+ * @method bool fromCache()
+ * @method bool fromServiceWorker()
+ * @method \Nesk\Puphpeteer\Resources\Frame|null frame()
+ */
+class HTTPResponse extends BasicResource
+{
+ //
+}
diff --git a/src/Resources/JSHandle.php b/src/Resources/JSHandle.php
index afb9e9f..df9f357 100644
--- a/src/Resources/JSHandle.php
+++ b/src/Resources/JSHandle.php
@@ -4,6 +4,17 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method \Nesk\Puphpeteer\Resources\ExecutionContext executionContext()
+ * @method mixed evaluate(mixed|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
+ * @method \Nesk\Puphpeteer\Resources\JSHandle|\Nesk\Puphpeteer\Resources\ElementHandle evaluateHandle(callable|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
+ * @method \Nesk\Puphpeteer\Resources\JSHandle|null getProperty(string $propertyName)
+ * @method array getProperties()
+ * @method array jsonValue()
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null asElement()
+ * @method void dispose()
+ * @method string toString()
+ */
class JSHandle extends BasicResource
{
//
diff --git a/src/Resources/Keyboard.php b/src/Resources/Keyboard.php
index cf6e58d..dd1ea96 100644
--- a/src/Resources/Keyboard.php
+++ b/src/Resources/Keyboard.php
@@ -4,6 +4,13 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method void down(mixed $key, array{ text: string } $options = null)
+ * @method void up(mixed $key)
+ * @method void sendCharacter(string $char)
+ * @method void type(string $text, array{ delay: float } $options = null)
+ * @method void press(mixed $key, array{ delay: float, text: string } $options = null)
+ */
class Keyboard extends BasicResource
{
//
diff --git a/src/Resources/Mouse.php b/src/Resources/Mouse.php
index d102e7a..c94afe1 100644
--- a/src/Resources/Mouse.php
+++ b/src/Resources/Mouse.php
@@ -4,6 +4,13 @@
use Nesk\Rialto\Data\BasicResource;
+/**
+ * @method void move(float $x, float $y, array{ steps: float } $options = null)
+ * @method void click(float $x, float $y, array&array{ delay: float } $options = null)
+ * @method void down(array $options = null)
+ * @method void up(array $options = null)
+ * @method void wheel(array $options = null)
+ */
class Mouse extends BasicResource
{
//
diff --git a/src/Resources/Page.php b/src/Resources/Page.php
index 8be0ec2..9fe96d6 100644
--- a/src/Resources/Page.php
+++ b/src/Resources/Page.php
@@ -2,11 +2,83 @@
namespace Nesk\Puphpeteer\Resources;
-use Nesk\Rialto\Data\BasicResource;
use Nesk\Puphpeteer\Traits\AliasesSelectionMethods;
use Nesk\Puphpeteer\Traits\AliasesEvaluationMethods;
-class Page extends BasicResource
+/**
+ * @property-read \Nesk\Puphpeteer\Resources\Keyboard keyboard
+ * @property-read \Nesk\Puphpeteer\Resources\Touchscreen touchscreen
+ * @property-read \Nesk\Puphpeteer\Resources\Coverage coverage
+ * @property-read \Nesk\Puphpeteer\Resources\Tracing tracing
+ * @property-read \Nesk\Puphpeteer\Resources\Accessibility accessibility
+ * @property-read \Nesk\Puphpeteer\Resources\Mouse mouse
+ * @method bool isJavaScriptEnabled()
+ * @method \Nesk\Puphpeteer\Resources\FileChooser waitForFileChooser(array $options = null)
+ * @method void setGeolocation(array $options)
+ * @method \Nesk\Puphpeteer\Resources\Target target()
+ * @method \Nesk\Puphpeteer\Resources\Browser browser()
+ * @method \Nesk\Puphpeteer\Resources\BrowserContext browserContext()
+ * @method \Nesk\Puphpeteer\Resources\Frame mainFrame()
+ * @method \Nesk\Puphpeteer\Resources\Frame[] frames()
+ * @method \Nesk\Puphpeteer\Resources\WebWorker[] workers()
+ * @method void setRequestInterception(bool $value)
+ * @method void setOfflineMode(bool $enabled)
+ * @method void setDefaultNavigationTimeout(float $timeout)
+ * @method void setDefaultTimeout(float $timeout)
+ * @method mixed evaluateHandle(callable|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
+ * @method \Nesk\Puphpeteer\Resources\JSHandle queryObjects(\Nesk\Puphpeteer\Resources\JSHandle $prototypeHandle)
+ * @method mixed[] cookies(string ...$urls)
+ * @method void deleteCookie(mixed ...$cookies)
+ * @method void setCookie(mixed ...$cookies)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle addScriptTag(array{ url: string, path: string, content: string, type: string } $options)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle addStyleTag(array{ url: string, path: string, content: string } $options)
+ * @method void exposeFunction(string $name, callable $puppeteerFunction)
+ * @method void authenticate(mixed $credentials)
+ * @method void setExtraHTTPHeaders(array $headers)
+ * @method void setUserAgent(string $userAgent)
+ * @method mixed metrics()
+ * @method string url()
+ * @method string content()
+ * @method void setContent(string $html, array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse goto(string $url, array&array{ referer: string } $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null reload(array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPRequest waitForRequest(string|callable $urlOrPredicate, array{ timeout: float } $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse waitForResponse(string|callable $urlOrPredicate, array{ timeout: float } $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goBack(array $options = null)
+ * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goForward(array $options = null)
+ * @method void bringToFront()
+ * @method void emulate(array{ viewport: mixed, userAgent: string } $options)
+ * @method void setJavaScriptEnabled(bool $enabled)
+ * @method void setBypassCSP(bool $enabled)
+ * @method void emulateMediaType(string $type = null)
+ * @method void emulateMediaFeatures(mixed[] $features = null)
+ * @method void emulateTimezone(string $timezoneId = null)
+ * @method void emulateIdleState(array{ isUserActive: bool, isScreenUnlocked: bool } $overrides = null)
+ * @method void emulateVisionDeficiency(mixed $type = null)
+ * @method void setViewport(mixed $viewport)
+ * @method mixed|null viewport()
+ * @method mixed evaluate(mixed $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args)
+ * @method void evaluateOnNewDocument(callable|string $pageFunction, mixed ...$args)
+ * @method void setCacheEnabled(bool $enabled = null)
+ * @method mixed|string|null screenshot(array $options = null)
+ * @method mixed pdf(array $options = null)
+ * @method string title()
+ * @method void close(array{ runBeforeUnload: bool } $options = null)
+ * @method bool isClosed()
+ * @method void click(string $selector, array{ delay: float, button: mixed, clickCount: float } $options = null)
+ * @method void focus(string $selector)
+ * @method void hover(string $selector)
+ * @method string[] select(string $selector, string ...$values)
+ * @method void tap(string $selector)
+ * @method void type(string $selector, string $text, array{ delay: float } $options = null)
+ * @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)
+ * @method void waitForTimeout(float $milliseconds)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForSelector(string $selector, array{ visible: bool, hidden: bool, timeout: float } $options = null)
+ * @method \Nesk\Puphpeteer\Resources\ElementHandle|null waitForXPath(string $xpath, array{ visible: bool, hidden: bool, timeout: float } $options = null)
+ * @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)
+ */
+class Page extends EventEmitter
{
use AliasesSelectionMethods, AliasesEvaluationMethods;
}
diff --git a/src/Resources/Request.php b/src/Resources/Request.php
deleted file mode 100644
index 9bb72ab..0000000
--- a/src/Resources/Request.php
+++ /dev/null
@@ -1,10 +0,0 @@
- $options = null)
+ * @method mixed stop()
+ */
class Tracing extends BasicResource
{
//
diff --git a/src/Resources/WebWorker.php b/src/Resources/WebWorker.php
new file mode 100644
index 0000000..764ebe7
--- /dev/null
+++ b/src/Resources/WebWorker.php
@@ -0,0 +1,16 @@
+ querySelectorAll(string $selector)
+ * @method array querySelectorXPath(string $expression)
+ */
trait AliasesSelectionMethods
{
public function querySelector(...$arguments)
diff --git a/src/doc-generator.ts b/src/doc-generator.ts
new file mode 100644
index 0000000..0c133e7
--- /dev/null
+++ b/src/doc-generator.ts
@@ -0,0 +1,595 @@
+import * as ts from 'typescript';
+const yargs = require('yargs/yargs');
+const { hideBin } = require('yargs/helpers');
+
+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(message || 'This type is currently not supported.');
+ }
+}
+
+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(
+ private readonly resourcesNamespace: string,
+ private 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 `callable(${parameters}): ${returnType}`;
+ }
+
+ formatFunction(name: string, parameters: string, returnType: string): string {
+ return `${returnType} ${name}(${parameters})`;
+ }
+
+ formatParameter(name: string, type: string, isVariadic: boolean, isOptional: boolean): string {
+ if (isVariadic && type.endsWith('[]')) {
+ type = type.slice(0, -2);
+ }
+
+ const defaultValue = isOptional ? ' = null' : '';
+ 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}\\${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(['callable', 'string']);
+ }
+
+ if (type === 'Function') {
+ return 'callable';
+ }
+
+ 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{ ${members.join(', ')} }`;
+ }
+
+ formatArray(type: string): string {
+ return `${type}[]`;
+ }
+}
+
+class DocumentationGenerator {
+ constructor(
+ private readonly supportChecker: SupportChecker,
+ private readonly formatter: DocumentationFormatter,
+ ) {}
+
+ private hasModifierForNode(
+ node: ts.Node,
+ 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('_')) {
+ 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 getParameterDeclarationAsString(node: ts.ParameterDeclaration): string {
+ const name = this.getNamedDeclarationAsString(node);
+ const type = this.getTypeNodeAsString(node.type);
+ const isVariadic = node.dotDotDotToken !== undefined;
+ const isOptional = node.questionToken !== undefined;
+ return this.formatter.formatParameter(name, type, isVariadic, isOptional);
+ }
+
+ private getTypeNodeAsString(node: ts.TypeNode, context?: TypeContext): string {
+ 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 {
+ throw new TypeNotSupportedError();
+ }
+ }
+
+ 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)) {
+ throw new TypeNotSupportedError();
+ }
+ return this.getIdentifierAsString(node.name);
+ }
+
+ private getIdentifierAsString(node: ts.Identifier): 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;
+ 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));
diff --git a/tests/PuphpeteerTest.php b/tests/PuphpeteerTest.php
index c30ad1c..d36247e 100644
--- a/tests/PuphpeteerTest.php
+++ b/tests/PuphpeteerTest.php
@@ -6,6 +6,7 @@
use Nesk\Rialto\Data\JsFunction;
use PHPUnit\Framework\ExpectationFailedException;
use Nesk\Puphpeteer\Resources\ElementHandle;
+use Psr\Log\LoggerInterface;
class PuphpeteerTest extends TestCase
{
@@ -156,30 +157,63 @@ public function resourceProvider(): \Generator
}
}
+ private function createBrowserLogger(callable $onBrowserLog): LoggerInterface
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+ $logger->expects(self::atLeastOnce())
+ ->method('log')
+ ->willReturn(self::returnCallback(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()
+ public function browser_console_calls_are_logged_if_enabled()
{
- $setups = [
- [false, function ($browser) { return $browser->newPage(); }, 'Received data from the port'],
- [true, function ($browser) { return $browser->newPage(); }, 'Received a Browser log:'],
- [true, function ($browser) { return $browser->pages()[0]; }, 'Received a Browser log:'],
- ];
-
- foreach ($setups as [$shoulLogBrowserConsole, $pageFactory, $startsWith]) {
- $puppeteer = new Puppeteer([
- 'log_browser_console' => $shoulLogBrowserConsole,
- 'logger' => $this->loggerMock(
- $this->at(9),
- $this->isLogLevel(),
- $this->stringStartsWith($startsWith)
- ),
- ]);
-
- $this->browser = $puppeteer->launch($this->browserOptions);
- $pageFactory($this->browser)->goto($this->url);
- }
+ $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);
}
}
diff --git a/tests/ResourceInstantiator.php b/tests/ResourceInstantiator.php
index e77aa12..2d8fe0d 100644
--- a/tests/ResourceInstantiator.php
+++ b/tests/ResourceInstantiator.php
@@ -40,12 +40,24 @@ public function __construct(array $browserOptions, string $url) {
'ElementHandle' => function ($puppeteer) {
return $this->Page($puppeteer)->querySelector('body');
},
+ 'EventEmitter' => function ($puppeteer) {
+ return $puppeteer->launch($this->browserOptions);
+ },
'ExecutionContext' => function ($puppeteer) {
return $this->Frame($puppeteer)->executionContext();
},
+ 'FileChooser' => function () {
+ return new UntestableResource;
+ },
'Frame' => function ($puppeteer) {
return $this->Page($puppeteer)->mainFrame();
},
+ 'HTTPRequest' => function ($puppeteer) {
+ return $this->HTTPResponse($puppeteer)->request();
+ },
+ 'HTTPResponse' => function ($puppeteer) {
+ return $this->Page($puppeteer)->goto($this->url);
+ },
'JSHandle' => function ($puppeteer) {
return $this->Page($puppeteer)->evaluateHandle(JsFunction::createWithBody('window'));
},
@@ -58,12 +70,6 @@ public function __construct(array $browserOptions, string $url) {
'Page' => function ($puppeteer) {
return $this->Browser($puppeteer)->newPage();
},
- 'Request' => function ($puppeteer) {
- return $this->Response($puppeteer)->request();
- },
- 'Response' => function ($puppeteer) {
- return $this->Page($puppeteer)->goto($this->url);
- },
'SecurityDetails' => function ($puppeteer) {
return new RiskyResource(function () use ($puppeteer) {
return $this->Page($puppeteer)->goto('https://example.com')->securityDetails();
@@ -81,7 +87,7 @@ public function __construct(array $browserOptions, string $url) {
'Tracing' => function ($puppeteer) {
return $this->Page($puppeteer)->tracing;
},
- 'Worker' => function ($puppeteer) {
+ 'WebWorker' => function ($puppeteer) {
$page = $this->Page($puppeteer);
$page->goto($this->url, ['waitUntil' => 'networkidle0']);
return $page->workers()[0];
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 0361225..9c75b8a 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -82,28 +82,6 @@ public function canPopulateProperty(string $propertyName): bool
return !in_array($propertyName, $this->dontPopulateProperties);
}
- public function loggerMock($expectations) {
- $loggerMock = $this->getMockBuilder(Logger::class)
- ->setConstructorArgs(['rialto'])
- ->setMethods(['log'])
- ->getMock();
-
- if ($expectations instanceof Invocation) {
- $expectations = [func_get_args()];
- }
-
- foreach ($expectations as $expectation) {
- [$matcher] = $expectation;
- $with = array_slice($expectation, 1);
-
- $loggerMock->expects($matcher)
- ->method('log')
- ->with(...$with);
- }
-
- return $loggerMock;
- }
-
public function isLogLevel(): Callback {
$psrLogLevels = (new ReflectionClass(LogLevel::class))->getConstants();
$monologLevels = (new ReflectionClass(Logger::class))->getConstants();