feat: introduce puppeteer/Errors (#3056)

This patch adds a new require, `puppeteer/Errors`, that
holds all the Puppeteer-specific error classes.

Currently, the only custom error class we use is `TimeoutError`. We'll
expand in future with `CrashError` and some others.

Fixes #1694.
This commit is contained in:
Andrey Lushnikov 2018-08-09 16:51:12 -07:00 committed by GitHub
parent 231a2be971
commit 204c7ec8c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 178 additions and 38 deletions

28
Errors.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Copyright 2018 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.
*/
let asyncawait = true;
try {
new Function('async function test(){await 1}');
} catch (error) {
asyncawait = false;
}
// If node does not support async await, use the compiled version.
if (asyncawait)
module.exports = require('./lib/Errors');
else
module.exports = require('./node6/lib/Errors');

View File

@ -12,6 +12,7 @@ Next Release: **Aug 9, 2018**
<!-- GEN:toc -->
- [Overview](#overview)
- [Environment Variables](#environment-variables)
- [Error handling](#error-handling)
- [Working with Chrome Extensions](#working-with-chrome-extensions)
- [class: Puppeteer](#class-puppeteer)
* [puppeteer.connect(options)](#puppeteerconnectoptions)
@ -285,6 +286,7 @@ Next Release: **Aug 9, 2018**
* [coverage.startJSCoverage(options)](#coveragestartjscoverageoptions)
* [coverage.stopCSSCoverage()](#coveragestopcsscoverage)
* [coverage.stopJSCoverage()](#coveragestopjscoverage)
- [class: TimeoutError](#class-timeouterror)
<!-- GEN:stop -->
### Overview
@ -316,6 +318,34 @@ If puppeteer doesn't find them in environment, lowercased variant of these varia
- `PUPPETEER_DOWNLOAD_HOST` - overwrite host part of URL that is used to download Chromium
- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of chrome you'd like puppeteer to use during the installation step.
### Error handling
Often times, async methods might throw an error, signaling about their inability
to fulfill request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
might fail if selector doesn't appear during the given timeframe.
For certain types of errors Puppeteer uses specific error classes.
These classes are available through the `require('puppeteer/Errors')`.
List of supported classes:
- [`TimeoutError`](#class-timeouterror)
An example of handling timeout error:
```js
const {TimeoutError} = require('puppeteer/Errors');
// ...
try {
await page.waitForSelector('.foo');
} catch (e) {
if (e instanceof TimeoutError) {
// Do something if this is a timeout.
}
}
```
### Working with Chrome Extensions
Puppeteer can be used for testing Chrome Extensions.
@ -3157,6 +3187,14 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb
> **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are
reported.
### class: TimeoutError
* extends: [Error]
TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) or [puppeteer.launch([options])](#puppeteerlaunchoptions).
[Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array"
[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean"
[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer"

View File

@ -156,7 +156,7 @@ class BrowserFetcher {
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
else
throw 'Unsupported platform: ' + this._platform;
throw new Error('Unsupported platform: ' + this._platform);
let url = downloadURLs[this._platform];
url = util.format(url, this._downloadHost, revision);
const local = fs.existsSync(folderPath);

29
lib/Errors.js Normal file
View File

@ -0,0 +1,29 @@
/**
* Copyright 2018 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.
*/
class CustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class TimeoutError extends CustomError {}
module.exports = {
TimeoutError,
};

View File

@ -19,6 +19,7 @@ const EventEmitter = require('events');
const {helper, assert} = require('./helper');
const {ExecutionContext, JSHandle} = require('./ExecutionContext');
const {ElementHandle} = require('./ElementHandle');
const {TimeoutError} = require('./Errors');
const readFileAsync = helper.promisify(fs.readFile);
@ -876,7 +877,7 @@ class WaitTask {
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
if (timeout) {
const timeoutError = new Error(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
}
this.rerun();

View File

@ -24,6 +24,7 @@ const readline = require('readline');
const fs = require('fs');
const {helper, assert, debugError} = require('./helper');
const ChromiumRevision = require(path.join(helper.projectRoot(), 'package.json')).puppeteer.chromium_revision;
const {TimeoutError} = require('./Errors');
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const removeFolderAsync = helper.promisify(removeFolder);
@ -306,7 +307,7 @@ function waitForWSEndpoint(chromeProcess, timeout) {
function onTimeout() {
cleanup();
reject(new Error(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${ChromiumRevision}`));
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${ChromiumRevision}`));
}
/**

View File

@ -16,6 +16,7 @@
const {helper, assert} = require('./helper');
const {FrameManager} = require('./FrameManager');
const {TimeoutError} = require('./Errors');
class NavigatorWatcher {
/**
@ -70,7 +71,7 @@ class NavigatorWatcher {
return new Promise(() => {});
const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded';
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
.then(() => new Error(errorMessage));
.then(() => new TimeoutError(errorMessage));
}
/**

View File

@ -15,6 +15,7 @@
*/
const fs = require('fs');
const path = require('path');
const {TimeoutError} = require('./Errors');
const debugError = require('debug')(`puppeteer:error`);
/** @type {?Map<string, boolean>} */
@ -262,7 +263,7 @@ class Helper {
if (timeout) {
eventTimeout = setTimeout(() => {
cleanup();
rejectCallback(new Error('Timeout exceeded while waiting for event'));
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
}, timeout);
}
function cleanup() {

View File

@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const utils = require('./utils');
const puppeteer = utils.requireRoot('index');
module.exports.addTests = function({testRunner, expect, puppeteer, headless}) {
module.exports.addTests = function({testRunner, expect, headless}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

View File

@ -15,8 +15,9 @@
*/
const utils = require('./utils');
const puppeteer = utils.requireRoot('index');
module.exports.addTests = function({testRunner, expect, puppeteer}) {
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

View File

@ -15,6 +15,7 @@
*/
const utils = require('./utils');
const {TimeoutError} = utils.requireRoot('Errors');
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
@ -165,6 +166,7 @@ module.exports.addTests = function({testRunner, expect}) {
await page.waitForFunction('false', {timeout: 10}).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for function failed: timeout');
expect(error).toBeInstanceOf(TimeoutError);
});
it('should disable timeout when its set to 0', async({page}) => {
const watchdog = page.waitForFunction(() => {
@ -317,6 +319,7 @@ module.exports.addTests = function({testRunner, expect}) {
await page.waitForSelector('div', {timeout: 10}).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "div" failed: timeout');
expect(error).toBeInstanceOf(TimeoutError);
});
it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => {
await page.setContent(`<div></div>`);
@ -359,6 +362,7 @@ module.exports.addTests = function({testRunner, expect}) {
await page.waitForXPath('//div', {timeout: 10}).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for XPath "//div" failed: timeout');
expect(error).toBeInstanceOf(TimeoutError);
});
it('should run in specified frame', async({page, server}) => {
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);

View File

@ -16,7 +16,9 @@
const path = require('path');
const os = require('os');
const {waitEvent} = require('./utils.js');
const utils = require('./utils');
const {waitEvent} = utils;
const puppeteer = utils.requireRoot('index.js');
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
@ -34,11 +36,10 @@ function waitForBackgroundPageTarget(browser) {
});
}
module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions}) {
module.exports.addTests = function({testRunner, expect, defaultBrowserOptions}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const puppeteer = require(PROJECT_ROOT);
const headfulOptions = Object.assign({}, defaultBrowserOptions, {
headless: false

View File

@ -14,11 +14,13 @@
* limitations under the License.
*/
module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions}) {
const utils = require('./utils');
const puppeteer = utils.requireRoot('index.js');
module.exports.addTests = function({testRunner, expect, defaultBrowserOptions}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const puppeteer = require(PROJECT_ROOT);
describe('ignoreHTTPSErrors', function() {
beforeAll(async state => {
const options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);

View File

@ -16,12 +16,13 @@
const path = require('path');
const utils = require('./utils');
const DeviceDescriptors = utils.requireRoot('DeviceDescriptors');
const iPhone = DeviceDescriptors['iPhone 6'];
module.exports.addTests = function({testRunner, expect, DeviceDescriptors}) {
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const iPhone = DeviceDescriptors['iPhone 6'];
describe('input', function() {
it('should click the button', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');

View File

@ -16,14 +16,17 @@
const fs = require('fs');
const path = require('path');
const utils = require('./utils');
const {waitEvent} = require('./utils');
const {waitEvent} = utils;
const {TimeoutError} = utils.requireRoot('Errors');
module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescriptors, headless}) {
const DeviceDescriptors = utils.requireRoot('DeviceDescriptors');
const iPhone = DeviceDescriptors['iPhone 6'];
const iPhoneLandscape = DeviceDescriptors['iPhone 6 landscape'];
module.exports.addTests = function({testRunner, expect, headless}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const iPhone = DeviceDescriptors['iPhone 6'];
const iPhoneLandscape = DeviceDescriptors['iPhone 6 landscape'];
describe('Page.close', function() {
it('should reject all promises when page is closed', async({context}) => {
@ -531,6 +534,7 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
let error = null;
await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e);
expect(error.message).toContain('Navigation Timeout Exceeded: 1ms');
expect(error).toBeInstanceOf(TimeoutError);
});
it('should fail when exceeding default maximum navigation timeout', async({page, server}) => {
// Hang for request to the empty.html
@ -539,6 +543,7 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
expect(error.message).toContain('Navigation Timeout Exceeded: 1ms');
expect(error).toBeInstanceOf(TimeoutError);
});
it('should disable timeout when its set to 0', async({page, server}) => {
let error = null;

View File

@ -23,12 +23,12 @@ const readFileAsync = helper.promisify(fs.readFile);
const statAsync = helper.promisify(fs.stat);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const utils = require('./utils');
const puppeteer = utils.requireRoot('index');
module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions}) {
module.exports.addTests = function({testRunner, expect, defaultBrowserOptions}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const puppeteer = require(PROJECT_ROOT);
describe('Puppeteer', function() {
describe('BrowserFetcher', function() {
@ -145,7 +145,7 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro
const {spawn} = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
const res = spawn('node',
[path.join(__dirname, 'fixtures', 'dumpio.js'), PROJECT_ROOT, JSON.stringify(options), server.EMPTY_PAGE, dumpioTextToLog]);
[path.join(__dirname, 'fixtures', 'dumpio.js'), utils.projectRoot(), JSON.stringify(options), server.EMPTY_PAGE, dumpioTextToLog]);
res.stderr.on('data', data => dumpioData += data.toString('utf8'));
await new Promise(resolve => res.on('close', resolve));
@ -153,7 +153,7 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro
});
it('should close the browser when the node process closes', async({ server }) => {
const {spawn, execSync} = require('child_process');
const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), PROJECT_ROOT, JSON.stringify(defaultBrowserOptions)]);
const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), utils.projectRoot(), JSON.stringify(defaultBrowserOptions)]);
let wsEndPointCallback;
const wsEndPointPromise = new Promise(x => wsEndPointCallback = x);
let output = '';

View File

@ -14,9 +14,10 @@
* limitations under the License.
*/
const {waitEvent} = require('./utils');
const utils = require('./utils');
const {waitEvent} = utils;
module.exports.addTests = function({testRunner, expect, puppeteer}) {
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

View File

@ -21,14 +21,13 @@ const GoldenUtils = require('./golden-utils');
const GOLDEN_DIR = path.join(__dirname, 'golden');
const OUTPUT_DIR = path.join(__dirname, 'output');
const {TestRunner, Reporter, Matchers} = require('../utils/testrunner/');
const utils = require('./utils');
const {helper, assert} = require('../lib/helper');
if (process.env.COVERAGE)
helper.recordPublicAPICoverage();
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
const puppeteer = require(PROJECT_ROOT);
const DeviceDescriptors = require(path.join(PROJECT_ROOT, 'DeviceDescriptors'));
const puppeteer = utils.requireRoot('index');
const YELLOW_COLOR = '\x1b[33m';
const RESET_COLOR = '\x1b[0m';
@ -145,28 +144,28 @@ describe('Browser', function() {
// Page-level tests that are given a browser, a context and a page.
// Each test is launched in a new browser context.
require('./CDPSession.spec.js').addTests({testRunner, expect});
require('./browser.spec.js').addTests({testRunner, expect, puppeteer, headless});
require('./browser.spec.js').addTests({testRunner, expect, headless});
require('./cookies.spec.js').addTests({testRunner, expect});
require('./coverage.spec.js').addTests({testRunner, expect});
require('./elementhandle.spec.js').addTests({testRunner, expect});
require('./frame.spec.js').addTests({testRunner, expect});
require('./input.spec.js').addTests({testRunner, expect, DeviceDescriptors});
require('./input.spec.js').addTests({testRunner, expect});
require('./jshandle.spec.js').addTests({testRunner, expect});
require('./network.spec.js').addTests({testRunner, expect});
require('./page.spec.js').addTests({testRunner, expect, puppeteer, DeviceDescriptors, headless});
require('./target.spec.js').addTests({testRunner, expect, puppeteer});
require('./page.spec.js').addTests({testRunner, expect, headless});
require('./target.spec.js').addTests({testRunner, expect});
require('./tracing.spec.js').addTests({testRunner, expect});
require('./worker.spec.js').addTests({testRunner, expect});
});
// Browser-level tests that are given a browser.
require('./browsercontext.spec.js').addTests({testRunner, expect, puppeteer});
require('./browsercontext.spec.js').addTests({testRunner, expect});
});
// Top-level tests that launch Browser themselves.
require('./ignorehttpserrors.spec.js').addTests({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions});
require('./puppeteer.spec.js').addTests({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions});
require('./headful.spec.js').addTests({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions});
require('./ignorehttpserrors.spec.js').addTests({testRunner, expect, defaultBrowserOptions});
require('./puppeteer.spec.js').addTests({testRunner, expect, defaultBrowserOptions});
require('./headful.spec.js').addTests({testRunner, expect, defaultBrowserOptions});
if (process.env.COVERAGE) {
describe('COVERAGE', function() {

View File

@ -14,7 +14,25 @@
* limitations under the License.
*/
const fs = require('fs');
const path = require('path');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
const utils = module.exports = {
/**
* @return {string}
*/
projectRoot: function() {
return PROJECT_ROOT;
},
/**
* @return {*}
*/
requireRoot: function(name) {
return require(path.join(PROJECT_ROOT, name));
},
/**
* @param {!Page} page
* @param {string} frameId

View File

@ -22,6 +22,7 @@ const Message = require('../Message');
const EXCLUDE_CLASSES = new Set([
'CSSCoverage',
'Connection',
'CustomError',
'EmulationManager',
'FrameManager',
'JSCoverage',
@ -35,11 +36,12 @@ const EXCLUDE_CLASSES = new Set([
'WaitTask',
]);
const EXCLUDE_METHODS = new Set([
const EXCLUDE_PROPERTIES = new Set([
'Browser.create',
'Headers.fromPayload',
'Page.create',
'JSHandle.toString',
'TimeoutError.name',
]);
/**
@ -137,7 +139,7 @@ function filterJSDocumentation(jsDocumentation) {
// Exclude all constructors by default.
if (member.name === 'constructor' && member.type === 'method')
return false;
return !EXCLUDE_METHODS.has(`${cls.name}.${member.name}`);
return !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`);
});
classes.push(new Documentation.Class(cls.name, members));
}

View File

@ -108,7 +108,12 @@ const DefaultMatchers = {
pass: Math.abs(value - other) < Math.pow(10, -precision),
message
};
}
},
toBeInstanceOf: function(value, other, message) {
message = message || `${value.constructor.name} instanceof ${other.name}`;
return { pass: value instanceof other, message };
},
};
function stringify(value) {