Skip to content

Commit

Permalink
feat(firefox): introduce async stacks for Puppeteer-Firefox (#3948)
Browse files Browse the repository at this point in the history
This patch refactors Puppeteer-Firefox code to declare public
API in `/lib/api.js` and use it to setup async stack hooks
over the public API method calls.
  • Loading branch information
aslushnikov committed Feb 7, 2019
1 parent 9216056 commit 6b18e8c
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 48 deletions.
47 changes: 8 additions & 39 deletions experimental/puppeteer-firefox/index.js
Expand Up @@ -13,44 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const FirefoxLauncher = require('./lib/Launcher.js').Launcher;
const BrowserFetcher = require('./lib/BrowserFetcher.js');

class Puppeteer {
constructor() {
this._firefoxLauncher = new FirefoxLauncher();
}
const {helper} = require('./lib/helper');
const api = require('./lib/api');
for (const className in api)
helper.installAsyncStackHooks(api[className]);

async launch(options = {}) {
const {
args = [],
dumpio = !!process.env.DUMPIO,
handleSIGHUP = true,
handleSIGINT = true,
handleSIGTERM = true,
headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true',
defaultViewport = {width: 800, height: 600},
ignoreHTTPSErrors = false,
slowMo = 0,
executablePath = this.executablePath(),
} = options;
options = {
args, slowMo, dumpio, executablePath, handleSIGHUP, handleSIGINT, handleSIGTERM, headless, defaultViewport,
ignoreHTTPSErrors
};
return await this._firefoxLauncher.launch(options);
}

createBrowserFetcher(options) {
return new BrowserFetcher(__dirname, options);
}

executablePath() {
const browserFetcher = new BrowserFetcher(__dirname, { product: 'firefox' });
const revision = require('./package.json').puppeteer.firefox_revision;
const revisionInfo = browserFetcher.revisionInfo(revision);
return revisionInfo.executablePath;
}
}

module.exports = new Puppeteer();
const {Puppeteer} = require('./lib/Puppeteer');
const packageJson = require('./package.json');
const preferredRevision = packageJson.puppeteer.firefox_revision;
module.exports = new Puppeteer(__dirname, preferredRevision);
2 changes: 1 addition & 1 deletion experimental/puppeteer-firefox/lib/Browser.js
Expand Up @@ -312,4 +312,4 @@ BrowserContext.Events = {
TargetDestroyed: 'targetdestroyed'
}

module.exports = {Browser, Target};
module.exports = {Browser, BrowserContext, Target};
2 changes: 1 addition & 1 deletion experimental/puppeteer-firefox/lib/BrowserFetcher.js
Expand Up @@ -228,7 +228,7 @@ class BrowserFetcher {
}
}

module.exports = BrowserFetcher;
module.exports = {BrowserFetcher};

/**
* @param {string} folderPath
Expand Down
20 changes: 16 additions & 4 deletions experimental/puppeteer-firefox/lib/Launcher.js
Expand Up @@ -19,6 +19,7 @@ const removeFolder = require('rimraf');
const childProcess = require('child_process');
const {Connection} = require('./Connection');
const {Browser} = require('./Browser');
const {BrowserFetcher} = require('./BrowserFetcher');
const readline = require('readline');
const fs = require('fs');
const util = require('util');
Expand All @@ -35,6 +36,11 @@ const FIREFOX_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_firefox_profile-'
* @internal
*/
class Launcher {
constructor(projectRoot, preferredRevision) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
}

/**
* @param {Object} options
* @return {!Promise<!Browser>}
Expand All @@ -43,7 +49,7 @@ class Launcher {
const {
args = [],
dumpio = false,
executablePath = null,
executablePath = this.executablePath(),
handleSIGHUP = true,
handleSIGINT = true,
handleSIGTERM = true,
Expand All @@ -53,9 +59,6 @@ class Launcher {
slowMo = 0,
} = options;

if (!executablePath)
throw new Error('Firefox launching is only supported with local version of firefox!');

const firefoxArguments = args.slice();
firefoxArguments.push('-no-remote');
firefoxArguments.push('-juggler', '0');
Expand Down Expand Up @@ -152,6 +155,15 @@ class Launcher {
} catch (e) { }
}
}

/**
* @return {string}
*/
executablePath() {
const browserFetcher = new BrowserFetcher(this._projectRoot, { product: 'firefox' });
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
return revisionInfo.executablePath;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion experimental/puppeteer-firefox/lib/Page.js
Expand Up @@ -1446,4 +1446,4 @@ class NavigationWatchdog {
}
}

module.exports = {Page};
module.exports = {Page, Frame, ConsoleMessage};
27 changes: 27 additions & 0 deletions experimental/puppeteer-firefox/lib/Puppeteer.js
@@ -0,0 +1,27 @@
const {Launcher} = require('./Launcher.js');
const {BrowserFetcher} = require('./BrowserFetcher.js');

class Puppeteer {
/**
* @param {string} projectRoot
* @param {string} preferredRevision
*/
constructor(projectRoot, preferredRevision) {
this._projectRoot = projectRoot;
this._launcher = new Launcher(projectRoot, preferredRevision);
}

async launch(options = {}) {
return this._launcher.launch(options);
}

createBrowserFetcher(options) {
return new BrowserFetcher(this._projectRoot, options);
}

executablePath() {
return this._launcher.executablePath();
}
}

module.exports = {Puppeteer};
16 changes: 16 additions & 0 deletions experimental/puppeteer-firefox/lib/api.js
@@ -0,0 +1,16 @@
module.exports = {
Browser: require('./Browser').Browser,
BrowserContext: require('./Browser').BrowserContext,
BrowserFetcher: require('./BrowserFetcher').BrowserFetcher,
ConsoleMessage: require('./Page').ConsoleMessage,
Dialog: require('./Dialog').Dialog,
ElementHandle: require('./JSHandle').ElementHandle,
Frame: require('./Page').Frame,
JSHandle: require('./JSHandle').JSHandle,
Keyboard: require('./Input').Keyboard,
Mouse: require('./Input').Mouse,
Page: require('./Page').Page,
Puppeteer: require('./Puppeteer').Puppeteer,
Target: require('./Browser').Target,
TimeoutError: require('./Errors').TimeoutError,
};
21 changes: 21 additions & 0 deletions experimental/puppeteer-firefox/lib/helper.js
Expand Up @@ -19,6 +19,27 @@ const {TimeoutError} = require('./Errors');
* @internal
*/
class Helper {
/**
* @param {!Object} classType
*/
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')
continue;
Reflect.set(classType.prototype, methodName, function(...args) {
const syncStack = new Error();
return method.call(this, ...args).catch(e => {
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
const clientStack = stack.substring(stack.indexOf('\n'));
if (!e.stack.includes(clientStack))
e.stack += '\n -- ASYNC --\n' + stack;
throw e;
});
});
}
}

/**
* @param {Function|string} fun
* @param {!Array<*>} args
Expand Down
4 changes: 2 additions & 2 deletions test/page.spec.js
Expand Up @@ -79,7 +79,7 @@ module.exports.addTests = function({testRunner, expect, headless, Errors, Device
});
});

(asyncawait ? describe_fails_ffox : xdescribe)('Async stacks', () => {
(asyncawait ? describe : xdescribe)('Async stacks', () => {
it('should work', async({page, server}) => {
server.setRoute('/empty.html', (req, res) => {
res.statusCode = 204;
Expand All @@ -88,7 +88,7 @@ module.exports.addTests = function({testRunner, expect, headless, Errors, Device
let error = null;
await page.goto(server.EMPTY_PAGE).catch(e => error = e);
expect(error).not.toBe(null);
expect(error.message).toContain('net::ERR_ABORTED');
expect(error.stack).toContain(__filename);
});
});

Expand Down

0 comments on commit 6b18e8c

Please sign in to comment.