feat(firefox): introduce async stacks for Puppeteer-Firefox (#3948)

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.
This commit is contained in:
Andrey Lushnikov 2019-02-07 15:18:43 -08:00 committed by GitHub
parent 9216056d12
commit 6b18e8cef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 93 additions and 48 deletions

View File

@ -13,44 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const FirefoxLauncher = require('./lib/Launcher.js').Launcher;
const BrowserFetcher = require('./lib/BrowserFetcher.js');
class Puppeteer { const {helper} = require('./lib/helper');
constructor() { const api = require('./lib/api');
this._firefoxLauncher = new FirefoxLauncher(); for (const className in api)
} helper.installAsyncStackHooks(api[className]);
async launch(options = {}) { const {Puppeteer} = require('./lib/Puppeteer');
const { const packageJson = require('./package.json');
args = [], const preferredRevision = packageJson.puppeteer.firefox_revision;
dumpio = !!process.env.DUMPIO, module.exports = new Puppeteer(__dirname, preferredRevision);
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();

View File

@ -312,4 +312,4 @@ BrowserContext.Events = {
TargetDestroyed: 'targetdestroyed' TargetDestroyed: 'targetdestroyed'
} }
module.exports = {Browser, Target}; module.exports = {Browser, BrowserContext, Target};

View File

@ -228,7 +228,7 @@ class BrowserFetcher {
} }
} }
module.exports = BrowserFetcher; module.exports = {BrowserFetcher};
/** /**
* @param {string} folderPath * @param {string} folderPath

View File

@ -19,6 +19,7 @@ const removeFolder = require('rimraf');
const childProcess = require('child_process'); const childProcess = require('child_process');
const {Connection} = require('./Connection'); const {Connection} = require('./Connection');
const {Browser} = require('./Browser'); const {Browser} = require('./Browser');
const {BrowserFetcher} = require('./BrowserFetcher');
const readline = require('readline'); const readline = require('readline');
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
@ -35,6 +36,11 @@ const FIREFOX_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_firefox_profile-'
* @internal * @internal
*/ */
class Launcher { class Launcher {
constructor(projectRoot, preferredRevision) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
}
/** /**
* @param {Object} options * @param {Object} options
* @return {!Promise<!Browser>} * @return {!Promise<!Browser>}
@ -43,7 +49,7 @@ class Launcher {
const { const {
args = [], args = [],
dumpio = false, dumpio = false,
executablePath = null, executablePath = this.executablePath(),
handleSIGHUP = true, handleSIGHUP = true,
handleSIGINT = true, handleSIGINT = true,
handleSIGTERM = true, handleSIGTERM = true,
@ -53,9 +59,6 @@ class Launcher {
slowMo = 0, slowMo = 0,
} = options; } = options;
if (!executablePath)
throw new Error('Firefox launching is only supported with local version of firefox!');
const firefoxArguments = args.slice(); const firefoxArguments = args.slice();
firefoxArguments.push('-no-remote'); firefoxArguments.push('-no-remote');
firefoxArguments.push('-juggler', '0'); firefoxArguments.push('-juggler', '0');
@ -152,6 +155,15 @@ class Launcher {
} catch (e) { } } catch (e) { }
} }
} }
/**
* @return {string}
*/
executablePath() {
const browserFetcher = new BrowserFetcher(this._projectRoot, { product: 'firefox' });
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
return revisionInfo.executablePath;
}
} }
/** /**

View File

@ -1446,4 +1446,4 @@ class NavigationWatchdog {
} }
} }
module.exports = {Page}; module.exports = {Page, Frame, ConsoleMessage};

View File

@ -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};

View File

@ -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,
};

View File

@ -19,6 +19,27 @@ const {TimeoutError} = require('./Errors');
* @internal * @internal
*/ */
class Helper { 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 {Function|string} fun
* @param {!Array<*>} args * @param {!Array<*>} args

View File

@ -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}) => { it('should work', async({page, server}) => {
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.statusCode = 204; res.statusCode = 204;
@ -88,7 +88,7 @@ module.exports.addTests = function({testRunner, expect, headless, Errors, Device
let error = null; let error = null;
await page.goto(server.EMPTY_PAGE).catch(e => error = e); await page.goto(server.EMPTY_PAGE).catch(e => error = e);
expect(error).not.toBe(null); expect(error).not.toBe(null);
expect(error.message).toContain('net::ERR_ABORTED'); expect(error.stack).toContain(__filename);
}); });
}); });