Move screenshot task chain in Browser

Currently, it's impossible to do screenshots in parallel.
This patch:
- makes all screenshot tasks sequential inside one browser
- starts activating target before taking screenshot
- adds a test to make sure it's possible to take screenshots across
  tabs
- starts waiting for a proper page closing after each test. This might
  finally solve the ECONNRESET issues in tests.

References #89
This commit is contained in:
Andrey Lushnikov 2017-07-18 22:10:38 -07:00
parent 2e94f9f67b
commit 21af495b65
10 changed files with 119 additions and 77 deletions

View File

@ -65,6 +65,7 @@ class Browser {
this._terminated = false; this._terminated = false;
this._chromeProcess = null; this._chromeProcess = null;
this._launchPromise = null; this._launchPromise = null;
this._screenshotTaskQueue = new TaskQueue();
this.stderr = new ProxyStream(); this.stderr = new ProxyStream();
this.stdout = new ProxyStream(); this.stdout = new ProxyStream();
@ -78,7 +79,7 @@ class Browser {
if (!this._chromeProcess || this._terminated) if (!this._chromeProcess || this._terminated)
throw new Error('ERROR: this chrome instance is not alive any more!'); throw new Error('ERROR: this chrome instance is not alive any more!');
let client = await Connection.create(this._remoteDebuggingPort); let client = await Connection.create(this._remoteDebuggingPort);
let page = await Page.create(client); let page = await Page.create(client, this._screenshotTaskQueue);
return page; return page;
} }
@ -162,6 +163,22 @@ function waitForRemoteDebuggingPort(chromeProcess) {
} }
} }
class TaskQueue {
constructor() {
this._chain = Promise.resolve();
}
/**
* @param {function():!Promise} task
* @return {!Promise}
*/
postTask(task) {
let result = this._chain.then(task);
this._chain = result.catch(() => {});
return result;
}
}
class ProxyStream extends Duplex { class ProxyStream extends Duplex {
_read() { } _read() { }

View File

@ -23,13 +23,13 @@ const COMMAND_TIMEOUT = 10000;
class Connection extends EventEmitter { class Connection extends EventEmitter {
/** /**
* @param {number} port * @param {number} port
* @param {string} pageId * @param {string} targetId
* @param {!WebSocket} ws * @param {!WebSocket} ws
*/ */
constructor(port, pageId, ws) { constructor(port, targetId, ws) {
super(); super();
this._port = port; this._port = port;
this._pageId = pageId; this._targetId = targetId;
this._lastId = 0; this._lastId = 0;
/** @type {!Map<number, {resolve: function(*), reject: function(*), method: string}>}*/ /** @type {!Map<number, {resolve: function(*), reject: function(*), method: string}>}*/
this._callbacks = new Map(); this._callbacks = new Map();
@ -39,6 +39,13 @@ class Connection extends EventEmitter {
this._ws.on('close', this._onClose.bind(this)); this._ws.on('close', this._onClose.bind(this));
} }
/**
* @return {string}
*/
targetId() {
return this._targetId;
}
/** /**
* @param {string} method * @param {string} method
* @param {(!Object|undefined)} params * @param {(!Object|undefined)} params
@ -84,7 +91,7 @@ class Connection extends EventEmitter {
* @return {!Promise} * @return {!Promise}
*/ */
async dispose() { async dispose() {
await runJsonCommand(this._port, `close/${this._pageId}`); await runJsonCommand(this._port, `close/${this._targetId}`);
} }
/** /**

View File

@ -28,9 +28,10 @@ let helper = require('./helper');
class Page extends EventEmitter { class Page extends EventEmitter {
/** /**
* @param {!Connection} client * @param {!Connection} client
* @param {!TaskQueue} screenshotTaskQueue
* @return {!Promise<!Page>} * @return {!Promise<!Page>}
*/ */
static async create(client) { static async create(client, screenshotTaskQueue) {
await Promise.all([ await Promise.all([
client.send('Network.enable', {}), client.send('Network.enable', {}),
client.send('Page.enable', {}), client.send('Page.enable', {}),
@ -41,7 +42,7 @@ class Page extends EventEmitter {
let {result:{value: userAgent}} = await client.send('Runtime.evaluate', { expression: userAgentExpression, returnByValue: true }); let {result:{value: userAgent}} = await client.send('Runtime.evaluate', { expression: userAgentExpression, returnByValue: true });
let frameManager = await FrameManager.create(client); let frameManager = await FrameManager.create(client);
let networkManager = new NetworkManager(client, userAgent); let networkManager = new NetworkManager(client, userAgent);
let page = new Page(client, frameManager, networkManager); let page = new Page(client, frameManager, networkManager, screenshotTaskQueue);
// Initialize default page size. // Initialize default page size.
await page.setViewport({width: 400, height: 300}); await page.setViewport({width: 400, height: 300});
return page; return page;
@ -51,8 +52,9 @@ class Page extends EventEmitter {
* @param {!Connection} client * @param {!Connection} client
* @param {!FrameManager} frameManager * @param {!FrameManager} frameManager
* @param {!NetworkManager} networkManager * @param {!NetworkManager} networkManager
* @param {!TaskQueue} screenshotTaskQueue
*/ */
constructor(client, frameManager, networkManager) { constructor(client, frameManager, networkManager, screenshotTaskQueue) {
super(); super();
this._client = client; this._client = client;
this._frameManager = frameManager; this._frameManager = frameManager;
@ -62,7 +64,7 @@ class Page extends EventEmitter {
this._keyboard = new Keyboard(this._client); this._keyboard = new Keyboard(this._client);
this._screenshotTaskChain = Promise.resolve(); this._screenshotTaskQueue = screenshotTaskQueue;
this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event)); this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event)); this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
@ -421,8 +423,7 @@ class Page extends EventEmitter {
console.assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width)); console.assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
console.assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height)); console.assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height));
} }
this._screenshotTaskChain = this._screenshotTaskChain.then(this._screenshotTask.bind(this, screenshotType, options)); return this._screenshotTaskQueue.postTask(this._screenshotTask.bind(this, screenshotType, options));
return this._screenshotTaskChain;
} }
/** /**
@ -431,6 +432,7 @@ class Page extends EventEmitter {
* @return {!Promise<!Buffer>} * @return {!Promise<!Buffer>}
*/ */
async _screenshotTask(format, options) { async _screenshotTask(format, options) {
await this._client.send('Target.activateTarget', {targetId: this._client.targetId()});
if (options.fullPage) { if (options.fullPage) {
const metrics = await this._client.send('Page.getLayoutMetrics'); const metrics = await this._client.send('Page.getLayoutMetrics');
const width = Math.ceil(metrics.contentSize.width); const width = Math.ceil(metrics.contentSize.width);

BIN
test/golden/grid-cell-0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

BIN
test/golden/grid-cell-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

BIN
test/golden/grid-cell-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

BIN
test/golden/grid-cell-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 B

View File

@ -50,7 +50,7 @@ describe('Puppeteer', function() {
let page; let page;
beforeAll(SX(async function() { beforeAll(SX(async function() {
browser = new Browser({args: ['--no-sandbox']}); browser = new Browser({headless: true, args: ['--no-sandbox']});
const assetsPath = path.join(__dirname, 'assets'); const assetsPath = path.join(__dirname, 'assets');
server = await SimpleServer.create(assetsPath, PORT); server = await SimpleServer.create(assetsPath, PORT);
httpsServer = await SimpleServer.createHTTPS(assetsPath, HTTPS_PORT); httpsServer = await SimpleServer.createHTTPS(assetsPath, HTTPS_PORT);
@ -73,9 +73,9 @@ describe('Puppeteer', function() {
GoldenUtils.addMatchers(jasmine, GOLDEN_DIR, OUTPUT_DIR); GoldenUtils.addMatchers(jasmine, GOLDEN_DIR, OUTPUT_DIR);
})); }));
afterEach(function() { afterEach(SX(async function() {
page.close(); await page.close();
}); }));
describe('Page.evaluate', function() { describe('Page.evaluate', function() {
it('should work', SX(async function() { it('should work', SX(async function() {
@ -581,66 +581,6 @@ describe('Puppeteer', function() {
})); }));
}); });
describe('Page.screenshot', function() {
it('should work', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot();
expect(screenshot).toBeGolden('screenshot-sanity.png');
}));
it('should clip rect', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
clip: {
x: 50,
y: 100,
width: 150,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
}));
it('should work for offscreen clip', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
clip: {
x: 50,
y: 600,
width: 100,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}));
it('should run in parallel', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let promises = [];
for (let i = 0; i < 3; ++i) {
promises.push(page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50
}
}));
}
let screenshots = await Promise.all(promises);
expect(screenshots[1]).toBeGolden('screenshot-parallel-calls.png');
}));
it('should take fullPage screenshots', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
fullPage: true
});
expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
}));
});
describe('Frame Management', function() { describe('Frame Management', function() {
let FrameUtils = require('./frame-utils'); let FrameUtils = require('./frame-utils');
it('should handle nested frames', SX(async function() { it('should handle nested frames', SX(async function() {
@ -1064,6 +1004,81 @@ describe('Puppeteer', function() {
expect((await page.$$('span', (element, index, arg1) => arg1, 'value1'))[0]).toBe('value1'); expect((await page.$$('span', (element, index, arg1) => arg1, 'value1'))[0]).toBe('value1');
})); }));
}); });
describe('Page.screenshot', function() {
it('should work', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot();
expect(screenshot).toBeGolden('screenshot-sanity.png');
}));
it('should clip rect', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
clip: {
x: 50,
y: 100,
width: 150,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
}));
it('should work for offscreen clip', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
clip: {
x: 50,
y: 600,
width: 100,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}));
it('should run in parallel', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let promises = [];
for (let i = 0; i < 3; ++i) {
promises.push(page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50
}
}));
}
let screenshots = await Promise.all(promises);
expect(screenshots[1]).toBeGolden('grid-cell-1.png');
}));
it('should take fullPage screenshots', SX(async function() {
await page.setViewport({width: 500, height: 500});
await page.navigate(PREFIX + '/grid.html');
let screenshot = await page.screenshot({
fullPage: true
});
expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
}));
it('should run in parallel in multiple pages', SX(async function() {
const N = 2;
let pages = await Promise.all(Array(N).fill(0).map(async() => {
let page = await browser.newPage();
await page.navigate(PREFIX + '/grid.html');
return page;
}));
let promises = [];
for (let i = 0; i < N; ++i)
promises.push(pages[i].screenshot({ clip: { x: 50 * i, y: 0, width: 50, height: 50 } }));
let screenshots = await Promise.all(promises);
for (let i = 0; i < N; ++i)
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
await Promise.all(pages.map(page => page.close()));
}));
});
}); });
/** /**

View File

@ -13,7 +13,8 @@ let EXCLUDE_CLASSES = new Set([
'Helper', 'Helper',
'NavigatorWatcher', 'NavigatorWatcher',
'NetworkManager', 'NetworkManager',
'ProxyStream' 'ProxyStream',
'TaskQueue',
]); ]);
let EXCLUDE_METHODS = new Set([ let EXCLUDE_METHODS = new Set([
@ -23,11 +24,11 @@ let EXCLUDE_METHODS = new Set([
'Headers.constructor', 'Headers.constructor',
'Headers.fromPayload', 'Headers.fromPayload',
'InterceptedRequest.constructor', 'InterceptedRequest.constructor',
'Keyboard.constructor',
'Page.constructor', 'Page.constructor',
'Page.create', 'Page.create',
'Request.constructor', 'Request.constructor',
'Response.constructor', 'Response.constructor',
'Keyboard.constructor',
]); ]);
/** /**