From 36c564a8f254d51ff8858f9bb2ba81a408d12e48 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Fri, 30 Oct 2020 15:56:32 +0100 Subject: [PATCH 01/18] Support Puppeteer v5.4 --- package.json | 4 ++-- src/Resources/{Response.php => HTTPRequest.php} | 2 +- src/Resources/{Worker.php => HTTPResponse.php} | 2 +- src/Resources/{Request.php => WebWorker.php} | 2 +- tests/ResourceInstantiator.php | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) rename src/Resources/{Response.php => HTTPRequest.php} (69%) rename src/Resources/{Worker.php => HTTPResponse.php} (69%) rename src/Resources/{Request.php => WebWorker.php} (70%) diff --git a/package.json b/package.json index 781b308..23a2223 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ "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.4.1" } } diff --git a/src/Resources/Response.php b/src/Resources/HTTPRequest.php similarity index 69% rename from src/Resources/Response.php rename to src/Resources/HTTPRequest.php index 34a2ce5..4d3fedc 100644 --- a/src/Resources/Response.php +++ b/src/Resources/HTTPRequest.php @@ -4,7 +4,7 @@ use Nesk\Rialto\Data\BasicResource; -class Response extends BasicResource +class HTTPRequest extends BasicResource { // } diff --git a/src/Resources/Worker.php b/src/Resources/HTTPResponse.php similarity index 69% rename from src/Resources/Worker.php rename to src/Resources/HTTPResponse.php index 6d3cac0..395de14 100644 --- a/src/Resources/Worker.php +++ b/src/Resources/HTTPResponse.php @@ -4,7 +4,7 @@ use Nesk\Rialto\Data\BasicResource; -class Worker extends BasicResource +class HTTPResponse extends BasicResource { // } diff --git a/src/Resources/Request.php b/src/Resources/WebWorker.php similarity index 70% rename from src/Resources/Request.php rename to src/Resources/WebWorker.php index 9bb72ab..8df7033 100644 --- a/src/Resources/Request.php +++ b/src/Resources/WebWorker.php @@ -4,7 +4,7 @@ use Nesk\Rialto\Data\BasicResource; -class Request extends BasicResource +class WebWorker extends BasicResource { // } diff --git a/tests/ResourceInstantiator.php b/tests/ResourceInstantiator.php index e77aa12..f049b87 100644 --- a/tests/ResourceInstantiator.php +++ b/tests/ResourceInstantiator.php @@ -46,6 +46,12 @@ public function __construct(array $browserOptions, string $url) { '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 +64,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 +81,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]; From 291107197e9d2f533be3763030cde6040ce0cd76 Mon Sep 17 00:00:00 2001 From: Peter Thaleikis Date: Wed, 21 Oct 2020 16:32:38 +0400 Subject: [PATCH 02/18] Dropping 7.1, adding 7.4 --- .github/workflows/tests.yaml | 2 +- README.md | 2 +- composer.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 33b6d75..3f5834b 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.2, 7.3, 7.4] composer-flags: [null, --prefer-lowest] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 319dce6..e486705 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.2 and Node >= 8. Install it with these two command lines: diff --git a/composer.json b/composer.json index f9e5ea6..d649fe4 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } ], "require": { - "php": ">=7.1", + "php": ">=7.2", "nesk/rialto": "^1.2.0", "psr/log": "^1.0", "vierbergenlars/php-semver": "^3.0.2" From 77bdd4d6b1884d3ecd0272d5ac3eeba086c871c3 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Fri, 13 Nov 2020 19:10:19 +0100 Subject: [PATCH 03/18] Close #43: Add typings to selection methods --- src/Traits/AliasesEvaluationMethods.php | 6 ++++++ src/Traits/AliasesSelectionMethods.php | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/Traits/AliasesEvaluationMethods.php b/src/Traits/AliasesEvaluationMethods.php index 6ca0066..fe29882 100644 --- a/src/Traits/AliasesEvaluationMethods.php +++ b/src/Traits/AliasesEvaluationMethods.php @@ -2,6 +2,12 @@ namespace Nesk\Puphpeteer\Traits; +use Nesk\Rialto\Data\JsFunction; + +/** + * @method bool|int|float|string|array|null querySelectorEval(string $selector, JsFunction $pageFunction, bool|int|float|string|array|null|JSHandle ...args) + * @method bool|int|float|string|array|null querySelectorAllEval(string $selector, JsFunction $pageFunction, bool|int|float|string|array|null|JSHandle ...args) + */ trait AliasesEvaluationMethods { public function querySelectorEval(...$arguments) diff --git a/src/Traits/AliasesSelectionMethods.php b/src/Traits/AliasesSelectionMethods.php index c250dfb..22d4380 100644 --- a/src/Traits/AliasesSelectionMethods.php +++ b/src/Traits/AliasesSelectionMethods.php @@ -2,6 +2,11 @@ namespace Nesk\Puphpeteer\Traits; +/** + * @method ElementHandle|null querySelector(string $selector) + * @method array querySelectorAll(string $selector) + * @method array querySelectorXPath(string $expression) + */ trait AliasesSelectionMethods { public function querySelector(...$arguments) From d719d83764e3101ec5063bd4fa73575b81c6f593 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:11:14 +0100 Subject: [PATCH 04/18] Add missing FileChooser resource --- src/Resources/FileChooser.php | 10 ++++++++++ tests/ResourceInstantiator.php | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 src/Resources/FileChooser.php diff --git a/src/Resources/FileChooser.php b/src/Resources/FileChooser.php new file mode 100644 index 0000000..0b45e81 --- /dev/null +++ b/src/Resources/FileChooser.php @@ -0,0 +1,10 @@ + function ($puppeteer) { return $this->Frame($puppeteer)->executionContext(); }, + 'FileChooser' => function () { + return new UntestableResource; + }, 'Frame' => function ($puppeteer) { return $this->Page($puppeteer)->mainFrame(); }, From d3b4b251ceb570e2bfc6257310b01c526bd69c7b Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:11:33 +0100 Subject: [PATCH 05/18] Add missing EventEmitter resource --- src/Resources/Browser.php | 4 +--- src/Resources/BrowserContext.php | 4 +--- src/Resources/CDPSession.php | 4 +--- src/Resources/EventEmitter.php | 10 ++++++++++ src/Resources/Page.php | 3 +-- tests/ResourceInstantiator.php | 3 +++ 6 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 src/Resources/EventEmitter.php diff --git a/src/Resources/Browser.php b/src/Resources/Browser.php index 284e178..e24f17b 100644 --- a/src/Resources/Browser.php +++ b/src/Resources/Browser.php @@ -2,9 +2,7 @@ namespace Nesk\Puphpeteer\Resources; -use Nesk\Rialto\Data\BasicResource; - -class Browser extends BasicResource +class Browser extends EventEmitter { // } diff --git a/src/Resources/BrowserContext.php b/src/Resources/BrowserContext.php index 5c99647..0df197b 100644 --- a/src/Resources/BrowserContext.php +++ b/src/Resources/BrowserContext.php @@ -2,9 +2,7 @@ namespace Nesk\Puphpeteer\Resources; -use Nesk\Rialto\Data\BasicResource; - -class BrowserContext extends BasicResource +class BrowserContext extends EventEmitter { // } diff --git a/src/Resources/CDPSession.php b/src/Resources/CDPSession.php index db4b8eb..714ace9 100644 --- a/src/Resources/CDPSession.php +++ b/src/Resources/CDPSession.php @@ -2,9 +2,7 @@ namespace Nesk\Puphpeteer\Resources; -use Nesk\Rialto\Data\BasicResource; - -class CDPSession extends BasicResource +class CDPSession extends EventEmitter { // } diff --git a/src/Resources/EventEmitter.php b/src/Resources/EventEmitter.php new file mode 100644 index 0000000..31f9075 --- /dev/null +++ b/src/Resources/EventEmitter.php @@ -0,0 +1,10 @@ + function ($puppeteer) { return $this->Page($puppeteer)->querySelector('body'); }, + 'EventEmitter' => function ($puppeteer) { + return $puppeteer->launch($this->browserOptions); + }, 'ExecutionContext' => function ($puppeteer) { return $this->Frame($puppeteer)->executionContext(); }, From bbae6b849f0e74c93abe9806f7910c68fb1a92d6 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:13:21 +0100 Subject: [PATCH 06/18] Install yargs package --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 23a2223..fc10ae3 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,9 @@ "dependencies": { "@nesk/rialto": "^1.2.1", "puppeteer": "~5.4.1" + }, + "devDependencies": { + "@types/yargs": "^15.0.10", + "yargs": "^16.1.1" } } From 56e46b19e03d56ec02ffcd3f832bc5676d3f7811 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:13:31 +0100 Subject: [PATCH 07/18] Install typescript package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index fc10ae3..7d12159 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@types/yargs": "^15.0.10", + "typescript": "^4.1.2", "yargs": "^16.1.1" } } From a5305dc338d5cd637f946d47f0be106b0806b83e Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:14:34 +0100 Subject: [PATCH 08/18] Create a documentation generator Uses the TS compiler to extract everything from Puppeteer package --- src/doc-generator.ts | 595 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 src/doc-generator.ts 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)); From 7542e9c7c74531e0b9ade8b8ed68509706af46e3 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Sun, 22 Nov 2020 22:17:35 +0100 Subject: [PATCH 09/18] Create a PHP command to generate and write the documentation --- .gitignore | 1 + src/Command/GenerateDocumentationCommand.php | 178 +++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/Command/GenerateDocumentationCommand.php diff --git a/.gitignore b/.gitignore index 7dc1fb2..fcf24a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.build/ /node_modules/ /vendor/ composer.lock 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; + } +} From 121e0b7ad559de6ee4b285893a45acca9dda1fe7 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Sun, 22 Nov 2020 22:17:56 +0100 Subject: [PATCH 10/18] Create a console executable to run commands --- bin/console | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 bin/console 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(); From 3aa95696473a44d090f2fe410b41ad6cbac6ca7e Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:20:37 +0100 Subject: [PATCH 11/18] Update resources documentation --- src/Puppeteer.php | 9 ++++ src/Resources/Accessibility.php | 3 ++ src/Resources/Browser.php | 17 +++++++ src/Resources/BrowserContext.php | 11 +++++ src/Resources/BrowserFetcher.php | 10 ++++ src/Resources/CDPSession.php | 4 ++ src/Resources/ConsoleMessage.php | 7 +++ src/Resources/Coverage.php | 6 +++ src/Resources/Dialog.php | 7 +++ src/Resources/ElementHandle.php | 16 +++++++ src/Resources/EventEmitter.php | 10 ++++ src/Resources/ExecutionContext.php | 6 +++ src/Resources/FileChooser.php | 5 ++ src/Resources/Frame.php | 28 ++++++++++++ src/Resources/HTTPRequest.php | 15 ++++++ src/Resources/HTTPResponse.php | 16 +++++++ src/Resources/JSHandle.php | 11 +++++ src/Resources/Keyboard.php | 7 +++ src/Resources/Mouse.php | 7 +++ src/Resources/Page.php | 73 ++++++++++++++++++++++++++++++ src/Resources/SecurityDetails.php | 8 ++++ src/Resources/Target.php | 10 ++++ src/Resources/Touchscreen.php | 3 ++ src/Resources/Tracing.php | 4 ++ src/Resources/WebWorker.php | 6 +++ 25 files changed, 299 insertions(+) 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 e24f17b..a01b14a 100644 --- a/src/Resources/Browser.php +++ b/src/Resources/Browser.php @@ -2,6 +2,23 @@ namespace Nesk\Puphpeteer\Resources; +/** + * @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 0df197b..faca430 100644 --- a/src/Resources/BrowserContext.php +++ b/src/Resources/BrowserContext.php @@ -2,6 +2,17 @@ namespace Nesk\Puphpeteer\Resources; +/** + * @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 714ace9..4983013 100644 --- a/src/Resources/CDPSession.php +++ b/src/Resources/CDPSession.php @@ -2,6 +2,10 @@ namespace Nesk\Puphpeteer\Resources; +/** + * @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 index 31f9075..180460c 100644 --- a/src/Resources/EventEmitter.php +++ b/src/Resources/EventEmitter.php @@ -4,6 +4,16 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method \Nesk\Puphpeteer\Resources\EventEmitter on(mixed $event, mixed $handler) + * @method \Nesk\Puphpeteer\Resources\EventEmitter off(mixed $event, mixed $handler) + * @method \Nesk\Puphpeteer\Resources\EventEmitter removeListener(mixed $event, mixed $handler) + * @method \Nesk\Puphpeteer\Resources\EventEmitter addListener(mixed $event, mixed $handler) + * @method bool emit(mixed $event, mixed $eventData = null) + * @method \Nesk\Puphpeteer\Resources\EventEmitter once(mixed $event, mixed $handler) + * @method float listenerCount(mixed $event) + * @method \Nesk\Puphpeteer\Resources\EventEmitter removeAllListeners(mixed $event = null) + */ class EventEmitter extends BasicResource { // diff --git a/src/Resources/ExecutionContext.php b/src/Resources/ExecutionContext.php index a7f6708..f506b22 100644 --- a/src/Resources/ExecutionContext.php +++ b/src/Resources/ExecutionContext.php @@ -4,6 +4,12 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method \Nesk\Puphpeteer\Resources\Frame|null frame() + * @method mixed evaluate(callable|string $pageFunction, mixed ...$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 queryObjects(\Nesk\Puphpeteer\Resources\JSHandle $prototypeHandle) + */ class ExecutionContext extends BasicResource { // diff --git a/src/Resources/FileChooser.php b/src/Resources/FileChooser.php index 0b45e81..b0b5cf3 100644 --- a/src/Resources/FileChooser.php +++ b/src/Resources/FileChooser.php @@ -4,6 +4,11 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method bool isMultiple() + * @method void accept(string[] $filePaths) + * @method void cancel() + */ class FileChooser extends BasicResource { // diff --git a/src/Resources/Frame.php b/src/Resources/Frame.php index e0d32b7..87948a4 100644 --- a/src/Resources/Frame.php +++ b/src/Resources/Frame.php @@ -6,6 +6,34 @@ use Nesk\Puphpeteer\Traits\AliasesSelectionMethods; use Nesk\Puphpeteer\Traits\AliasesEvaluationMethods; +/** + * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null goto(string $url, array{ referer: string, timeout: float, waitUntil: string|string[] } $options = null) + * @method \Nesk\Puphpeteer\Resources\HTTPResponse|null waitForNavigation(array{ timeout: float, waitUntil: string|string[] } $options = null) + * @method \Nesk\Puphpeteer\Resources\ExecutionContext executionContext() + * @method mixed evaluateHandle(callable|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args) + * @method mixed evaluate(mixed $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args) + * @method string content() + * @method void setContent(string $html, array{ timeout: float, waitUntil: string|string[] } $options = null) + * @method string name() + * @method string url() + * @method \Nesk\Puphpeteer\Resources\Frame|null parentFrame() + * @method \Nesk\Puphpeteer\Resources\Frame[] childFrames() + * @method bool isDetached() + * @method \Nesk\Puphpeteer\Resources\ElementHandle addScriptTag(array $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 index 4d3fedc..7ce8558 100644 --- a/src/Resources/HTTPRequest.php +++ b/src/Resources/HTTPRequest.php @@ -4,6 +4,21 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method string url() + * @method string resourceType() + * @method string method() + * @method string|null postData() + * @method array 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 index 395de14..d8b6ead 100644 --- a/src/Resources/HTTPResponse.php +++ b/src/Resources/HTTPResponse.php @@ -4,6 +4,22 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method mixed remoteAddress() + * @method string url() + * @method bool ok() + * @method float status() + * @method string statusText() + * @method array 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 51ca440..9fe96d6 100644 --- a/src/Resources/Page.php +++ b/src/Resources/Page.php @@ -5,6 +5,79 @@ use Nesk\Puphpeteer\Traits\AliasesSelectionMethods; use Nesk\Puphpeteer\Traits\AliasesEvaluationMethods; +/** + * @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/SecurityDetails.php b/src/Resources/SecurityDetails.php index 6b0298a..2b40242 100644 --- a/src/Resources/SecurityDetails.php +++ b/src/Resources/SecurityDetails.php @@ -4,6 +4,14 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method string issuer() + * @method float validFrom() + * @method float validTo() + * @method string protocol() + * @method string subjectName() + * @method string[] subjectAlternativeNames() + */ class SecurityDetails extends BasicResource { // diff --git a/src/Resources/Target.php b/src/Resources/Target.php index de09769..0a51543 100644 --- a/src/Resources/Target.php +++ b/src/Resources/Target.php @@ -4,6 +4,16 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method \Nesk\Puphpeteer\Resources\CDPSession createCDPSession() + * @method \Nesk\Puphpeteer\Resources\Page|null page() + * @method \Nesk\Puphpeteer\Resources\WebWorker|null worker() + * @method string url() + * @method string type() + * @method \Nesk\Puphpeteer\Resources\Browser browser() + * @method \Nesk\Puphpeteer\Resources\BrowserContext browserContext() + * @method \Nesk\Puphpeteer\Resources\Target|null opener() + */ class Target extends BasicResource { // diff --git a/src/Resources/Touchscreen.php b/src/Resources/Touchscreen.php index 5b6e601..80bae89 100644 --- a/src/Resources/Touchscreen.php +++ b/src/Resources/Touchscreen.php @@ -4,6 +4,9 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method void tap(float $x, float $y) + */ class Touchscreen extends BasicResource { // diff --git a/src/Resources/Tracing.php b/src/Resources/Tracing.php index 942fa0d..6cc364d 100644 --- a/src/Resources/Tracing.php +++ b/src/Resources/Tracing.php @@ -4,6 +4,10 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method void start(array $options = null) + * @method mixed stop() + */ class Tracing extends BasicResource { // diff --git a/src/Resources/WebWorker.php b/src/Resources/WebWorker.php index 8df7033..764ebe7 100644 --- a/src/Resources/WebWorker.php +++ b/src/Resources/WebWorker.php @@ -4,6 +4,12 @@ use Nesk\Rialto\Data\BasicResource; +/** + * @method string url() + * @method \Nesk\Puphpeteer\Resources\ExecutionContext executionContext() + * @method mixed evaluate(callable|string $pageFunction, mixed ...$args) + * @method \Nesk\Puphpeteer\Resources\JSHandle evaluateHandle(callable|string $pageFunction, int|float|string|bool|null|array|\Nesk\Puphpeteer\Resources\JSHandle ...$args) + */ class WebWorker extends BasicResource { // From c1ea4dbb1e7f7a30da16b847113b2f30e1519aa1 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Sun, 22 Nov 2020 22:56:54 +0100 Subject: [PATCH 12/18] Upgrade to monolog v2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d649fe4..c3d7a17 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "vierbergenlars/php-semver": "^3.0.2" }, "require-dev": { - "monolog/monolog": "^1.23", + "monolog/monolog": "^2.0", "phpunit/phpunit": "^6.5|^7.0", "symfony/process": "^4.0|^5.0" }, From 7779e00455ee4280b3f1c575b60c6eb721257957 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Mon, 23 Nov 2020 14:00:22 +0100 Subject: [PATCH 13/18] Rework tests for browser logs Better decoupling and compatible with PHPUnit 9 --- tests/PuphpeteerTest.php | 74 +++++++++++++++++++++++++++++----------- tests/TestCase.php | 22 ------------ 2 files changed, 54 insertions(+), 42 deletions(-) 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/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(); From 67e6eb900b6032fcebc996f09595b6642b35a281 Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Mon, 23 Nov 2020 14:01:07 +0100 Subject: [PATCH 14/18] Upgrade to PHPUnit v9 --- .gitignore | 1 + composer.json | 2 +- phpunit.xml | 21 +++++---------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index fcf24a6..ca31087 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.build/ /node_modules/ /vendor/ +.phpunit.result.cache composer.lock package-lock.json diff --git a/composer.json b/composer.json index c3d7a17..0ec2293 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ }, "require-dev": { "monolog/monolog": "^2.0", - "phpunit/phpunit": "^6.5|^7.0", + "phpunit/phpunit": "^9.0", "symfony/process": "^4.0|^5.0" }, "autoload": { 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 + From 07b2c725eab5977136670f48d71d5de2f550500b Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Mon, 23 Nov 2020 14:01:47 +0100 Subject: [PATCH 15/18] Drop support for PHP 7.2 --- .github/workflows/tests.yaml | 2 +- README.md | 2 +- composer.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3f5834b..6dc8d59 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.2, 7.3, 7.4] + php-version: [7.3, 7.4] composer-flags: [null, --prefer-lowest] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index e486705..a3d763c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ $browser->close(); ## Requirements and installation -This package requires PHP >= 7.2 and Node >= 8. +This package requires PHP >= 7.3 and Node >= 8. Install it with these two command lines: diff --git a/composer.json b/composer.json index 0ec2293..440800d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } ], "require": { - "php": ">=7.2", + "php": ">=7.3", "nesk/rialto": "^1.2.0", "psr/log": "^1.0", "vierbergenlars/php-semver": "^3.0.2" From 7b51883173473c36c53e99b66072bc92538e790a Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Mon, 23 Nov 2020 14:02:35 +0100 Subject: [PATCH 16/18] Add PHP 8.0 to the CI matrix --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6dc8d59..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.3, 7.4] + php-version: [7.3, 7.4, 8.0] composer-flags: [null, --prefer-lowest] steps: - uses: actions/checkout@v2 From d033f119705bb4035add55bc7d5e67aa6d3118da Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Mon, 23 Nov 2020 23:23:56 +0100 Subject: [PATCH 17/18] Upgrade to Puppeteer 5.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d12159..b2bd255 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@nesk/rialto": "^1.2.1", - "puppeteer": "~5.4.1" + "puppeteer": "~5.5.0" }, "devDependencies": { "@types/yargs": "^15.0.10", From e972abd85d75a34f285806e9c87e61c2177c9c9c Mon Sep 17 00:00:00 2001 From: Johann Pardanaud Date: Tue, 24 Nov 2020 13:16:29 +0100 Subject: [PATCH 18/18] Prepare changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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