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();