[doclint] lint events

This patch:
- adds event linting to the doclint
- improves `api.md` to add events and more information about
  classes reported by events.

References #14.
This commit is contained in:
Andrey Lushnikov 2017-07-14 13:03:21 -07:00
parent ac75455983
commit 6eac22dd87
12 changed files with 213 additions and 13 deletions

View File

@ -13,6 +13,17 @@
* [browser.stdout](#browserstdout) * [browser.stdout](#browserstdout)
* [browser.version()](#browserversion) * [browser.version()](#browserversion)
- [class: Page](#class-page) - [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.addScriptTag(url)](#pageaddscripttagurl)
* [page.click(selector)](#pageclickselector) * [page.click(selector)](#pageclickselector)
* [page.close()](#pageclose) * [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) #### page.addScriptTag(url)
- `url` <[string]> Url of a script to be added - `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). Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
### class: Dialog ### class: Dialog
[Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event.
#### dialog.accept([promptText]) #### dialog.accept([promptText])
- `promptText` <[string]> A text to enter in prompt. Does not cause any effects if the dialog's `type` is not prompt. - `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. - 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`. Dialog's type, could be one of the `alert`, `beforeunload`, `confirm` and `prompt`.
### class: Frame ### 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() #### frame.childFrames()
- returns: <[Array]<[Frame]>> - returns: <[Array]<[Frame]>>
@ -446,6 +521,15 @@ immediately.
### class: Request ### 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] 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 #### 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" [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" [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" [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"

View File

@ -66,6 +66,14 @@ class Documentation {
errors.push(`Non-existing property found: ${className}.${propertyName}`); errors.push(`Non-existing property found: ${className}.${propertyName}`);
for (let propertyName of propertyDiff.missing) for (let propertyName of propertyDiff.missing)
errors.push(`Property not found: ${className}.${propertyName}`); 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; return errors;
} }
@ -110,12 +118,15 @@ Documentation.Class = class {
this.members = new Map(); this.members = new Map();
this.properties = new Map(); this.properties = new Map();
this.methods = new Map(); this.methods = new Map();
this.events = new Map();
for (let member of membersArray) { for (let member of membersArray) {
this.members.set(member.name, member); this.members.set(member.name, member);
if (member.type === 'method') if (member.type === 'method')
this.methods.set(member.name, member); this.methods.set(member.name, member);
else if (member.type === 'property') else if (member.type === 'property')
this.properties.set(member.name, member); 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) { static createProperty(name) {
return new Documentation.Member('property', name, [], false, false); 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 { Documentation.Argument = class {

View File

@ -7,6 +7,8 @@ const Documentation = require('./Documentation');
class JSOutline { class JSOutline {
constructor(text) { constructor(text) {
this.classes = []; this.classes = [];
this.errors = [];
this._eventsByClassName = new Map();
this._currentClassName = null; this._currentClassName = null;
this._currentClassMembers = []; this._currentClassMembers = [];
@ -17,9 +19,12 @@ class JSOutline {
this._onClassDeclaration(node); this._onClassDeclaration(node);
else if (node.type === 'MethodDefinition') else if (node.type === 'MethodDefinition')
this._onMethodDefinition(node); this._onMethodDefinition(node);
else if (node.type === 'AssignmentExpression')
this._onAssignmentExpression(node);
}); });
walker.walk(ast); walker.walk(ast);
this._flushClassIfNeeded(); this._flushClassIfNeeded();
this._recreateClassesWithEvents();
} }
_onClassDeclaration(node) { _onClassDeclaration(node) {
@ -68,6 +73,24 @@ class JSOutline {
return ESTreeWalker.SkipSubtree; 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() { _flushClassIfNeeded() {
if (this._currentClassName === null) if (this._currentClassName === null)
return; return;
@ -77,6 +100,14 @@ class JSOutline {
this._currentClassMembers = []; 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) { _extractText(node) {
if (!node) if (!node)
return null; return null;

View File

@ -55,6 +55,7 @@ class MDOutline {
const constructorRegex = /^new (\w+)\((.*)\)$/; const constructorRegex = /^new (\w+)\((.*)\)$/;
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/; const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/;
const propertyRegex = /^(\w+)\.(\w+)$/; const propertyRegex = /^(\w+)\.(\w+)$/;
const eventRegex = /^event: '(\w+)'$/;
let currentClassName = null; let currentClassName = null;
let currentClassMembers = []; let currentClassMembers = [];
for (const cls of classes) { for (const cls of classes) {
@ -72,6 +73,9 @@ class MDOutline {
} else if (propertyRegex.test(member.name)) { } else if (propertyRegex.test(member.name)) {
let match = member.name.match(propertyRegex); let match = member.name.match(propertyRegex);
handleProperty.call(this, member, match[1], match[2]); 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); flushClassIfNeeded.call(this);
@ -98,6 +102,14 @@ class MDOutline {
currentClassMembers.push(Documentation.Member.createProperty(propertyName)); 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() { function flushClassIfNeeded() {
if (currentClassName === null) if (currentClassName === null)
return; return;

View File

@ -61,20 +61,44 @@ async function lint(page, docsFolderPath, jsFolderPath) {
*/ */
function lintMarkdown(doc) { function lintMarkdown(doc) {
const errors = []; const errors = [];
// Methods should be sorted alphabetically.
for (let cls of doc.classesArray) { for (let cls of doc.classesArray) {
for (let i = 0; i < cls.membersArray.length - 1; ++i) { let members = cls.membersArray;
// Constructor always goes first.
if (cls.membersArray[i].name === 'constructor') { // Events should go first.
if (i > 0) let eventIndex = 0;
errors.push(`Constructor of ${cls.name} should go before other methods`); for (; eventIndex < members.length && members[eventIndex].type === 'event'; ++eventIndex);
continue; 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 member1 = cls.membersArray[i];
let member2 = cls.membersArray[i + 1]; 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) { if (member1.name > member2.name) {
let memberName = `${cls.name}.${member1.name}` + (member1.type === 'method' ? '()' : ''); let memberName = `${cls.name}.${member1.name}`;
errors.push(`${memberName} breaks alphabetic member sorting inside class ${cls.name}`); if (member1.type === 'method')
memberName += '()';
errors.push(`${memberName} breaks alphabetic ordering of class members.`);
} }
} }
} }

View File

@ -1,11 +1,17 @@
### class: Foo ### class: Foo
#### event: 'c'
#### event: 'a'
#### foo.aaa() #### foo.aaa()
#### new Foo() #### event: 'b'
#### foo.ddd #### foo.ddd
#### new Foo()
#### foo.ccc() #### foo.ccc()
#### foo.bbb() #### foo.bbb()

View File

@ -9,3 +9,9 @@ class Foo {
ccc() {} ccc() {}
} }
Foo.Events = {
a: 'a',
b: 'b',
c: 'c'
}

View File

@ -0,0 +1,5 @@
### class: Foo
#### event: 'start'
#### event: 'stop'

View File

@ -0,0 +1,7 @@
class Foo {
}
Foo.Events = {
Start: 'start',
Finish: 'finish',
};

View File

@ -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] Constructor of Foo should go before other methods
[MarkDown] Foo.ddd breaks alphabetic member sorting inside class Foo [MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events
[MarkDown] Foo.ccc() breaks alphabetic member sorting inside class Foo [MarkDown] Foo.ddd breaks alphabetic ordering of class members.
[MarkDown] Foo.ccc() breaks alphabetic ordering of class members.

View File

@ -0,0 +1,2 @@
[MarkDown] Non-existing event found in class Foo: 'stop'
[MarkDown] Event not found in class Foo: 'finish'

View File

@ -39,6 +39,7 @@ describe('doclint', function() {
it('06-duplicates', SX(test)); it('06-duplicates', SX(test));
it('07-sorting', SX(test)); it('07-sorting', SX(test));
it('08-return', SX(test)); it('08-return', SX(test));
it('09-event-errors', SX(test));
}); });
async function test() { async function test() {