chore: introduce //lib/api.js (#3835)

Introduce `//lib/api.js` that declares a list of publicly exposed
classes.

The `//lib/api.js` list superceedes dynamic `helper.tracePublicAPI()` calls
and is used in the following places:
- [ASYNC STACKS]: generate "async stacks" for publicy exposed API in `//index.js`
- [COVERAGE]: move coverage support from `//lib/helper` to `//test/utils`
- [DOCLINT]: get rid of 'exluded classes' hardcoded list

This will help us to re-use our coverage and doclint infrastructure
for Puppeteer-Firefox.

Drive-By: it turns out we didn't run coverage for `SecurityDetails`
class, so we lack coverage for a few methods there. These are excluded
for now, sanity tests will be added in a follow-up.
This commit is contained in:
Andrey Lushnikov 2019-01-25 23:21:14 -05:00 committed by GitHub
parent cd678fb591
commit 62da2366c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 139 additions and 128 deletions

View File

@ -21,6 +21,16 @@ try {
asyncawait = false;
}
if (asyncawait) {
const {helper} = require('./lib/helper');
const api = require('./lib/api');
for (const className in api) {
// Puppeteer-web excludes certain classes from bundle, e.g. BrowserFetcher.
if (typeof api[className] === 'function')
helper.installAsyncStackHooks(api[className]);
}
}
// If node does not support async await, use the compiled version.
const Puppeteer = asyncawait ? require('./lib/Puppeteer') : require('./node6/lib/Puppeteer');
const packageJson = require('./package.json');

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper} = require('./helper');
/**
* @typedef {Object} SerializedAXNode
@ -390,4 +389,3 @@ class AXNode {
}
module.exports = {Accessibility};
helper.tracePublicAPI(Accessibility);

View File

@ -373,7 +373,4 @@ class BrowserContext extends EventEmitter {
}
}
helper.tracePublicAPI(BrowserContext);
helper.tracePublicAPI(Browser);
module.exports = {Browser, BrowserContext};

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper, assert} = require('./helper');
const {assert} = require('./helper');
const {Events} = require('./Events');
const debugProtocol = require('debug')('puppeteer:protocol');
const EventEmitter = require('events');
@ -216,8 +216,6 @@ class CDPSession extends EventEmitter {
}
}
helper.tracePublicAPI(CDPSession);
/**
* @param {!Error} error
* @param {string} method

View File

@ -64,7 +64,6 @@ class Coverage {
}
module.exports = {Coverage};
helper.tracePublicAPI(Coverage);
class JSCoverage {
/**

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
const {helper, assert} = require('./helper');
const {assert} = require('./helper');
class Dialog {
/**
@ -81,4 +81,3 @@ Dialog.Type = {
};
module.exports = {Dialog};
helper.tracePublicAPI(Dialog);

View File

@ -175,6 +175,4 @@ class ExecutionContext {
}
}
helper.tracePublicAPI(ExecutionContext);
module.exports = {ExecutionContext, EVALUATION_SCRIPT_URL};

View File

@ -680,7 +680,6 @@ class Frame {
this._parentFrame = null;
}
}
helper.tracePublicAPI(Frame);
function assertNoLegacyNavigationOptions(options) {
assert(options['networkIdleTimeout'] === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
const {helper, assert} = require('./helper');
const {assert} = require('./helper');
const keyDefinitions = require('./USKeyboardLayout');
/**
@ -302,6 +302,3 @@ class Touchscreen {
}
module.exports = { Keyboard, Mouse, Touchscreen};
helper.tracePublicAPI(Keyboard);
helper.tracePublicAPI(Mouse);
helper.tracePublicAPI(Touchscreen);

View File

@ -122,8 +122,6 @@ class JSHandle {
}
}
helper.tracePublicAPI(JSHandle);
class ElementHandle extends JSHandle {
/**
* @param {!Puppeteer.ExecutionContext} context
@ -507,5 +505,4 @@ function computeQuadArea(quad) {
* @property {number} height
*/
helper.tracePublicAPI(ElementHandle);
module.exports = {createJSHandle, JSHandle, ElementHandle};

View File

@ -507,8 +507,6 @@ const errorReasons = {
'failed': 'Failed',
};
helper.tracePublicAPI(Request);
class Response {
/**
* @param {!Puppeteer.CDPSession} client
@ -649,7 +647,6 @@ class Response {
return this._request.frame();
}
}
helper.tracePublicAPI(Response);
const IGNORED_HEADERS = new Set(['accept', 'referer', 'x-devtools-emulate-network-conditions-client-id', 'cookie', 'origin', 'content-type', 'intervention']);
@ -798,4 +795,4 @@ const statusTexts = {
'511': 'Network Authentication Required',
};
module.exports = {Request, Response, NetworkManager};
module.exports = {Request, Response, NetworkManager, SecurityDetails};

View File

@ -1270,5 +1270,4 @@ class ConsoleMessage {
}
module.exports = {Page};
helper.tracePublicAPI(Page);
module.exports = {Page, ConsoleMessage};

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper} = require('./helper');
const Launcher = require('./Launcher');
const BrowserFetcher = require('./BrowserFetcher');
@ -68,4 +67,3 @@ module.exports = class {
}
};
helper.tracePublicAPI(module.exports, 'Puppeteer');

View File

@ -1,6 +1,21 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {Events} = require('./Events');
const {Page} = require('./Page');
const {helper} = require('./helper');
class Target {
/**
@ -113,6 +128,4 @@ class Target {
}
}
helper.tracePublicAPI(Target);
module.exports = {Target};

View File

@ -101,6 +101,5 @@ class Tracing {
}
}
}
helper.tracePublicAPI(Tracing);
module.exports = Tracing;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
const EventEmitter = require('events');
const {helper, debugError} = require('./helper');
const {debugError} = require('./helper');
const {ExecutionContext} = require('./ExecutionContext');
const {JSHandle} = require('./JSHandle');
@ -78,4 +78,3 @@ class Worker extends EventEmitter {
}
module.exports = {Worker};
helper.tracePublicAPI(Worker);

42
lib/api.js Normal file
View File

@ -0,0 +1,42 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = {
Accessibility: require('./Accessibility').Accessibility,
Browser: require('./Browser').Browser,
BrowserContext: require('./Browser').BrowserContext,
BrowserFetcher: require('./BrowserFetcher'),
CDPSession: require('./Connection').CDPSession,
ConsoleMessage: require('./Page').ConsoleMessage,
Coverage: require('./Coverage').Coverage,
Dialog: require('./Dialog').Dialog,
ElementHandle: require('./JSHandle').ElementHandle,
ExecutionContext: require('./ExecutionContext').ExecutionContext,
Frame: require('./FrameManager').Frame,
JSHandle: require('./JSHandle').JSHandle,
Keyboard: require('./Input').Keyboard,
Mouse: require('./Input').Mouse,
Page: require('./Page').Page,
Puppeteer: require('./Puppeteer'),
Request: require('./NetworkManager').Request,
Response: require('./NetworkManager').Response,
SecurityDetails: require('./NetworkManager').SecurityDetails,
Target: require('./Target').Target,
TimeoutError: require('./Errors').TimeoutError,
Touchscreen: require('./Input').Touchscreen,
Tracing: require('./Tracing'),
Worker: require('./Worker').Worker,
};

View File

@ -16,41 +16,6 @@
const {TimeoutError} = require('./Errors');
const debugError = require('debug')(`puppeteer:error`);
/** @type {?Map<string, boolean>} */
let apiCoverage = null;
/**
* @param {!Object} classType
* @param {string=} publicName
*/
function traceAPICoverage(classType, publicName) {
if (!apiCoverage)
return;
let className = publicName || classType.prototype.constructor.name;
className = className.substring(0, 1).toLowerCase() + className.substring(1);
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
continue;
apiCoverage.set(`${className}.${methodName}`, false);
Reflect.set(classType.prototype, methodName, function(...args) {
apiCoverage.set(`${className}.${methodName}`, true);
return method.call(this, ...args);
});
}
if (classType.Events) {
for (const event of Object.values(classType.Events))
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
const method = Reflect.get(classType.prototype, 'emit');
Reflect.set(classType.prototype, 'emit', function(event, ...args) {
if (this.listenerCount(event))
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
return method.call(this, event, ...args);
});
}
}
class Helper {
/**
@ -133,9 +98,8 @@ class Helper {
/**
* @param {!Object} classType
* @param {string=} publicName
*/
static tracePublicAPI(classType, publicName) {
static installAsyncStackHooks(classType) {
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction')
@ -151,8 +115,6 @@ class Helper {
});
});
}
traceAPICoverage(classType, publicName);
}
/**
@ -175,17 +137,6 @@ class Helper {
listeners.splice(0, listeners.length);
}
/**
* @return {?Map<string, boolean>}
*/
static publicAPICoverage() {
return apiCoverage;
}
static recordPublicAPICoverage() {
apiCoverage = new Map();
}
/**
* @param {!Object} obj
* @return {boolean}

View File

@ -75,32 +75,24 @@ beforeEach(async({server, httpsServer}) => {
httpsServer.reset();
});
describe('Chromium', () => {
const {helper} = require('../lib/helper');
if (process.env.COVERAGE)
helper.recordPublicAPICoverage();
const CHROMIUM_NO_COVERAGE = new Set([
'page.bringToFront',
'securityDetails.subjectName',
'securityDetails.issuer',
'securityDetails.validFrom',
'securityDetails.validTo',
...(headless ? [] : ['page.pdf']),
]);
describe('Chromium', () => {
require('./puppeteer.spec.js').addTests({
product: 'Chromium',
puppeteer: utils.requireRoot('index'),
defaultBrowserOptions,
testRunner,
});
if (process.env.COVERAGE) {
describe('COVERAGE', function() {
const coverage = helper.publicAPICoverage();
const disabled = new Set(['page.bringToFront']);
if (!headless)
disabled.add('page.pdf');
for (const method of coverage.keys()) {
(disabled.has(method) ? xit : it)(`public api '${method}' should be called`, async({page, server}) => {
if (!coverage.get(method))
throw new Error('NOT CALLED!');
});
}
});
}
if (process.env.COVERAGE)
utils.recordAPICoverage(testRunner, require('../lib/api'), CHROMIUM_NO_COVERAGE);
});
if (process.env.CI && testRunner.hasFocusedTestsOrSuites()) {

View File

@ -18,7 +18,51 @@ const fs = require('fs');
const path = require('path');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
/**
* @param {Map<string, boolean>} apiCoverage
* @param {string} className
* @param {!Object} classType
*/
function traceAPICoverage(apiCoverage, className, classType) {
className = className.substring(0, 1).toLowerCase() + className.substring(1);
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
continue;
apiCoverage.set(`${className}.${methodName}`, false);
Reflect.set(classType.prototype, methodName, function(...args) {
apiCoverage.set(`${className}.${methodName}`, true);
return method.call(this, ...args);
});
}
if (classType.Events) {
for (const event of Object.values(classType.Events))
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
const method = Reflect.get(classType.prototype, 'emit');
Reflect.set(classType.prototype, 'emit', function(event, ...args) {
if (this.listenerCount(event))
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
return method.call(this, event, ...args);
});
}
}
const utils = module.exports = {
recordAPICoverage: function(testRunner, api, disabled) {
const coverage = new Map();
for (const [className, classType] of Object.entries(api))
traceAPICoverage(coverage, className, classType);
testRunner.describe('COVERAGE', () => {
for (const method of coverage.keys()) {
(disabled.has(method) ? testRunner.xit : testRunner.it)(`public api '${method}' should be called`, async({page, server}) => {
if (!coverage.get(method))
throw new Error('NOT CALLED!');
});
}
});
},
/**
* @return {string}
*/

View File

@ -19,26 +19,6 @@ const mdBuilder = require('./MDBuilder');
const Documentation = require('./Documentation');
const Message = require('../Message');
const EXCLUDE_CLASSES = new Set([
'AXNode',
'CSSCoverage',
'Connection',
'CustomError',
'DOMWorld',
'EmulationManager',
'FrameManager',
'JSCoverage',
'Helper',
'Launcher',
'Multimap',
'LifecycleWatcher',
'NetworkManager',
'PipeTransport',
'TaskQueue',
'WaitTask',
'WebSocketTransport',
]);
const EXCLUDE_PROPERTIES = new Set([
'Browser.create',
'Headers.fromPayload',
@ -55,7 +35,7 @@ const EXCLUDE_PROPERTIES = new Set([
module.exports = async function lint(page, mdSources, jsSources) {
const mdResult = await mdBuilder(page, mdSources);
const jsResult = await jsBuilder(jsSources);
const jsDocumentation = filterJSDocumentation(jsResult.documentation);
const jsDocumentation = filterJSDocumentation(jsSources, jsResult.documentation);
const mdDocumentation = mdResult.documentation;
const jsErrors = jsResult.errors;
@ -126,14 +106,19 @@ function checkSorting(doc) {
}
/**
* @param {!Array<!Source>} jsSources
* @param {!Documentation} jsDocumentation
* @return {!Documentation}
*/
function filterJSDocumentation(jsDocumentation) {
// Filter classes and methods.
function filterJSDocumentation(jsSources, jsDocumentation) {
const apijs = jsSources.find(source => source.name() === 'api.js');
let includedClasses = null;
if (apijs)
includedClasses = new Set(Object.keys(require(apijs.filePath())));
// Filter private classes and methods.
const classes = [];
for (const cls of jsDocumentation.classesArray) {
if (EXCLUDE_CLASSES.has(cls.name))
if (includedClasses && !includedClasses.has(cls.name))
continue;
const members = cls.membersArray.filter(member => !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`));
classes.push(new Documentation.Class(cls.name, members));