Skip to content

Commit

Permalink
feat: introduce puppeteer/Errors (puppeteer#3056)
Browse files Browse the repository at this point in the history
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 puppeteer#1694.
  • Loading branch information
aslushnikov authored Aug 9, 2018
1 parent 231a2be commit 204c7ec
Show file tree
Hide file tree
Showing 21 changed files with 178 additions and 38 deletions.
28 changes: 28 additions & 0 deletions Errors.js
Original file line number Diff line number Diff line change
@@ -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');
38 changes: 38 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/BrowserFetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions lib/Errors.js
Original file line number Diff line number Diff line change
@@ -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,
};
3 changes: 2 additions & 1 deletion lib/FrameManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion lib/Launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}`));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion lib/NavigatorWatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const {helper, assert} = require('./helper');
const {FrameManager} = require('./FrameManager');
const {TimeoutError} = require('./Errors');

class NavigatorWatcher {
/**
Expand Down Expand Up @@ -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));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>} */
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 3 additions & 1 deletion test/browser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion test/browsercontext.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions test/frame.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

const utils = require('./utils');
const {TimeoutError} = utils.requireRoot('Errors');

module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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>`);
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions test/headful.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-');

Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions test/ignorehttpserrors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions test/input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
13 changes: 9 additions & 4 deletions test/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) => {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions test/puppeteer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -145,15 +145,15 @@ 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));

expect(dumpioData).toContain(dumpioTextToLog);
});
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 = '';
Expand Down
5 changes: 3 additions & 2 deletions test/target.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 204c7ec

Please sign in to comment.