puppeteer-core
Advanced tools
Comparing version 1.8.0 to 1.9.0
@@ -136,8 +136,2 @@ # How to Contribute | ||
- To filter tests by name: | ||
```bash | ||
npm run unit --filter=waitFor | ||
``` | ||
- To run tests in parallel, use `-j` flag: | ||
@@ -144,0 +138,0 @@ |
@@ -21,2 +21,3 @@ /** | ||
const {TaskQueue} = require('./TaskQueue'); | ||
const {Connection} = require('./Connection'); | ||
@@ -49,5 +50,3 @@ class Browser extends EventEmitter { | ||
this._targets = new Map(); | ||
this._connection.setClosedCallback(() => { | ||
this.emit(Browser.Events.Disconnected); | ||
}); | ||
this._connection.on(Connection.Events.Disconnected, () => this.emit(Browser.Events.Disconnected)); | ||
this._connection.on('Target.targetCreated', this._targetCreated.bind(this)); | ||
@@ -192,2 +191,9 @@ this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); | ||
/** | ||
* @return {!Target} | ||
*/ | ||
target() { | ||
return this.targets().find(target => target.type() === 'browser'); | ||
} | ||
/** | ||
* @return {!Promise<!Array<!Puppeteer.Page>>} | ||
@@ -194,0 +200,0 @@ */ |
@@ -31,9 +31,38 @@ /** | ||
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com'; | ||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64']; | ||
const downloadURLs = { | ||
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip', | ||
mac: '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', | ||
win32: '%s/chromium-browser-snapshots/Win/%d/chrome-win32.zip', | ||
win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip', | ||
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', | ||
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', | ||
win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', | ||
win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', | ||
}; | ||
/** | ||
* @param {string} platform | ||
* @param {string} revision | ||
* @return {string} | ||
*/ | ||
function archiveName(platform, revision) { | ||
if (platform === 'linux') | ||
return 'chrome-linux'; | ||
if (platform === 'mac') | ||
return 'chrome-mac'; | ||
if (platform === 'win32' || platform === 'win64') { | ||
// Windows archive name changed at r591479. | ||
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; | ||
} | ||
return null; | ||
} | ||
/** | ||
* @param {string} platform | ||
* @param {string} host | ||
* @param {string} revision | ||
* @return {string} | ||
*/ | ||
function downloadURL(platform, host, revision) { | ||
return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision)); | ||
} | ||
const readdirAsync = helper.promisify(fs.readdir.bind(fs)); | ||
@@ -70,3 +99,2 @@ const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); | ||
} | ||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64']; | ||
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform); | ||
@@ -87,4 +115,3 @@ } | ||
canDownload(revision) { | ||
const url = util.format(downloadURLs[this._platform], this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
let resolve; | ||
@@ -108,4 +135,3 @@ const promise = new Promise(x => resolve = x); | ||
async download(revision, progressCallback) { | ||
let url = downloadURLs[this._platform]; | ||
url = util.format(url, this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`); | ||
@@ -158,11 +184,10 @@ const folderPath = this._getFolderPath(revision); | ||
if (this._platform === 'mac') | ||
executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); | ||
else if (this._platform === 'linux') | ||
executablePath = path.join(folderPath, 'chrome-linux', 'chrome'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome'); | ||
else if (this._platform === 'win32' || this._platform === 'win64') | ||
executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe'); | ||
else | ||
throw new Error('Unsupported platform: ' + this._platform); | ||
let url = downloadURLs[this._platform]; | ||
url = util.format(url, this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
const local = fs.existsSync(folderPath); | ||
@@ -193,3 +218,3 @@ return {revision, executablePath, folderPath, local, url}; | ||
const [platform, revision] = splits; | ||
if (!downloadURLs[platform]) | ||
if (!supportedPlatforms.includes(platform)) | ||
return null; | ||
@@ -196,0 +221,0 @@ return {platform, revision}; |
@@ -19,6 +19,3 @@ /** | ||
const debugSession = require('debug')('puppeteer:session'); | ||
const EventEmitter = require('events'); | ||
const WebSocket = require('ws'); | ||
const Pipe = require('./Pipe'); | ||
@@ -28,25 +25,2 @@ class Connection extends EventEmitter { | ||
* @param {string} url | ||
* @param {number=} delay | ||
* @return {!Promise<!Connection>} | ||
*/ | ||
static async createForWebSocket(url, delay = 0) { | ||
return new Promise((resolve, reject) => { | ||
const ws = new WebSocket(url, { perMessageDeflate: false }); | ||
ws.on('open', () => resolve(new Connection(url, ws, delay))); | ||
ws.on('error', reject); | ||
}); | ||
} | ||
/** | ||
* @param {!NodeJS.WritableStream} pipeWrite | ||
* @param {!NodeJS.ReadableStream} pipeRead | ||
* @param {number=} delay | ||
* @return {!Connection} | ||
*/ | ||
static createForPipe(pipeWrite, pipeRead, delay = 0) { | ||
return new Connection('', new Pipe(pipeWrite, pipeRead), delay); | ||
} | ||
/** | ||
* @param {string} url | ||
* @param {!Puppeteer.ConnectionTransport} transport | ||
@@ -64,9 +38,22 @@ * @param {number=} delay | ||
this._transport = transport; | ||
this._transport.on('message', this._onMessage.bind(this)); | ||
this._transport.on('close', this._onClose.bind(this)); | ||
this._transport.onmessage = this._onMessage.bind(this); | ||
this._transport.onclose = this._onClose.bind(this); | ||
/** @type {!Map<string, !CDPSession>}*/ | ||
this._sessions = new Map(); | ||
this._closed = false; | ||
} | ||
/** | ||
* @param {!CDPSession} session | ||
* @return {!Connection} | ||
*/ | ||
static fromSession(session) { | ||
let connection = session._connection; | ||
// TODO(lushnikov): move to flatten protocol to avoid this. | ||
while (connection instanceof CDPSession) | ||
connection = connection._connection; | ||
return connection; | ||
} | ||
/** | ||
* @return {string} | ||
@@ -94,9 +81,2 @@ */ | ||
/** | ||
* @param {function()} callback | ||
*/ | ||
setClosedCallback(callback) { | ||
this._closeCallback = callback; | ||
} | ||
/** | ||
* @param {string} message | ||
@@ -136,9 +116,7 @@ */ | ||
_onClose() { | ||
if (this._closeCallback) { | ||
this._closeCallback(); | ||
this._closeCallback = null; | ||
} | ||
this._transport.removeAllListeners(); | ||
// If transport throws any error at this point of time, we don't care and should swallow it. | ||
this._transport.on('error', () => {}); | ||
if (this._closed) | ||
return; | ||
this._closed = true; | ||
this._transport.onmessage = null; | ||
this._transport.onclose = null; | ||
for (const callback of this._callbacks.values()) | ||
@@ -150,2 +128,3 @@ callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); | ||
this._sessions.clear(); | ||
this.emit(Connection.Events.Disconnected); | ||
} | ||
@@ -170,2 +149,6 @@ | ||
Connection.Events = { | ||
Disconnected: Symbol('Connection.Events.Disconnected'), | ||
}; | ||
class CDPSession extends EventEmitter { | ||
@@ -172,0 +155,0 @@ /** |
@@ -22,2 +22,4 @@ /** | ||
const {TimeoutError} = require('./Errors'); | ||
const {NetworkManager} = require('./NetworkManager'); | ||
const {Connection} = require('./Connection'); | ||
@@ -31,7 +33,10 @@ const readFileAsync = helper.promisify(fs.readFile); | ||
* @param {!Puppeteer.Page} page | ||
* @param {!Puppeteer.NetworkManager} networkManager | ||
*/ | ||
constructor(client, frameTree, page) { | ||
constructor(client, frameTree, page, networkManager) { | ||
super(); | ||
this._client = client; | ||
this._page = page; | ||
this._networkManager = networkManager; | ||
this._defaultNavigationTimeout = 30000; | ||
/** @type {!Map<string, !Frame>} */ | ||
@@ -56,2 +61,73 @@ this._frames = new Map(); | ||
/** | ||
* @param {number} timeout | ||
*/ | ||
setDefaultNavigationTimeout(timeout) { | ||
this._defaultNavigationTimeout = timeout; | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {string} url | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
async navigateFrame(frame, url, options = {}) { | ||
const referrer = typeof options.referer === 'string' ? options.referer : this._networkManager.extraHTTPHeaders()['referer']; | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options); | ||
let ensureNewDocumentNavigation = false; | ||
let error = await Promise.race([ | ||
navigate(this._client, url, referrer, frame._id), | ||
watcher.timeoutOrTerminationPromise(), | ||
]); | ||
if (!error) { | ||
error = await Promise.race([ | ||
watcher.timeoutOrTerminationPromise(), | ||
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(), | ||
]); | ||
} | ||
watcher.dispose(); | ||
if (error) | ||
throw error; | ||
return watcher.navigationResponse(); | ||
/** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {string} url | ||
* @param {string} referrer | ||
* @param {string} frameId | ||
* @return {!Promise<?Error>} | ||
*/ | ||
async function navigate(client, url, referrer, frameId) { | ||
try { | ||
const response = await client.send('Page.navigate', {url, referrer, frameId}); | ||
ensureNewDocumentNavigation = !!response.loaderId; | ||
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null; | ||
} catch (error) { | ||
return error; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
async waitForFrameNavigation(frame, options) { | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options); | ||
const error = await Promise.race([ | ||
watcher.timeoutOrTerminationPromise(), | ||
watcher.sameDocumentNavigationPromise(), | ||
watcher.newDocumentNavigationPromise() | ||
]); | ||
watcher.dispose(); | ||
if (error) | ||
throw error; | ||
return watcher.navigationResponse(); | ||
} | ||
/** | ||
* @param {!Protocol.Page.lifecycleEventPayload} event | ||
@@ -326,2 +402,19 @@ */ | ||
/** | ||
* @param {string} url | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
async goto(url, options = {}) { | ||
return await this._frameManager.navigateFrame(this, url, options); | ||
} | ||
/** | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
async waitForNavigation(options = {}) { | ||
return await this._frameManager.waitForFrameNavigation(this, options); | ||
} | ||
/** | ||
* @return {!Promise<!ExecutionContext>} | ||
@@ -703,3 +796,3 @@ */ | ||
if (helper.isNumber(selectorOrFunctionOrTimeout)) | ||
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); | ||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout))); | ||
if (typeof selectorOrFunctionOrTimeout === 'function') | ||
@@ -1025,2 +1118,174 @@ return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args); | ||
class NavigatorWatcher { | ||
/** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {!FrameManager} frameManager | ||
* @param {!NetworkManager} networkManager | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {number} timeout | ||
* @param {!Object=} options | ||
*/ | ||
constructor(client, frameManager, networkManager, frame, timeout, options = {}) { | ||
assert(options.networkIdleTimeout === undefined, 'ERROR: networkIdleTimeout option is no longer supported.'); | ||
assert(options.networkIdleInflight === undefined, 'ERROR: networkIdleInflight option is no longer supported.'); | ||
assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'); | ||
let waitUntil = ['load']; | ||
if (Array.isArray(options.waitUntil)) | ||
waitUntil = options.waitUntil.slice(); | ||
else if (typeof options.waitUntil === 'string') | ||
waitUntil = [options.waitUntil]; | ||
this._expectedLifecycle = waitUntil.map(value => { | ||
const protocolEvent = puppeteerToProtocolLifecycle[value]; | ||
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value); | ||
return protocolEvent; | ||
}); | ||
this._frameManager = frameManager; | ||
this._networkManager = networkManager; | ||
this._frame = frame; | ||
this._initialLoaderId = frame._loaderId; | ||
this._timeout = timeout; | ||
/** @type {?Puppeteer.Request} */ | ||
this._navigationRequest = null; | ||
this._hasSameDocumentNavigation = false; | ||
this._eventListeners = [ | ||
helper.addEventListener(Connection.fromSession(client), Connection.Events.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.LifecycleEvent, this._checkLifecycleComplete.bind(this)), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameDetached, this._onFrameDetached.bind(this)), | ||
helper.addEventListener(this._networkManager, NetworkManager.Events.Request, this._onRequest.bind(this)), | ||
]; | ||
this._sameDocumentNavigationPromise = new Promise(fulfill => { | ||
this._sameDocumentNavigationCompleteCallback = fulfill; | ||
}); | ||
this._newDocumentNavigationPromise = new Promise(fulfill => { | ||
this._newDocumentNavigationCompleteCallback = fulfill; | ||
}); | ||
this._timeoutPromise = this._createTimeoutPromise(); | ||
this._terminationPromise = new Promise(fulfill => { | ||
this._terminationCallback = fulfill; | ||
}); | ||
} | ||
/** | ||
* @param {!Puppeteer.Request} request | ||
*/ | ||
_onRequest(request) { | ||
if (request.frame() !== this._frame || !request.isNavigationRequest()) | ||
return; | ||
this._navigationRequest = request; | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
*/ | ||
_onFrameDetached(frame) { | ||
if (this._frame === frame) { | ||
this._terminationCallback.call(null, new Error('Navigating frame was detached')); | ||
return; | ||
} | ||
this._checkLifecycleComplete(); | ||
} | ||
/** | ||
* @return {?Puppeteer.Response} | ||
*/ | ||
navigationResponse() { | ||
return this._navigationRequest ? this._navigationRequest.response() : null; | ||
} | ||
/** | ||
* @param {!Error} error | ||
*/ | ||
_terminate(error) { | ||
this._terminationCallback.call(null, error); | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
sameDocumentNavigationPromise() { | ||
return this._sameDocumentNavigationPromise; | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
newDocumentNavigationPromise() { | ||
return this._newDocumentNavigationPromise; | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
timeoutOrTerminationPromise() { | ||
return Promise.race([this._timeoutPromise, this._terminationPromise]); | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
_createTimeoutPromise() { | ||
if (!this._timeout) | ||
return new Promise(() => {}); | ||
const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded'; | ||
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout)) | ||
.then(() => new TimeoutError(errorMessage)); | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
*/ | ||
_navigatedWithinDocument(frame) { | ||
if (frame !== this._frame) | ||
return; | ||
this._hasSameDocumentNavigation = true; | ||
this._checkLifecycleComplete(); | ||
} | ||
_checkLifecycleComplete() { | ||
// We expect navigation to commit. | ||
if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation) | ||
return; | ||
if (!checkLifecycle(this._frame, this._expectedLifecycle)) | ||
return; | ||
if (this._hasSameDocumentNavigation) | ||
this._sameDocumentNavigationCompleteCallback(); | ||
if (this._frame._loaderId !== this._initialLoaderId) | ||
this._newDocumentNavigationCompleteCallback(); | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {!Array<string>} expectedLifecycle | ||
* @return {boolean} | ||
*/ | ||
function checkLifecycle(frame, expectedLifecycle) { | ||
for (const event of expectedLifecycle) { | ||
if (!frame._lifecycleEvents.has(event)) | ||
return false; | ||
} | ||
for (const child of frame.childFrames()) { | ||
if (!checkLifecycle(child, expectedLifecycle)) | ||
return false; | ||
} | ||
return true; | ||
} | ||
} | ||
dispose() { | ||
helper.removeEventListeners(this._eventListeners); | ||
clearTimeout(this._maximumTimer); | ||
} | ||
} | ||
const puppeteerToProtocolLifecycle = { | ||
'load': 'load', | ||
'domcontentloaded': 'DOMContentLoaded', | ||
'networkidle0': 'networkIdle', | ||
'networkidle2': 'networkAlmostIdle', | ||
}; | ||
module.exports = {FrameManager, Frame}; |
@@ -22,2 +22,35 @@ /** | ||
/** | ||
* @param {!Object} classType | ||
* @param {string=} publicName | ||
*/ | ||
function traceAPICoverage(classType, publicName) { | ||
if (!apiCoverage) | ||
return; | ||
let className = publicName || classType.prototype.constructor.name; | ||
className = className.substring(0, 1).toLowerCase() + className.substring(1); | ||
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') | ||
continue; | ||
apiCoverage.set(`${className}.${methodName}`, false); | ||
Reflect.set(classType.prototype, methodName, function(...args) { | ||
apiCoverage.set(`${className}.${methodName}`, true); | ||
return method.call(this, ...args); | ||
}); | ||
} | ||
if (classType.Events) { | ||
for (const event of Object.values(classType.Events)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); | ||
const method = Reflect.get(classType.prototype, 'emit'); | ||
Reflect.set(classType.prototype, 'emit', function(event, ...args) { | ||
if (this.listenerCount(event)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); | ||
return method.call(this, event, ...args); | ||
}); | ||
} | ||
} | ||
class Helper { | ||
@@ -107,63 +140,19 @@ /** | ||
static tracePublicAPI(classType, publicName) { | ||
let className = publicName || classType.prototype.constructor.name; | ||
className = className.substring(0, 1).toLowerCase() + className.substring(1); | ||
const debug = require('debug')(`puppeteer:${className}`); | ||
if (!debug.enabled && !apiCoverage) | ||
return; | ||
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') | ||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction') | ||
continue; | ||
if (apiCoverage) | ||
apiCoverage.set(`${className}.${methodName}`, false); | ||
Reflect.set(classType.prototype, methodName, function(...args) { | ||
const argsText = args.map(stringifyArgument).join(', '); | ||
const callsite = `${className}.${methodName}(${argsText})`; | ||
if (debug.enabled) | ||
debug(callsite); | ||
if (apiCoverage) | ||
apiCoverage.set(`${className}.${methodName}`, true); | ||
return method.call(this, ...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; | ||
}); | ||
}); | ||
} | ||
if (classType.Events) { | ||
if (apiCoverage) { | ||
for (const event of Object.values(classType.Events)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); | ||
} | ||
const method = Reflect.get(classType.prototype, 'emit'); | ||
Reflect.set(classType.prototype, 'emit', function(event, ...args) { | ||
const argsText = [JSON.stringify(event)].concat(args.map(stringifyArgument)).join(', '); | ||
if (debug.enabled && this.listenerCount(event)) | ||
debug(`${className}.emit(${argsText})`); | ||
if (apiCoverage && this.listenerCount(event)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); | ||
return method.call(this, event, ...args); | ||
}); | ||
} | ||
/** | ||
* @param {!Object} arg | ||
* @return {string} | ||
*/ | ||
function stringifyArgument(arg) { | ||
if (Helper.isString(arg) || Helper.isNumber(arg) || !arg) | ||
return JSON.stringify(arg); | ||
if (typeof arg === 'function') { | ||
let text = arg.toString().split('\n').map(line => line.trim()).join(''); | ||
if (text.length > 20) | ||
text = text.substring(0, 20) + '…'; | ||
return `"${text}"`; | ||
} | ||
const state = {}; | ||
const keys = Object.keys(arg); | ||
for (const key of keys) { | ||
const value = arg[key]; | ||
if (Helper.isString(value) || Helper.isNumber(value)) | ||
state[key] = JSON.stringify(value); | ||
} | ||
const name = arg.constructor.name === 'Object' ? '' : arg.constructor.name; | ||
return name + JSON.stringify(state); | ||
} | ||
traceAPICoverage(classType, publicName); | ||
} | ||
@@ -173,5 +162,5 @@ | ||
* @param {!NodeJS.EventEmitter} emitter | ||
* @param {string} eventName | ||
* @param {(string|symbol)} eventName | ||
* @param {function(?)} handler | ||
* @return {{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}} | ||
* @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}} | ||
*/ | ||
@@ -184,3 +173,3 @@ static addEventListener(emitter, eventName, handler) { | ||
/** | ||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}>} listeners | ||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}>} listeners | ||
*/ | ||
@@ -187,0 +176,0 @@ static removeEventListeners(listeners) { |
@@ -27,2 +27,4 @@ /** | ||
const {TimeoutError} = require('./Errors'); | ||
const WebSocketTransport = require('./WebSocketTransport'); | ||
const PipeTransport = require('./PipeTransport'); | ||
@@ -116,2 +118,3 @@ const mkdtempAsync = helper.promisify(fs.mkdtemp); | ||
const usePipe = chromeArguments.includes('--remote-debugging-pipe'); | ||
/** @type {!Array<"ignore"|"pipe">} */ | ||
const stdio = usePipe ? ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']; | ||
@@ -163,5 +166,7 @@ const chromeProcess = childProcess.spawn( | ||
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout, this._preferredRevision); | ||
connection = await Connection.createForWebSocket(browserWSEndpoint, slowMo); | ||
const transport = await WebSocketTransport.create(browserWSEndpoint); | ||
connection = new Connection(browserWSEndpoint, transport, slowMo); | ||
} else { | ||
connection = Connection.createForPipe(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]), slowMo); | ||
const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4])); | ||
connection = new Connection('', transport, slowMo); | ||
} | ||
@@ -272,3 +277,3 @@ const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome); | ||
/** | ||
* @param {!(BrowserOptions & {browserWSEndpoint: string})} options | ||
* @param {!(BrowserOptions & {browserWSEndpoint: string, transport?: !Puppeteer.ConnectionTransport})} options | ||
* @return {!Promise<!Browser>} | ||
@@ -281,5 +286,6 @@ */ | ||
defaultViewport = {width: 800, height: 600}, | ||
transport = await WebSocketTransport.create(browserWSEndpoint), | ||
slowMo = 0, | ||
} = options; | ||
const connection = await Connection.createForWebSocket(browserWSEndpoint, slowMo); | ||
const connection = new Connection(browserWSEndpoint, transport, slowMo); | ||
const {browserContextIds} = await connection.send('Target.getBrowserContexts'); | ||
@@ -286,0 +292,0 @@ return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError)); |
@@ -23,8 +23,7 @@ /** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {!Puppeteer.FrameManager} frameManager | ||
*/ | ||
constructor(client, frameManager) { | ||
constructor(client) { | ||
super(); | ||
this._client = client; | ||
this._frameManager = frameManager; | ||
this._frameManager = null; | ||
/** @type {!Map<string, !Request>} */ | ||
@@ -59,2 +58,9 @@ this._requestIdToRequest = new Map(); | ||
/** | ||
* @param {!Puppeteer.FrameManager} frameManager | ||
*/ | ||
setFrameManager(frameManager) { | ||
this._frameManager = frameManager; | ||
} | ||
/** | ||
* @param {?{username: string, password: string}} credentials | ||
@@ -201,3 +207,3 @@ */ | ||
} | ||
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null; | ||
const frame = event.frameId && this._frameManager ? this._frameManager.frame(event.frameId) : null; | ||
const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain); | ||
@@ -255,3 +261,7 @@ this._requestIdToRequest.set(event.requestId, request); | ||
return; | ||
request.response()._bodyLoadedPromiseFulfill.call(null); | ||
// Under certain conditions we never get the Network.responseReceived | ||
// event from protocol. @see https://crbug.com/883475 | ||
if (request.response()) | ||
request.response()._bodyLoadedPromiseFulfill.call(null); | ||
this._requestIdToRequest.delete(request._requestId); | ||
@@ -427,6 +437,4 @@ this._attemptedAuthentications.delete(request._interceptionId); | ||
responseHeaders['content-type'] = response.contentType; | ||
if (responseBody && !('content-length' in responseHeaders)) { | ||
// @ts-ignore | ||
if (responseBody && !('content-length' in responseHeaders)) | ||
responseHeaders['content-length'] = Buffer.byteLength(responseBody); | ||
} | ||
@@ -627,2 +635,9 @@ const statusCode = response.status || 200; | ||
} | ||
/** | ||
* @return {?Puppeteer.Frame} | ||
*/ | ||
frame() { | ||
return this._request.frame(); | ||
} | ||
} | ||
@@ -629,0 +644,0 @@ helper.tracePublicAPI(Response); |
@@ -21,3 +21,2 @@ /** | ||
const {NetworkManager} = require('./NetworkManager'); | ||
const {NavigatorWatcher} = require('./NavigatorWatcher'); | ||
const {Dialog} = require('./Dialog'); | ||
@@ -83,5 +82,6 @@ const {EmulationManager} = require('./EmulationManager'); | ||
this._touchscreen = new Touchscreen(client, this._keyboard); | ||
this._networkManager = new NetworkManager(client); | ||
/** @type {!FrameManager} */ | ||
this._frameManager = new FrameManager(client, frameTree, this); | ||
this._networkManager = new NetworkManager(client, this._frameManager); | ||
this._frameManager = new FrameManager(client, frameTree, this, this._networkManager); | ||
this._networkManager.setFrameManager(this._frameManager); | ||
this._emulationManager = new EmulationManager(client); | ||
@@ -93,3 +93,2 @@ this._tracing = new Tracing(client); | ||
this._coverage = new Coverage(client); | ||
this._defaultNavigationTimeout = 30000; | ||
this._javascriptEnabled = true; | ||
@@ -260,3 +259,3 @@ /** @type {?Puppeteer.Viewport} */ | ||
setDefaultNavigationTimeout(timeout) { | ||
this._defaultNavigationTimeout = timeout; | ||
this._frameManager.setDefaultNavigationTimeout(timeout); | ||
} | ||
@@ -584,49 +583,3 @@ | ||
async goto(url, options = {}) { | ||
const referrer = typeof options.referer === 'string' ? options.referer : this._networkManager.extraHTTPHeaders()['referer']; | ||
/** @type {Map<string, !Puppeteer.Request>} */ | ||
const requests = new Map(); | ||
const eventListeners = [ | ||
helper.addEventListener(this._networkManager, NetworkManager.Events.Request, request => { | ||
if (!requests.get(request.url())) | ||
requests.set(request.url(), request); | ||
}) | ||
]; | ||
const mainFrame = this._frameManager.mainFrame(); | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options); | ||
let ensureNewDocumentNavigation = false; | ||
let error = await Promise.race([ | ||
navigate(this._client, url, referrer), | ||
watcher.timeoutPromise(), | ||
]); | ||
if (!error) { | ||
error = await Promise.race([ | ||
watcher.timeoutPromise(), | ||
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(), | ||
]); | ||
} | ||
watcher.dispose(); | ||
helper.removeEventListeners(eventListeners); | ||
if (error) | ||
throw error; | ||
const request = requests.get(mainFrame._navigationURL); | ||
return request ? request.response() : null; | ||
/** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {string} url | ||
* @param {string} referrer | ||
* @return {!Promise<?Error>} | ||
*/ | ||
async function navigate(client, url, referrer) { | ||
try { | ||
const response = await client.send('Page.navigate', {url, referrer}); | ||
ensureNewDocumentNavigation = !!response.loaderId; | ||
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null; | ||
} catch (error) { | ||
return error; | ||
} | ||
} | ||
return await this._frameManager.mainFrame().goto(url, options); | ||
} | ||
@@ -651,18 +604,3 @@ | ||
async waitForNavigation(options = {}) { | ||
const mainFrame = this._frameManager.mainFrame(); | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options); | ||
const responses = new Map(); | ||
const listener = helper.addEventListener(this._networkManager, NetworkManager.Events.Response, response => responses.set(response.url(), response)); | ||
const error = await Promise.race([ | ||
watcher.timeoutPromise(), | ||
watcher.sameDocumentNavigationPromise(), | ||
watcher.newDocumentNavigationPromise() | ||
]); | ||
watcher.dispose(); | ||
helper.removeEventListeners([listener]); | ||
if (error) | ||
throw error; | ||
return responses.get(this.mainFrame().url()) || null; | ||
return await this._frameManager.mainFrame().waitForNavigation(options); | ||
} | ||
@@ -867,3 +805,2 @@ | ||
if (options.fullPage) { | ||
assert(this._viewport, 'fullPage screenshots do not work without first setting viewport.'); | ||
const metrics = await this._client.send('Page.getLayoutMetrics'); | ||
@@ -875,17 +812,19 @@ const width = Math.ceil(metrics.contentSize.width); | ||
clip = { x: 0, y: 0, width, height, scale: 1 }; | ||
const mobile = this._viewport.isMobile || false; | ||
const deviceScaleFactor = this._viewport.deviceScaleFactor || 1; | ||
const landscape = this._viewport.isLandscape || false; | ||
const { | ||
isMobile = false, | ||
deviceScaleFactor = 1, | ||
isLandscape = false | ||
} = this._viewport || {}; | ||
/** @type {!Protocol.Emulation.ScreenOrientation} */ | ||
const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; | ||
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }); | ||
const screenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; | ||
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }); | ||
} | ||
if (options.omitBackground) | ||
const shouldSetDefaultBackground = options.omitBackground && format === 'png'; | ||
if (shouldSetDefaultBackground) | ||
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } }); | ||
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip }); | ||
if (options.omitBackground) | ||
if (shouldSetDefaultBackground) | ||
await this._client.send('Emulation.setDefaultBackgroundColorOverride'); | ||
if (options.fullPage) | ||
if (options.fullPage && this._viewport) | ||
await this.setViewport(this._viewport); | ||
@@ -1091,3 +1030,3 @@ | ||
/** @enum {string} */ | ||
/** @enum {!{width: number, height: number}} */ | ||
Page.PaperFormats = { | ||
@@ -1094,0 +1033,0 @@ letter: {width: 8.5, height: 11}, |
@@ -40,3 +40,3 @@ /** | ||
/** | ||
* @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean}} options | ||
* @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean, transport?: !Puppeteer.ConnectionTransport}} options | ||
* @return {!Promise<!Puppeteer.Browser>} | ||
@@ -43,0 +43,0 @@ */ |
@@ -21,2 +21,3 @@ /** | ||
const {TaskQueue} = require('./TaskQueue'); | ||
const {Connection} = require('./Connection'); | ||
@@ -49,5 +50,3 @@ class Browser extends EventEmitter { | ||
this._targets = new Map(); | ||
this._connection.setClosedCallback(() => { | ||
this.emit(Browser.Events.Disconnected); | ||
}); | ||
this._connection.on(Connection.Events.Disconnected, () => this.emit(Browser.Events.Disconnected)); | ||
this._connection.on('Target.targetCreated', this._targetCreated.bind(this)); | ||
@@ -374,2 +373,9 @@ this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); | ||
/** | ||
* @return {!Target} | ||
*/ | ||
target() { | ||
return this.targets().find(target => target.type() === 'browser'); | ||
} | ||
/** | ||
* @return {!Promise<!Array<!Puppeteer.Page>>} | ||
@@ -376,0 +382,0 @@ */ |
@@ -31,9 +31,38 @@ /** | ||
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com'; | ||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64']; | ||
const downloadURLs = { | ||
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip', | ||
mac: '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', | ||
win32: '%s/chromium-browser-snapshots/Win/%d/chrome-win32.zip', | ||
win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip', | ||
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', | ||
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', | ||
win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', | ||
win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', | ||
}; | ||
/** | ||
* @param {string} platform | ||
* @param {string} revision | ||
* @return {string} | ||
*/ | ||
function archiveName(platform, revision) { | ||
if (platform === 'linux') | ||
return 'chrome-linux'; | ||
if (platform === 'mac') | ||
return 'chrome-mac'; | ||
if (platform === 'win32' || platform === 'win64') { | ||
// Windows archive name changed at r591479. | ||
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; | ||
} | ||
return null; | ||
} | ||
/** | ||
* @param {string} platform | ||
* @param {string} host | ||
* @param {string} revision | ||
* @return {string} | ||
*/ | ||
function downloadURL(platform, host, revision) { | ||
return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision)); | ||
} | ||
const readdirAsync = helper.promisify(fs.readdir.bind(fs)); | ||
@@ -70,3 +99,2 @@ const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); | ||
} | ||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64']; | ||
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform); | ||
@@ -87,4 +115,3 @@ } | ||
canDownload(revision) { | ||
const url = util.format(downloadURLs[this._platform], this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
let resolve; | ||
@@ -134,4 +161,3 @@ const promise = new Promise(x => resolve = x); | ||
})(function*(){ | ||
let url = downloadURLs[this._platform]; | ||
url = util.format(url, this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`); | ||
@@ -236,11 +262,10 @@ const folderPath = this._getFolderPath(revision); | ||
if (this._platform === 'mac') | ||
executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); | ||
else if (this._platform === 'linux') | ||
executablePath = path.join(folderPath, 'chrome-linux', 'chrome'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome'); | ||
else if (this._platform === 'win32' || this._platform === 'win64') | ||
executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe'); | ||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe'); | ||
else | ||
throw new Error('Unsupported platform: ' + this._platform); | ||
let url = downloadURLs[this._platform]; | ||
url = util.format(url, this._downloadHost, revision); | ||
const url = downloadURL(this._platform, this._downloadHost, revision); | ||
const local = fs.existsSync(folderPath); | ||
@@ -271,3 +296,3 @@ return {revision, executablePath, folderPath, local, url}; | ||
const [platform, revision] = splits; | ||
if (!downloadURLs[platform]) | ||
if (!supportedPlatforms.includes(platform)) | ||
return null; | ||
@@ -274,0 +299,0 @@ return {platform, revision}; |
@@ -19,6 +19,3 @@ /** | ||
const debugSession = require('debug')('puppeteer:session'); | ||
const EventEmitter = require('events'); | ||
const WebSocket = require('ws'); | ||
const Pipe = require('./Pipe'); | ||
@@ -28,51 +25,2 @@ class Connection extends EventEmitter { | ||
* @param {string} url | ||
* @param {number=} delay | ||
* @return {!Promise<!Connection>} | ||
*/ | ||
static /* async */ createForWebSocket(url, delay = 0) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
return new Promise((resolve, reject) => { | ||
const ws = new WebSocket(url, { perMessageDeflate: false }); | ||
ws.on('open', () => resolve(new Connection(url, ws, delay))); | ||
ws.on('error', reject); | ||
}); | ||
});} | ||
/** | ||
* @param {!NodeJS.WritableStream} pipeWrite | ||
* @param {!NodeJS.ReadableStream} pipeRead | ||
* @param {number=} delay | ||
* @return {!Connection} | ||
*/ | ||
static createForPipe(pipeWrite, pipeRead, delay = 0) { | ||
return new Connection('', new Pipe(pipeWrite, pipeRead), delay); | ||
} | ||
/** | ||
* @param {string} url | ||
* @param {!Puppeteer.ConnectionTransport} transport | ||
@@ -90,9 +38,22 @@ * @param {number=} delay | ||
this._transport = transport; | ||
this._transport.on('message', this._onMessage.bind(this)); | ||
this._transport.on('close', this._onClose.bind(this)); | ||
this._transport.onmessage = this._onMessage.bind(this); | ||
this._transport.onclose = this._onClose.bind(this); | ||
/** @type {!Map<string, !CDPSession>}*/ | ||
this._sessions = new Map(); | ||
this._closed = false; | ||
} | ||
/** | ||
* @param {!CDPSession} session | ||
* @return {!Connection} | ||
*/ | ||
static fromSession(session) { | ||
let connection = session._connection; | ||
// TODO(lushnikov): move to flatten protocol to avoid this. | ||
while (connection instanceof CDPSession) | ||
connection = connection._connection; | ||
return connection; | ||
} | ||
/** | ||
* @return {string} | ||
@@ -120,9 +81,2 @@ */ | ||
/** | ||
* @param {function()} callback | ||
*/ | ||
setClosedCallback(callback) { | ||
this._closeCallback = callback; | ||
} | ||
/** | ||
* @param {string} message | ||
@@ -188,9 +142,7 @@ */ | ||
_onClose() { | ||
if (this._closeCallback) { | ||
this._closeCallback(); | ||
this._closeCallback = null; | ||
} | ||
this._transport.removeAllListeners(); | ||
// If transport throws any error at this point of time, we don't care and should swallow it. | ||
this._transport.on('error', () => {}); | ||
if (this._closed) | ||
return; | ||
this._closed = true; | ||
this._transport.onmessage = null; | ||
this._transport.onclose = null; | ||
for (const callback of this._callbacks.values()) | ||
@@ -202,2 +154,3 @@ callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); | ||
this._sessions.clear(); | ||
this.emit(Connection.Events.Disconnected); | ||
} | ||
@@ -248,2 +201,6 @@ | ||
Connection.Events = { | ||
Disconnected: Symbol('Connection.Events.Disconnected'), | ||
}; | ||
class CDPSession extends EventEmitter { | ||
@@ -250,0 +207,0 @@ /** |
@@ -22,2 +22,4 @@ /** | ||
const {TimeoutError} = require('./Errors'); | ||
const {NetworkManager} = require('./NetworkManager'); | ||
const {Connection} = require('./Connection'); | ||
@@ -31,7 +33,10 @@ const readFileAsync = helper.promisify(fs.readFile); | ||
* @param {!Puppeteer.Page} page | ||
* @param {!Puppeteer.NetworkManager} networkManager | ||
*/ | ||
constructor(client, frameTree, page) { | ||
constructor(client, frameTree, page, networkManager) { | ||
super(); | ||
this._client = client; | ||
this._page = page; | ||
this._networkManager = networkManager; | ||
this._defaultNavigationTimeout = 30000; | ||
/** @type {!Map<string, !Frame>} */ | ||
@@ -56,2 +61,151 @@ this._frames = new Map(); | ||
/** | ||
* @param {number} timeout | ||
*/ | ||
setDefaultNavigationTimeout(timeout) { | ||
this._defaultNavigationTimeout = timeout; | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {string} url | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
/* async */ navigateFrame(frame, url, options = {}) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
const referrer = typeof options.referer === 'string' ? options.referer : this._networkManager.extraHTTPHeaders()['referer']; | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options); | ||
let ensureNewDocumentNavigation = false; | ||
let error = (yield Promise.race([ | ||
navigate(this._client, url, referrer, frame._id), | ||
watcher.timeoutOrTerminationPromise(), | ||
])); | ||
if (!error) { | ||
error = (yield Promise.race([ | ||
watcher.timeoutOrTerminationPromise(), | ||
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(), | ||
])); | ||
} | ||
watcher.dispose(); | ||
if (error) | ||
throw error; | ||
return watcher.navigationResponse(); | ||
/** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {string} url | ||
* @param {string} referrer | ||
* @param {string} frameId | ||
* @return {!Promise<?Error>} | ||
*/ | ||
/* async */ function navigate(client, url, referrer, frameId) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
try { | ||
const response = (yield client.send('Page.navigate', {url, referrer, frameId})); | ||
ensureNewDocumentNavigation = !!response.loaderId; | ||
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null; | ||
} catch (error) { | ||
return error; | ||
} | ||
});} | ||
});} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
/* async */ waitForFrameNavigation(frame, options) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout; | ||
const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options); | ||
const error = (yield Promise.race([ | ||
watcher.timeoutOrTerminationPromise(), | ||
watcher.sameDocumentNavigationPromise(), | ||
watcher.newDocumentNavigationPromise() | ||
])); | ||
watcher.dispose(); | ||
if (error) | ||
throw error; | ||
return watcher.navigationResponse(); | ||
});} | ||
/** | ||
* @param {!Protocol.Page.lifecycleEventPayload} event | ||
@@ -326,2 +480,71 @@ */ | ||
/** | ||
* @param {string} url | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
/* async */ goto(url, options = {}) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
return (yield this._frameManager.navigateFrame(this, url, options)); | ||
});} | ||
/** | ||
* @param {!Object=} options | ||
* @return {!Promise<?Puppeteer.Response>} | ||
*/ | ||
/* async */ waitForNavigation(options = {}) {return (fn => { | ||
const gen = fn.call(this); | ||
return new Promise((resolve, reject) => { | ||
function step(key, arg) { | ||
let info, value; | ||
try { | ||
info = gen[key](arg); | ||
value = info.value; | ||
} catch (error) { | ||
reject(error); | ||
return; | ||
} | ||
if (info.done) { | ||
resolve(value); | ||
} else { | ||
return Promise.resolve(value).then( | ||
value => { | ||
step('next', value); | ||
}, | ||
err => { | ||
step('throw', err); | ||
}); | ||
} | ||
} | ||
return step('next'); | ||
}); | ||
})(function*(){ | ||
return (yield this._frameManager.waitForFrameNavigation(this, options)); | ||
});} | ||
/** | ||
* @return {!Promise<!ExecutionContext>} | ||
@@ -1249,3 +1472,3 @@ */ | ||
if (helper.isNumber(selectorOrFunctionOrTimeout)) | ||
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); | ||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout))); | ||
if (typeof selectorOrFunctionOrTimeout === 'function') | ||
@@ -1649,2 +1872,174 @@ return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args); | ||
class NavigatorWatcher { | ||
/** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {!FrameManager} frameManager | ||
* @param {!NetworkManager} networkManager | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {number} timeout | ||
* @param {!Object=} options | ||
*/ | ||
constructor(client, frameManager, networkManager, frame, timeout, options = {}) { | ||
assert(options.networkIdleTimeout === undefined, 'ERROR: networkIdleTimeout option is no longer supported.'); | ||
assert(options.networkIdleInflight === undefined, 'ERROR: networkIdleInflight option is no longer supported.'); | ||
assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'); | ||
let waitUntil = ['load']; | ||
if (Array.isArray(options.waitUntil)) | ||
waitUntil = options.waitUntil.slice(); | ||
else if (typeof options.waitUntil === 'string') | ||
waitUntil = [options.waitUntil]; | ||
this._expectedLifecycle = waitUntil.map(value => { | ||
const protocolEvent = puppeteerToProtocolLifecycle[value]; | ||
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value); | ||
return protocolEvent; | ||
}); | ||
this._frameManager = frameManager; | ||
this._networkManager = networkManager; | ||
this._frame = frame; | ||
this._initialLoaderId = frame._loaderId; | ||
this._timeout = timeout; | ||
/** @type {?Puppeteer.Request} */ | ||
this._navigationRequest = null; | ||
this._hasSameDocumentNavigation = false; | ||
this._eventListeners = [ | ||
helper.addEventListener(Connection.fromSession(client), Connection.Events.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.LifecycleEvent, this._checkLifecycleComplete.bind(this)), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)), | ||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameDetached, this._onFrameDetached.bind(this)), | ||
helper.addEventListener(this._networkManager, NetworkManager.Events.Request, this._onRequest.bind(this)), | ||
]; | ||
this._sameDocumentNavigationPromise = new Promise(fulfill => { | ||
this._sameDocumentNavigationCompleteCallback = fulfill; | ||
}); | ||
this._newDocumentNavigationPromise = new Promise(fulfill => { | ||
this._newDocumentNavigationCompleteCallback = fulfill; | ||
}); | ||
this._timeoutPromise = this._createTimeoutPromise(); | ||
this._terminationPromise = new Promise(fulfill => { | ||
this._terminationCallback = fulfill; | ||
}); | ||
} | ||
/** | ||
* @param {!Puppeteer.Request} request | ||
*/ | ||
_onRequest(request) { | ||
if (request.frame() !== this._frame || !request.isNavigationRequest()) | ||
return; | ||
this._navigationRequest = request; | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
*/ | ||
_onFrameDetached(frame) { | ||
if (this._frame === frame) { | ||
this._terminationCallback.call(null, new Error('Navigating frame was detached')); | ||
return; | ||
} | ||
this._checkLifecycleComplete(); | ||
} | ||
/** | ||
* @return {?Puppeteer.Response} | ||
*/ | ||
navigationResponse() { | ||
return this._navigationRequest ? this._navigationRequest.response() : null; | ||
} | ||
/** | ||
* @param {!Error} error | ||
*/ | ||
_terminate(error) { | ||
this._terminationCallback.call(null, error); | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
sameDocumentNavigationPromise() { | ||
return this._sameDocumentNavigationPromise; | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
newDocumentNavigationPromise() { | ||
return this._newDocumentNavigationPromise; | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
timeoutOrTerminationPromise() { | ||
return Promise.race([this._timeoutPromise, this._terminationPromise]); | ||
} | ||
/** | ||
* @return {!Promise<?Error>} | ||
*/ | ||
_createTimeoutPromise() { | ||
if (!this._timeout) | ||
return new Promise(() => {}); | ||
const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded'; | ||
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout)) | ||
.then(() => new TimeoutError(errorMessage)); | ||
} | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
*/ | ||
_navigatedWithinDocument(frame) { | ||
if (frame !== this._frame) | ||
return; | ||
this._hasSameDocumentNavigation = true; | ||
this._checkLifecycleComplete(); | ||
} | ||
_checkLifecycleComplete() { | ||
// We expect navigation to commit. | ||
if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation) | ||
return; | ||
if (!checkLifecycle(this._frame, this._expectedLifecycle)) | ||
return; | ||
if (this._hasSameDocumentNavigation) | ||
this._sameDocumentNavigationCompleteCallback(); | ||
if (this._frame._loaderId !== this._initialLoaderId) | ||
this._newDocumentNavigationCompleteCallback(); | ||
/** | ||
* @param {!Puppeteer.Frame} frame | ||
* @param {!Array<string>} expectedLifecycle | ||
* @return {boolean} | ||
*/ | ||
function checkLifecycle(frame, expectedLifecycle) { | ||
for (const event of expectedLifecycle) { | ||
if (!frame._lifecycleEvents.has(event)) | ||
return false; | ||
} | ||
for (const child of frame.childFrames()) { | ||
if (!checkLifecycle(child, expectedLifecycle)) | ||
return false; | ||
} | ||
return true; | ||
} | ||
} | ||
dispose() { | ||
helper.removeEventListeners(this._eventListeners); | ||
clearTimeout(this._maximumTimer); | ||
} | ||
} | ||
const puppeteerToProtocolLifecycle = { | ||
'load': 'load', | ||
'domcontentloaded': 'DOMContentLoaded', | ||
'networkidle0': 'networkIdle', | ||
'networkidle2': 'networkAlmostIdle', | ||
}; | ||
module.exports = {FrameManager, Frame}; |
@@ -22,2 +22,35 @@ /** | ||
/** | ||
* @param {!Object} classType | ||
* @param {string=} publicName | ||
*/ | ||
function traceAPICoverage(classType, publicName) { | ||
if (!apiCoverage) | ||
return; | ||
let className = publicName || classType.prototype.constructor.name; | ||
className = className.substring(0, 1).toLowerCase() + className.substring(1); | ||
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') | ||
continue; | ||
apiCoverage.set(`${className}.${methodName}`, false); | ||
Reflect.set(classType.prototype, methodName, function(...args) { | ||
apiCoverage.set(`${className}.${methodName}`, true); | ||
return method.call(this, ...args); | ||
}); | ||
} | ||
if (classType.Events) { | ||
for (const event of Object.values(classType.Events)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); | ||
const method = Reflect.get(classType.prototype, 'emit'); | ||
Reflect.set(classType.prototype, 'emit', function(event, ...args) { | ||
if (this.listenerCount(event)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); | ||
return method.call(this, event, ...args); | ||
}); | ||
} | ||
} | ||
class Helper { | ||
@@ -133,63 +166,19 @@ /** | ||
static tracePublicAPI(classType, publicName) { | ||
let className = publicName || classType.prototype.constructor.name; | ||
className = className.substring(0, 1).toLowerCase() + className.substring(1); | ||
const debug = require('debug')(`puppeteer:${className}`); | ||
if (!debug.enabled && !apiCoverage) | ||
return; | ||
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') | ||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction') | ||
continue; | ||
if (apiCoverage) | ||
apiCoverage.set(`${className}.${methodName}`, false); | ||
Reflect.set(classType.prototype, methodName, function(...args) { | ||
const argsText = args.map(stringifyArgument).join(', '); | ||
const callsite = `${className}.${methodName}(${argsText})`; | ||
if (debug.enabled) | ||
debug(callsite); | ||
if (apiCoverage) | ||
apiCoverage.set(`${className}.${methodName}`, true); | ||
return method.call(this, ...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; | ||
}); | ||
}); | ||
} | ||
if (classType.Events) { | ||
if (apiCoverage) { | ||
for (const event of Object.values(classType.Events)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); | ||
} | ||
const method = Reflect.get(classType.prototype, 'emit'); | ||
Reflect.set(classType.prototype, 'emit', function(event, ...args) { | ||
const argsText = [JSON.stringify(event)].concat(args.map(stringifyArgument)).join(', '); | ||
if (debug.enabled && this.listenerCount(event)) | ||
debug(`${className}.emit(${argsText})`); | ||
if (apiCoverage && this.listenerCount(event)) | ||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); | ||
return method.call(this, event, ...args); | ||
}); | ||
} | ||
/** | ||
* @param {!Object} arg | ||
* @return {string} | ||
*/ | ||
function stringifyArgument(arg) { | ||
if (Helper.isString(arg) || Helper.isNumber(arg) || !arg) | ||
return JSON.stringify(arg); | ||
if (typeof arg === 'function') { | ||
let text = arg.toString().split('\n').map(line => line.trim()).join(''); | ||
if (text.length > 20) | ||
text = text.substring(0, 20) + '…'; | ||
return `"${text}"`; | ||
} | ||
const state = {}; | ||
const keys = Object.keys(arg); | ||
for (const key of keys) { | ||
const value = arg[key]; | ||
if (Helper.isString(value) || Helper.isNumber(value)) | ||
state[key] = JSON.stringify(value); | ||
} | ||
const name = arg.constructor.name === 'Object' ? '' : arg.constructor.name; | ||
return name + JSON.stringify(state); | ||
} | ||
traceAPICoverage(classType, publicName); | ||
} | ||
@@ -199,5 +188,5 @@ | ||
* @param {!NodeJS.EventEmitter} emitter | ||
* @param {string} eventName | ||
* @param {(string|symbol)} eventName | ||
* @param {function(?)} handler | ||
* @return {{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}} | ||
* @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}} | ||
*/ | ||
@@ -210,3 +199,3 @@ static addEventListener(emitter, eventName, handler) { | ||
/** | ||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}>} listeners | ||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}>} listeners | ||
*/ | ||
@@ -213,0 +202,0 @@ static removeEventListeners(listeners) { |
@@ -27,2 +27,4 @@ /** | ||
const {TimeoutError} = require('./Errors'); | ||
const WebSocketTransport = require('./WebSocketTransport'); | ||
const PipeTransport = require('./PipeTransport'); | ||
@@ -142,2 +144,3 @@ const mkdtempAsync = helper.promisify(fs.mkdtemp); | ||
const usePipe = chromeArguments.includes('--remote-debugging-pipe'); | ||
/** @type {!Array<"ignore"|"pipe">} */ | ||
const stdio = usePipe ? ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']; | ||
@@ -189,5 +192,7 @@ const chromeProcess = childProcess.spawn( | ||
const browserWSEndpoint = (yield waitForWSEndpoint(chromeProcess, timeout, this._preferredRevision)); | ||
connection = (yield Connection.createForWebSocket(browserWSEndpoint, slowMo)); | ||
const transport = (yield WebSocketTransport.create(browserWSEndpoint)); | ||
connection = new Connection(browserWSEndpoint, transport, slowMo); | ||
} else { | ||
connection = Connection.createForPipe(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]), slowMo); | ||
const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4])); | ||
connection = new Connection('', transport, slowMo); | ||
} | ||
@@ -324,3 +329,3 @@ const browser = (yield Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome)); | ||
/** | ||
* @param {!(BrowserOptions & {browserWSEndpoint: string})} options | ||
* @param {!(BrowserOptions & {browserWSEndpoint: string, transport?: !Puppeteer.ConnectionTransport})} options | ||
* @return {!Promise<!Browser>} | ||
@@ -359,5 +364,6 @@ */ | ||
defaultViewport = {width: 800, height: 600}, | ||
transport = (yield WebSocketTransport.create(browserWSEndpoint)), | ||
slowMo = 0, | ||
} = options; | ||
const connection = (yield Connection.createForWebSocket(browserWSEndpoint, slowMo)); | ||
const connection = new Connection(browserWSEndpoint, transport, slowMo); | ||
const {browserContextIds} = (yield connection.send('Target.getBrowserContexts')); | ||
@@ -364,0 +370,0 @@ return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError)); |
@@ -23,8 +23,7 @@ /** | ||
* @param {!Puppeteer.CDPSession} client | ||
* @param {!Puppeteer.FrameManager} frameManager | ||
*/ | ||
constructor(client, frameManager) { | ||
constructor(client) { | ||
super(); | ||
this._client = client; | ||
this._frameManager = frameManager; | ||
this._frameManager = null; | ||
/** @type {!Map<string, !Request>} */ | ||
@@ -59,2 +58,9 @@ this._requestIdToRequest = new Map(); | ||
/** | ||
* @param {!Puppeteer.FrameManager} frameManager | ||
*/ | ||
setFrameManager(frameManager) { | ||
this._frameManager = frameManager; | ||
} | ||
/** | ||
* @param {?{username: string, password: string}} credentials | ||
@@ -357,3 +363,3 @@ */ | ||
} | ||
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null; | ||
const frame = event.frameId && this._frameManager ? this._frameManager.frame(event.frameId) : null; | ||
const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain); | ||
@@ -411,3 +417,7 @@ this._requestIdToRequest.set(event.requestId, request); | ||
return; | ||
request.response()._bodyLoadedPromiseFulfill.call(null); | ||
// Under certain conditions we never get the Network.responseReceived | ||
// event from protocol. @see https://crbug.com/883475 | ||
if (request.response()) | ||
request.response()._bodyLoadedPromiseFulfill.call(null); | ||
this._requestIdToRequest.delete(request._requestId); | ||
@@ -635,6 +645,4 @@ this._attemptedAuthentications.delete(request._interceptionId); | ||
responseHeaders['content-type'] = response.contentType; | ||
if (responseBody && !('content-length' in responseHeaders)) { | ||
// @ts-ignore | ||
if (responseBody && !('content-length' in responseHeaders)) | ||
responseHeaders['content-length'] = Buffer.byteLength(responseBody); | ||
} | ||
@@ -939,2 +947,9 @@ const statusCode = response.status || 200; | ||
} | ||
/** | ||
* @return {?Puppeteer.Frame} | ||
*/ | ||
frame() { | ||
return this._request.frame(); | ||
} | ||
} | ||
@@ -941,0 +956,0 @@ helper.tracePublicAPI(Response); |
@@ -40,3 +40,3 @@ /** | ||
/** | ||
* @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean}} options | ||
* @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean, transport?: !Puppeteer.ConnectionTransport}} options | ||
* @return {!Promise<!Puppeteer.Browser>} | ||
@@ -43,0 +43,0 @@ */ |
{ | ||
"name": "puppeteer-core", | ||
"version": "1.8.0", | ||
"version": "1.9.0", | ||
"description": "A high-level API to control headless Chrome over the DevTools Protocol", | ||
@@ -11,3 +11,3 @@ "main": "index.js", | ||
"puppeteer": { | ||
"chromium_revision": "588429" | ||
"chromium_revision": "594312" | ||
}, | ||
@@ -28,3 +28,5 @@ "scripts": { | ||
"prepublishOnly": "npm run build", | ||
"apply-next-version": "node utils/apply_next_version.js" | ||
"apply-next-version": "node utils/apply_next_version.js", | ||
"bundle": "npx browserify -r ./index.js:puppeteer -o utils/browser/puppeteer-web.js", | ||
"unit-bundle": "node utils/browser/test.js" | ||
}, | ||
@@ -47,3 +49,3 @@ "author": "The Chromium Authors", | ||
"@types/mime": "^2.0.0", | ||
"@types/node": "^8.0.26", | ||
"@types/node": "^8.10.34", | ||
"@types/rimraf": "^2.0.2", | ||
@@ -55,2 +57,3 @@ "@types/ws": "^3.0.2", | ||
"esprima": "^4.0.0", | ||
"jpeg-js": "^0.3.4", | ||
"minimist": "^1.2.0", | ||
@@ -61,4 +64,13 @@ "ncp": "^2.0.0", | ||
"text-diff": "^1.0.1", | ||
"typescript": "^3.0.1" | ||
"typescript": "^3.1.1" | ||
}, | ||
"browser": { | ||
"./lib/BrowserFetcher.js": false, | ||
"./node6/lib/Puppeteer": false, | ||
"ws": "./utils/browser/WebSocket", | ||
"fs": false, | ||
"child_process": false, | ||
"rimraf": false, | ||
"readline": false | ||
} | ||
} |
@@ -9,3 +9,3 @@ # Puppeteer | ||
###### [API](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/GoogleChrome/puppeteer/blob/master/CONTRIBUTING.md) | ||
###### [API](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/GoogleChrome/puppeteer/blob/master/CONTRIBUTING.md) | [Troubleshooting](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md) | ||
@@ -41,3 +41,3 @@ > Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. | ||
Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, see [Environment variables](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#environment-variables). | ||
Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, see [Environment variables](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#environment-variables). | ||
@@ -52,2 +52,3 @@ | ||
npm i puppeteer-core | ||
# or "yarn add puppeteer-core" | ||
``` | ||
@@ -64,3 +65,3 @@ | ||
Puppeteer will be familiar to people using other browser testing frameworks. You create an instance | ||
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#). | ||
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#). | ||
@@ -90,3 +91,3 @@ **Example** - navigating to https://example.com and saving a screenshot as *example.png*: | ||
Puppeteer sets an initial page size to 800px x 600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#pagesetviewportviewport). | ||
Puppeteer sets an initial page size to 800px x 600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#pagesetviewportviewport). | ||
@@ -116,3 +117,3 @@ **Example** - create a PDF. | ||
See [`Page.pdf()`](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. | ||
See [`Page.pdf()`](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. | ||
@@ -152,3 +153,3 @@ **Example** - evaluate script in the context of the page | ||
See [`Page.evaluate()`](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. | ||
See [`Page.evaluate()`](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. | ||
@@ -162,3 +163,3 @@ <!-- [END getstarted] --> | ||
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the ['headless' option](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: | ||
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the ['headless' option](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: | ||
@@ -179,3 +180,3 @@ ```js | ||
See [`Puppeteer.launch()`](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#puppeteerlaunchoptions) for more information. | ||
See [`Puppeteer.launch()`](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#puppeteerlaunchoptions) for more information. | ||
@@ -192,6 +193,7 @@ See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. | ||
- [API Documentation](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md) | ||
- [API Documentation](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md) | ||
- [Examples](https://github.com/GoogleChrome/puppeteer/tree/master/examples/) | ||
- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) | ||
<!-- [START debugging] --> | ||
@@ -242,3 +244,3 @@ | ||
5. Enable verbose logging - All public API calls and internal protocol traffic | ||
5. Enable verbose logging - internal DevTools protocol traffic | ||
will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. | ||
@@ -250,9 +252,21 @@ | ||
# Debug output can be enabled/disabled by namespace | ||
env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # everything BUT protocol messages | ||
env DEBUG="puppeteer:protocol" node script.js # protocol connection messages | ||
env DEBUG="puppeteer:session" node script.js # protocol session messages (protocol messages to targets) | ||
env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # only Mouse and Keyboard API calls | ||
# Protocol traffic can be rather noisy. This example filters out all Network domain messages | ||
env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' | ||
env DEBUG="puppeteer:session" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' | ||
6. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) | ||
- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) | ||
- add a `debugger` to your Puppeteer (node) code | ||
- add `ndb` (or `npx ndb`) before your test command. For example: | ||
`ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) | ||
- debug your test inside chromium like a boss! | ||
<!-- [END debugging] --> | ||
@@ -347,3 +361,3 @@ | ||
* Puppeteer is bundled with Chromium--not Chrome--and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) | ||
* Puppeteer is bundled with Chromium--not Chrome--and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/GoogleChrome/puppeteer/blob/v1.9.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) | ||
* Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). | ||
@@ -350,0 +364,0 @@ |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
622641
18480
376
17