diff --git a/docs/api.md b/docs/api.md index 32b5f2e8a9b..dc59e0e729c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -13,6 +13,17 @@ * [browser.stdout](#browserstdout) * [browser.version()](#browserversion) - [class: Page](#class-page) + * [event: 'consolemessage'](#event-consolemessage) + * [event: 'dialog'](#event-dialog) + * [event: 'frameattached'](#event-frameattached) + * [event: 'framedetached'](#event-framedetached) + * [event: 'framenavigated'](#event-framenavigated) + * [event: 'load'](#event-load) + * [event: 'pageerror'](#event-pageerror) + * [event: 'request'](#event-request) + * [event: 'requestfailed'](#event-requestfailed) + * [event: 'requestfinished'](#event-requestfinished) + * [event: 'response'](#event-response) * [page.addScriptTag(url)](#pageaddscripttagurl) * [page.click(selector)](#pageclickselector) * [page.close()](#pageclose) @@ -179,6 +190,59 @@ browser.newPage().then(async page => }); ``` +#### event: 'consolemessage' +- <[string]> + +Emitted when a page calls one of console API methods, e.g. `console.log`. + +#### event: 'dialog' +- <[Dialog]> + +Emitted when a javascript dialog, such as `alert`, `prompt`, `confirm` or `beforeunload`, gets opened on the page. Puppeteer can take action to the dialog via dialog's [accept](#dialogacceptprompttext) or [dismiss](#dialogdismiss) methods. + +#### event: 'frameattached' +- <[Frame]> + +Emitted when a frame gets attached. + +#### event: 'framedetached' +- <[Frame]> + +Emitted when a frame gets detached. + +#### event: 'framenavigated' +- <[Frame]> + +Emitted when a frame committed navigation. + +#### event: 'load' + +Emitted when a page's `load` event was dispatched. + +#### event: 'pageerror' +- <[string]> + +Emitted when an unhandled exception happens on the page. The only argument of the event holds the exception message. + +#### event: 'request' +- <[Request]> + +Emitted when a page issues a request. The [request] object is a read-only object. In order to intercept and mutate requests, see [page.setRequestInterceptor](#pagesetrequestinterceptorinterceptor) + +#### event: 'requestfailed' +- <[Request]> + +Emitted when a request is failed. + +#### event: 'requestfinished' +- <[Request]> + +Emitted when a request is successfully finished. + +#### event: 'response' +- <[Response]> + +Emitted when a [response] is received. #### page.addScriptTag(url) - `url` <[string]> Url of a script to be added @@ -373,6 +437,9 @@ This is a shortcut for [page.mainFrame().url()](#frameurl) Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector). ### class: Dialog + +[Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event. + #### dialog.accept([promptText]) - `promptText` <[string]> A text to enter in prompt. Does not cause any effects if the dialog's `type` is not prompt. - returns: <[Promise]> Promise which resolves when the dialog has being accepted. @@ -389,6 +456,14 @@ Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector). Dialog's type, could be one of the `alert`, `beforeunload`, `confirm` and `prompt`. ### class: Frame + +At every point of time, page exposes its current frame tree via the [page.mainFrame()](#pagemainframe) and [frame.childFrames()](#framechildframes) methods. + +[Frame] object's lifecycle is controlled by three events, dispatched on the page object: +- ['frameattached'](#event-frameattached) - fired when the frame gets attached to the page. Frame could be attached to the page only once. +- ['framenavigated'](#event-framenavigated) - fired when the frame commits navigation to a different URL. +- ['framedetached'](#event-framedetached) - fired when the frame gets detached from the page. Frame could be detached from the page only once. + #### frame.childFrames() - returns: <[Array]<[Frame]>> @@ -446,6 +521,15 @@ immediately. ### class: Request +Whenever the page sends a request, the following events are emitted by puppeteer's page: +- ['request'](#event-request) emitted when the request is issued by the page. +- ['response'](#event-response) emitted when/if the response is received for the request. +- ['requestfinished'](#event-requestfinished) emitted when the response body is downloaded and the request is complete. + +If request fails at some point, then instead of 'requestfinished' event (and possibly instead of 'response' event), the ['requestfailed'](#event-requestfailed) event is emitted. + +If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new request is issued to a redirected url. + [Request] class represents requests which are sent by page. [Request] implements [Body] mixin, which in case of HTTP POST requests allows clients to call `request.json()` or `request.text()` to get different representations of request's body. #### request.headers @@ -616,3 +700,4 @@ If there's already a header with name `name`, the header gets overwritten. [Request]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-request "Request" [Browser]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browser "Browser" [Body]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-body "Body" +[Dialog]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-dialog "Dialog" diff --git a/utils/doclint/Documentation.js b/utils/doclint/Documentation.js index 0e9f085c5bd..89900446a86 100644 --- a/utils/doclint/Documentation.js +++ b/utils/doclint/Documentation.js @@ -66,6 +66,14 @@ class Documentation { errors.push(`Non-existing property found: ${className}.${propertyName}`); for (let propertyName of propertyDiff.missing) errors.push(`Property not found: ${className}.${propertyName}`); + + const actualEvents = Array.from(actualClass.events.keys()).sort(); + const expectedEvents = Array.from(expectedClass.events.keys()).sort(); + const eventsDiff = diff(actualEvents, expectedEvents); + for (let eventName of eventsDiff.extra) + errors.push(`Non-existing event found in class ${className}: '${eventName}'`); + for (let eventName of eventsDiff.missing) + errors.push(`Event not found in class ${className}: '${eventName}'`); } return errors; } @@ -110,12 +118,15 @@ Documentation.Class = class { this.members = new Map(); this.properties = new Map(); this.methods = new Map(); + this.events = new Map(); for (let member of membersArray) { this.members.set(member.name, member); if (member.type === 'method') this.methods.set(member.name, member); else if (member.type === 'property') this.properties.set(member.name, member); + else if (member.type === 'event') + this.events.set(member.name, member); } } }; @@ -156,6 +167,14 @@ Documentation.Member = class { static createProperty(name) { return new Documentation.Member('property', name, [], false, false); } + + /** + * @param {string} name + * @return {!Documentation.Member} + */ + static createEvent(name) { + return new Documentation.Member('event', name, [], false, false); + } }; Documentation.Argument = class { diff --git a/utils/doclint/JSBuilder.js b/utils/doclint/JSBuilder.js index ced75b7621d..464725f02a0 100644 --- a/utils/doclint/JSBuilder.js +++ b/utils/doclint/JSBuilder.js @@ -7,6 +7,8 @@ const Documentation = require('./Documentation'); class JSOutline { constructor(text) { this.classes = []; + this.errors = []; + this._eventsByClassName = new Map(); this._currentClassName = null; this._currentClassMembers = []; @@ -17,9 +19,12 @@ class JSOutline { this._onClassDeclaration(node); else if (node.type === 'MethodDefinition') this._onMethodDefinition(node); + else if (node.type === 'AssignmentExpression') + this._onAssignmentExpression(node); }); walker.walk(ast); this._flushClassIfNeeded(); + this._recreateClassesWithEvents(); } _onClassDeclaration(node) { @@ -68,6 +73,24 @@ class JSOutline { return ESTreeWalker.SkipSubtree; } + _onAssignmentExpression(node) { + if (node.left.type !== 'MemberExpression' || node.right.type !== 'ObjectExpression') + return; + if (node.left.object.type !== 'Identifier' || node.left.property.type !== 'Identifier' || node.left.property.name !== 'Events') + return; + const className = node.left.object.name; + let events = this._eventsByClassName.get(className); + if (!events) { + events = []; + this._eventsByClassName.set(className, events); + } + for (let property of node.right.properties) { + if (property.type !== 'Property' || property.key.type !== 'Identifier' || property.value.type !== 'Literal') + continue; + events.push(Documentation.Member.createEvent(property.value.value)); + } + } + _flushClassIfNeeded() { if (this._currentClassName === null) return; @@ -77,6 +100,14 @@ class JSOutline { this._currentClassMembers = []; } + _recreateClassesWithEvents() { + this.classes = this.classes.map(cls => { + let events = this._eventsByClassName.get(cls.name) || []; + let members = cls.membersArray.concat(events); + return new Documentation.Class(cls.name, members); + }); + } + _extractText(node) { if (!node) return null; diff --git a/utils/doclint/MDBuilder.js b/utils/doclint/MDBuilder.js index 1eda3e9c69d..41887bd6110 100644 --- a/utils/doclint/MDBuilder.js +++ b/utils/doclint/MDBuilder.js @@ -55,6 +55,7 @@ class MDOutline { const constructorRegex = /^new (\w+)\((.*)\)$/; const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/; const propertyRegex = /^(\w+)\.(\w+)$/; + const eventRegex = /^event: '(\w+)'$/; let currentClassName = null; let currentClassMembers = []; for (const cls of classes) { @@ -72,6 +73,9 @@ class MDOutline { } else if (propertyRegex.test(member.name)) { let match = member.name.match(propertyRegex); handleProperty.call(this, member, match[1], match[2]); + } else if (eventRegex.test(member.name)) { + let match = member.name.match(eventRegex); + handleEvent.call(this, member, match[1]); } } flushClassIfNeeded.call(this); @@ -98,6 +102,14 @@ class MDOutline { currentClassMembers.push(Documentation.Member.createProperty(propertyName)); } + function handleEvent(member, eventName) { + if (!currentClassName || !eventName) { + this.errors.push(`Failed to process header as event: ${member.name}`); + return; + } + currentClassMembers.push(Documentation.Member.createEvent(eventName)); + } + function flushClassIfNeeded() { if (currentClassName === null) return; diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js index fb1208b415c..88bc56c60d1 100644 --- a/utils/doclint/lint.js +++ b/utils/doclint/lint.js @@ -61,20 +61,44 @@ async function lint(page, docsFolderPath, jsFolderPath) { */ function lintMarkdown(doc) { const errors = []; - // Methods should be sorted alphabetically. for (let cls of doc.classesArray) { - for (let i = 0; i < cls.membersArray.length - 1; ++i) { - // Constructor always goes first. - if (cls.membersArray[i].name === 'constructor') { - if (i > 0) - errors.push(`Constructor of ${cls.name} should go before other methods`); - continue; - } + let members = cls.membersArray; + + // Events should go first. + let eventIndex = 0; + for (; eventIndex < members.length && members[eventIndex].type === 'event'; ++eventIndex); + for (; eventIndex < members.length && members[eventIndex].type !== 'event'; ++eventIndex); + if (eventIndex < members.length) + errors.push(`Events should go first. Event '${members[eventIndex].name}' in class ${cls.name} breaks order`); + + // Constructor should be right after events and before all other members. + let constructorIndex = members.findIndex(member => member.type === 'method' && member.name === 'constructor'); + if (constructorIndex > 0 && members[constructorIndex - 1].type !== 'event') + errors.push(`Constructor of ${cls.name} should go before other methods`); + + // Events should be sorted alphabetically. + for (let i = 0; i < members.length - 1; ++i) { let member1 = cls.membersArray[i]; let member2 = cls.membersArray[i + 1]; + if (member1.type !== 'event' || member2.type !== 'event') + continue; + if (member1.name > member2.name) + errors.push(`Event '${member1.name}' in class ${cls.name} breaks alphabetic ordering of events`); + } + + // All other members should be sorted alphabetically. + for (let i = 0; i < members.length - 1; ++i) { + let member1 = cls.membersArray[i]; + let member2 = cls.membersArray[i + 1]; + if (member1.type === 'event' || member2.type === 'event') + continue; + if (member1.type === 'method' && member1.name === 'constructor') + continue; if (member1.name > member2.name) { - let memberName = `${cls.name}.${member1.name}` + (member1.type === 'method' ? '()' : ''); - errors.push(`${memberName} breaks alphabetic member sorting inside class ${cls.name}`); + let memberName = `${cls.name}.${member1.name}`; + if (member1.type === 'method') + memberName += '()'; + errors.push(`${memberName} breaks alphabetic ordering of class members.`); } } } diff --git a/utils/doclint/test/07-sorting/doc.md b/utils/doclint/test/07-sorting/doc.md index a38a941a147..2cc2ad57c3d 100644 --- a/utils/doclint/test/07-sorting/doc.md +++ b/utils/doclint/test/07-sorting/doc.md @@ -1,11 +1,17 @@ ### class: Foo +#### event: 'c' + +#### event: 'a' + #### foo.aaa() -#### new Foo() +#### event: 'b' #### foo.ddd +#### new Foo() + #### foo.ccc() #### foo.bbb() diff --git a/utils/doclint/test/07-sorting/foo.js b/utils/doclint/test/07-sorting/foo.js index cf442a87ca9..ff4ed3896d8 100644 --- a/utils/doclint/test/07-sorting/foo.js +++ b/utils/doclint/test/07-sorting/foo.js @@ -9,3 +9,9 @@ class Foo { ccc() {} } + +Foo.Events = { + a: 'a', + b: 'b', + c: 'c' +} diff --git a/utils/doclint/test/09-event-errors/doc.md b/utils/doclint/test/09-event-errors/doc.md new file mode 100644 index 00000000000..3da9c795113 --- /dev/null +++ b/utils/doclint/test/09-event-errors/doc.md @@ -0,0 +1,5 @@ +### class: Foo + +#### event: 'start' + +#### event: 'stop' diff --git a/utils/doclint/test/09-event-errors/foo.js b/utils/doclint/test/09-event-errors/foo.js new file mode 100644 index 00000000000..f9af75590d2 --- /dev/null +++ b/utils/doclint/test/09-event-errors/foo.js @@ -0,0 +1,7 @@ +class Foo { +} + +Foo.Events = { + Start: 'start', + Finish: 'finish', +}; diff --git a/utils/doclint/test/golden/07-sorting.txt b/utils/doclint/test/golden/07-sorting.txt index 9a41d6df2e1..339ef8ae343 100644 --- a/utils/doclint/test/golden/07-sorting.txt +++ b/utils/doclint/test/golden/07-sorting.txt @@ -1,3 +1,5 @@ +[MarkDown] Events should go first. Event 'b' in class Foo breaks order [MarkDown] Constructor of Foo should go before other methods -[MarkDown] Foo.ddd breaks alphabetic member sorting inside class Foo -[MarkDown] Foo.ccc() breaks alphabetic member sorting inside class Foo \ No newline at end of file +[MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events +[MarkDown] Foo.ddd breaks alphabetic ordering of class members. +[MarkDown] Foo.ccc() breaks alphabetic ordering of class members. \ No newline at end of file diff --git a/utils/doclint/test/golden/09-event-errors.txt b/utils/doclint/test/golden/09-event-errors.txt new file mode 100644 index 00000000000..8a18433aeda --- /dev/null +++ b/utils/doclint/test/golden/09-event-errors.txt @@ -0,0 +1,2 @@ +[MarkDown] Non-existing event found in class Foo: 'stop' +[MarkDown] Event not found in class Foo: 'finish' \ No newline at end of file diff --git a/utils/doclint/test/test.js b/utils/doclint/test/test.js index 606ab32683d..02a83bfc463 100644 --- a/utils/doclint/test/test.js +++ b/utils/doclint/test/test.js @@ -39,6 +39,7 @@ describe('doclint', function() { it('06-duplicates', SX(test)); it('07-sorting', SX(test)); it('08-return', SX(test)); + it('09-event-errors', SX(test)); }); async function test() {