[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:
parent
ac75455983
commit
6eac22dd87
85
docs/api.md
85
docs/api.md
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -9,3 +9,9 @@ class Foo {
|
|||||||
|
|
||||||
ccc() {}
|
ccc() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Foo.Events = {
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
c: 'c'
|
||||||
|
}
|
||||||
|
5
utils/doclint/test/09-event-errors/doc.md
Normal file
5
utils/doclint/test/09-event-errors/doc.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
### class: Foo
|
||||||
|
|
||||||
|
#### event: 'start'
|
||||||
|
|
||||||
|
#### event: 'stop'
|
7
utils/doclint/test/09-event-errors/foo.js
Normal file
7
utils/doclint/test/09-event-errors/foo.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class Foo {
|
||||||
|
}
|
||||||
|
|
||||||
|
Foo.Events = {
|
||||||
|
Start: 'start',
|
||||||
|
Finish: 'finish',
|
||||||
|
};
|
@ -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.
|
2
utils/doclint/test/golden/09-event-errors.txt
Normal file
2
utils/doclint/test/golden/09-event-errors.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[MarkDown] Non-existing event found in class Foo: 'stop'
|
||||||
|
[MarkDown] Event not found in class Foo: 'finish'
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user