playwright-core
Advanced tools
Comparing version 0.12.1 to 0.13.0-post-next.1586393147597
@@ -24,2 +24,4 @@ "use strict"; | ||
exports.Dialog = dialog_1.Dialog; | ||
var download_1 = require("./download"); | ||
exports.Download = download_1.Download; | ||
var dom_1 = require("./dom"); | ||
@@ -26,0 +28,0 @@ exports.ElementHandle = dom_1.ElementHandle; |
@@ -18,9 +18,44 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
async function createPageInNewContext(browser, options) { | ||
const context = await browser.newContext(options); | ||
const page = await context.newPage(); | ||
page._ownedContext = context; | ||
return page; | ||
const events_1 = require("events"); | ||
const download_1 = require("./download"); | ||
const transport_1 = require("./transport"); | ||
const events_2 = require("./events"); | ||
class BrowserBase extends events_1.EventEmitter { | ||
constructor() { | ||
super(...arguments); | ||
this._downloadsPath = ''; | ||
this._downloads = new Map(); | ||
this._debugProtocol = transport_1.debugProtocol; | ||
this._ownedServer = null; | ||
} | ||
async newPage(options) { | ||
const context = await this.newContext(options); | ||
const page = await context.newPage(); | ||
page._ownedContext = context; | ||
return page; | ||
} | ||
_downloadCreated(page, uuid, url) { | ||
const download = new download_1.Download(page, this._downloadsPath, uuid, url); | ||
this._downloads.set(uuid, download); | ||
} | ||
_downloadFinished(uuid, error) { | ||
const download = this._downloads.get(uuid); | ||
if (!download) | ||
return; | ||
download._reportFinished(error); | ||
this._downloads.delete(uuid); | ||
} | ||
async close() { | ||
if (this._ownedServer) { | ||
await this._ownedServer.close(); | ||
} | ||
else { | ||
await Promise.all(this.contexts().map(context => context.close())); | ||
this._disconnect(); | ||
} | ||
if (this.isConnected()) | ||
await new Promise(x => this.once(events_2.Events.Browser.Disconnected, x)); | ||
} | ||
} | ||
exports.createPageInNewContext = createPageInNewContext; | ||
exports.BrowserBase = BrowserBase; | ||
//# sourceMappingURL=browser.js.map |
@@ -21,6 +21,6 @@ "use strict"; | ||
const network = require("./network"); | ||
const platform = require("./platform"); | ||
const timeoutSettings_1 = require("./timeoutSettings"); | ||
const events_1 = require("./events"); | ||
class BrowserContextBase extends platform.EventEmitter { | ||
const extendedEventEmitter_1 = require("./extendedEventEmitter"); | ||
class BrowserContextBase extends extendedEventEmitter_1.ExtendedEventEmitter { | ||
constructor(options) { | ||
@@ -33,14 +33,24 @@ super(); | ||
this._permissions = new Map(); | ||
this._downloads = new Set(); | ||
this._options = options; | ||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); | ||
} | ||
_abortPromiseForEvent(event) { | ||
return event === events_1.Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise; | ||
} | ||
_computeDeadline(options) { | ||
return this._timeoutSettings.computeDeadline(options); | ||
} | ||
_browserClosed() { | ||
for (const page of this.pages()) | ||
page._didClose(); | ||
this._didCloseInternal(); | ||
this._didCloseInternal(true); | ||
} | ||
_didCloseInternal() { | ||
async _didCloseInternal(omitDeleteDownloads = false) { | ||
this._closed = true; | ||
this.emit(events_1.Events.BrowserContext.Close); | ||
this._closePromiseFulfill(new Error('Context closed')); | ||
if (!omitDeleteDownloads) | ||
await Promise.all([...this._downloads].map(d => d.delete())); | ||
this._downloads.clear(); | ||
} | ||
@@ -69,11 +79,2 @@ async grantPermissions(permissions, options) { | ||
} | ||
async waitForEvent(event, optionsOrPredicate) { | ||
if (!optionsOrPredicate) | ||
optionsOrPredicate = {}; | ||
if (typeof optionsOrPredicate === 'function') | ||
optionsOrPredicate = { predicate: optionsOrPredicate }; | ||
const { timeout = this._timeoutSettings.timeout(), predicate = () => true } = optionsOrPredicate; | ||
const abortPromise = (event === events_1.Events.BrowserContext.Close) ? new Promise(() => { }) : this._closePromise; | ||
return helper_1.helper.waitForEvent(this, event, (...args) => !!predicate(...args), timeout, abortPromise); | ||
} | ||
} | ||
@@ -80,0 +81,0 @@ exports.BrowserContextBase = BrowserContextBase; |
@@ -25,3 +25,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
@@ -33,3 +32,3 @@ const crConnection_1 = require("./crConnection"); | ||
const crExecutionContext_1 = require("./crExecutionContext"); | ||
class CRBrowser extends platform.EventEmitter { | ||
class CRBrowser extends browser_1.BrowserBase { | ||
constructor(connection) { | ||
@@ -61,21 +60,29 @@ super(); | ||
const session = connection.rootSession; | ||
const promises = [ | ||
session.send('Target.setDiscoverTargets', { discover: true }), | ||
session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), | ||
session.send('Target.setDiscoverTargets', { discover: false }), | ||
]; | ||
const existingPageAttachPromises = []; | ||
if (isPersistent) { | ||
// First page and background pages in the persistent context are created automatically | ||
// and may be initialized before we enable auto-attach. | ||
function attachToExistingPage({ targetInfo }) { | ||
if (targetInfo.type !== 'page' && targetInfo.type !== 'background_page') | ||
return; | ||
existingPageAttachPromises.push(session.send('Target.attachToTarget', { targetId: targetInfo.targetId, flatten: true })); | ||
} | ||
session.on('Target.targetCreated', attachToExistingPage); | ||
Promise.all(promises).then(() => session.off('Target.targetCreated', attachToExistingPage)).catch(helper_1.debugError); | ||
if (!isPersistent) { | ||
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); | ||
return browser; | ||
} | ||
await Promise.all(promises); | ||
await Promise.all(existingPageAttachPromises); | ||
const existingTargetAttachPromises = []; | ||
// First page, background pages and their service workers in the persistent context | ||
// are created automatically and may be initialized before we enable auto-attach. | ||
function attachToExistingPage({ targetInfo }) { | ||
if (targetInfo.type !== 'page' && targetInfo.type !== 'background_page' && targetInfo.type !== 'service_worker') | ||
return; | ||
// TODO: should we handle the error during 'Target.attachToTarget'? Can the target disappear? | ||
existingTargetAttachPromises.push(session.send('Target.attachToTarget', { targetId: targetInfo.targetId, flatten: true })); | ||
} | ||
session.on('Target.targetCreated', attachToExistingPage); | ||
const startDiscover = session.send('Target.setDiscoverTargets', { discover: true }); | ||
const autoAttachAndStopDiscover = session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }).then(() => { | ||
// All targets collected before setAutoAttach response will not be auto-attached, the rest will be. | ||
// TODO: We should fix this upstream and remove this tricky logic. | ||
session.off('Target.targetCreated', attachToExistingPage); | ||
return session.send('Target.setDiscoverTargets', { discover: false }); | ||
}); | ||
await Promise.all([ | ||
startDiscover, | ||
autoAttachAndStopDiscover, | ||
]); | ||
// Wait for initial targets to arrive. | ||
await Promise.all(existingTargetAttachPromises); | ||
return browser; | ||
@@ -94,5 +101,2 @@ } | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }) { | ||
@@ -167,7 +171,4 @@ const session = this._connection.session(sessionId); | ||
} | ||
async close() { | ||
const disconnected = new Promise(f => this._connection.once(crConnection_1.ConnectionEvents.Disconnected, f)); | ||
await Promise.all(this.contexts().map(context => context.close())); | ||
_disconnect() { | ||
this._connection.close(); | ||
await disconnected; | ||
} | ||
@@ -179,3 +180,3 @@ async newBrowserCDPSession() { | ||
helper_1.assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.'); | ||
this._tracingClient = page ? page._delegate._client : this._session; | ||
this._tracingClient = page ? page._delegate._mainFrameSession._client : this._session; | ||
const defaultCategories = [ | ||
@@ -199,10 +200,9 @@ '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', | ||
helper_1.assert(this._tracingClient, 'Tracing was not started.'); | ||
let fulfill; | ||
const contentPromise = new Promise(x => fulfill = x); | ||
this._tracingClient.once('Tracing.tracingComplete', event => { | ||
crProtocolHelper_1.readProtocolStream(this._tracingClient, event.stream, this._tracingPath).then(fulfill); | ||
}); | ||
await this._tracingClient.send('Tracing.end'); | ||
const [event] = await Promise.all([ | ||
new Promise(f => this._tracingClient.once('Tracing.tracingComplete', f)), | ||
this._tracingClient.send('Tracing.end') | ||
]); | ||
const result = await crProtocolHelper_1.readProtocolStream(this._tracingClient, event.stream, this._tracingPath); | ||
this._tracingRecording = false; | ||
return contentPromise; | ||
return result; | ||
} | ||
@@ -217,5 +217,2 @@ isConnected() { | ||
} | ||
_setDebugFunction(debugFunction) { | ||
this._connection._debugProtocol = debugFunction; | ||
} | ||
} | ||
@@ -243,8 +240,16 @@ exports.CRBrowser = CRBrowser; | ||
async _initialize() { | ||
const promises = [ | ||
this._browser._session.send('Browser.setDownloadBehavior', { | ||
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny', | ||
browserContextId: this._browserContextId || undefined, | ||
downloadPath: this._browser._downloadsPath | ||
}) | ||
]; | ||
if (this._options.permissions) | ||
await this.grantPermissions(this._options.permissions); | ||
promises.push(this.grantPermissions(this._options.permissions)); | ||
if (this._options.offline) | ||
await this.setOffline(this._options.offline); | ||
promises.push(this.setOffline(this._options.offline)); | ||
if (this._options.httpCredentials) | ||
await this.setHTTPCredentials(this._options.httpCredentials); | ||
promises.push(this.setHTTPCredentials(this._options.httpCredentials)); | ||
await Promise.all(promises); | ||
} | ||
@@ -282,2 +287,9 @@ pages() { | ||
async addCookies(cookies) { | ||
cookies = cookies.map(c => { | ||
const copy = { ...c }; | ||
// Working around setter issue in Chrome. Cookies are now None by default. | ||
if (copy.sameSite === 'None') | ||
delete copy.sameSite; | ||
return copy; | ||
}); | ||
await this._browser._session.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined }); | ||
@@ -323,3 +335,3 @@ } | ||
for (const page of this.pages()) | ||
await page._delegate._client.send('Emulation.setGeolocationOverride', geolocation || {}); | ||
await page._delegate.updateGeolocation(); | ||
} | ||
@@ -334,3 +346,3 @@ async setExtraHTTPHeaders(headers) { | ||
for (const page of this.pages()) | ||
await page._delegate._networkManager.setOffline(offline); | ||
await page._delegate.updateOffline(); | ||
} | ||
@@ -340,3 +352,3 @@ async setHTTPCredentials(httpCredentials) { | ||
for (const page of this.pages()) | ||
await page._delegate._networkManager.authenticate(httpCredentials); | ||
await page._delegate.updateHttpCredentials(); | ||
} | ||
@@ -377,3 +389,3 @@ async addInitScript(script, arg) { | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -380,0 +392,0 @@ backgroundPages() { |
@@ -20,3 +20,4 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
const events_1 = require("events"); | ||
exports.ConnectionEvents = { | ||
@@ -28,3 +29,3 @@ Disconnected: Symbol('ConnectionEvents.Disconnected') | ||
exports.kBrowserCloseMessageId = -9999; | ||
class CRConnection extends platform.EventEmitter { | ||
class CRConnection extends events_1.EventEmitter { | ||
constructor(transport) { | ||
@@ -40,4 +41,2 @@ super(); | ||
this._sessions.set('', this.rootSession); | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._debugProtocol.color = '34'; | ||
} | ||
@@ -50,33 +49,33 @@ static fromSession(session) { | ||
} | ||
_rawSend(sessionId, message) { | ||
_rawSend(sessionId, method, params) { | ||
const id = ++this._lastId; | ||
message.id = id; | ||
const message = { id, method, params }; | ||
if (sessionId) | ||
message.sessionId = sessionId; | ||
const data = JSON.stringify(message); | ||
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); | ||
this._transport.send(data); | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); | ||
this._transport.send(message); | ||
return id; | ||
} | ||
async _onMessage(message) { | ||
this._debugProtocol('◀ RECV ' + message); | ||
const object = JSON.parse(message); | ||
if (object.id === exports.kBrowserCloseMessageId) | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('◀ RECV ' + JSON.stringify(message)); | ||
if (message.id === exports.kBrowserCloseMessageId) | ||
return; | ||
if (object.method === 'Target.attachedToTarget') { | ||
const sessionId = object.params.sessionId; | ||
const rootSessionId = object.sessionId || ''; | ||
const session = new CRSession(this, rootSessionId, object.params.targetInfo.type, sessionId); | ||
if (message.method === 'Target.attachedToTarget') { | ||
const sessionId = message.params.sessionId; | ||
const rootSessionId = message.sessionId || ''; | ||
const session = new CRSession(this, rootSessionId, message.params.targetInfo.type, sessionId); | ||
this._sessions.set(sessionId, session); | ||
} | ||
else if (object.method === 'Target.detachedFromTarget') { | ||
const session = this._sessions.get(object.params.sessionId); | ||
else if (message.method === 'Target.detachedFromTarget') { | ||
const session = this._sessions.get(message.params.sessionId); | ||
if (session) { | ||
session._onClosed(); | ||
this._sessions.delete(object.params.sessionId); | ||
this._sessions.delete(message.params.sessionId); | ||
} | ||
} | ||
const session = this._sessions.get(object.sessionId || ''); | ||
const session = this._sessions.get(message.sessionId || ''); | ||
if (session) | ||
session._onMessage(object); | ||
session._onMessage(message); | ||
} | ||
@@ -109,3 +108,3 @@ _onClose() { | ||
}; | ||
class CRSession extends platform.EventEmitter { | ||
class CRSession extends events_1.EventEmitter { | ||
constructor(connection, rootSessionId, targetType, sessionId) { | ||
@@ -127,3 +126,3 @@ super(); | ||
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); | ||
const id = this._connection._rawSend(this._sessionId, { method, params }); | ||
const id = this._connection._rawSend(this._sessionId, method, params); | ||
return new Promise((resolve, reject) => { | ||
@@ -138,3 +137,3 @@ this._callbacks.set(id, { resolve, reject, error: new Error(), method }); | ||
if (object.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, object)); | ||
callback.reject(createProtocolError(callback.error, callback.method, object.error)); | ||
else | ||
@@ -165,6 +164,6 @@ callback.resolve(object.result); | ||
exports.CRSession = CRSession; | ||
function createProtocolError(error, method, object) { | ||
let message = `Protocol error (${method}): ${object.error.message}`; | ||
if ('data' in object.error) | ||
message += ` ${object.error.data}`; | ||
function createProtocolError(error, method, protocolError) { | ||
let message = `Protocol error (${method}): ${protocolError.message}`; | ||
if ('data' in protocolError) | ||
message += ` ${protocolError.data}`; | ||
return rewriteError(error, message); | ||
@@ -181,3 +180,4 @@ } | ||
return `{"id":${message.id} [evaluate injected script]}`; | ||
return JSON.stringify(message); | ||
} | ||
//# sourceMappingURL=crConnection.js.map |
@@ -21,3 +21,2 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
class CRNetworkManager { | ||
@@ -174,3 +173,3 @@ constructor(client, page) { | ||
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); | ||
return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); | ||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); | ||
}; | ||
@@ -250,3 +249,3 @@ return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); | ||
async fulfill(response) { | ||
const responseBody = response.body && helper_1.helper.isString(response.body) ? platform.Buffer.from(response.body) : (response.body || null); | ||
const responseBody = response.body && helper_1.helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null); | ||
const responseHeaders = {}; | ||
@@ -260,3 +259,3 @@ if (response.headers) { | ||
if (responseBody && !('content-length' in responseHeaders)) | ||
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody)); | ||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); | ||
await this._client.send('Fetch.fulfillRequest', { | ||
@@ -263,0 +262,0 @@ requestId: this._interceptionId, |
@@ -34,11 +34,7 @@ "use strict"; | ||
const console_1 = require("../console"); | ||
const platform = require("../platform"); | ||
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; | ||
class CRPage { | ||
constructor(client, targetId, browserContext, opener) { | ||
this._contextIdToContext = new Map(); | ||
this._eventListeners = []; | ||
this._firstNonInitialNavigationCommittedCallback = () => { }; | ||
this._sessions = new Map(); | ||
this._initializedPage = null; | ||
this._client = client; | ||
this._targetId = targetId; | ||
@@ -52,37 +48,255 @@ this._opener = opener; | ||
this._page = new page_1.Page(this, browserContext); | ||
this._networkManager = new crNetworkManager_1.CRNetworkManager(client, this._page); | ||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); | ||
this._mainFrameSession = new FrameSession(this, client, targetId); | ||
this._sessions.set(targetId, this._mainFrameSession); | ||
client.once(crConnection_1.CRSessionEvents.Disconnected, () => this._page._didDisconnect()); | ||
this._pagePromise = this._initialize().then(() => this._initializedPage = this._page).catch(e => e); | ||
this._pagePromise = this._mainFrameSession._initialize().then(() => this._initializedPage = this._page).catch(e => e); | ||
} | ||
async _forAllFrameSessions(cb) { | ||
await Promise.all(Array.from(this._sessions.values()).map(frame => cb(frame))); | ||
} | ||
_sessionForFrame(frame) { | ||
// Frame id equals target id. | ||
while (!this._sessions.has(frame._id)) { | ||
const parent = frame.parentFrame(); | ||
if (!parent) | ||
throw new Error(`Frame has been detached.`); | ||
frame = parent; | ||
} | ||
return this._sessions.get(frame._id); | ||
} | ||
_sessionForHandle(handle) { | ||
const frame = handle._context.frame; | ||
return this._sessionForFrame(frame); | ||
} | ||
addFrameSession(targetId, session) { | ||
// Frame id equals target id. | ||
const frame = this._page._frameManager.frame(targetId); | ||
helper_1.assert(frame); | ||
this._page._frameManager.removeChildFramesRecursively(frame); | ||
const frameSession = new FrameSession(this, session, targetId); | ||
this._sessions.set(targetId, frameSession); | ||
frameSession._initialize().catch(e => e); | ||
} | ||
removeFrameSession(targetId) { | ||
const frameSession = this._sessions.get(targetId); | ||
if (!frameSession) | ||
return; | ||
// Frame id equals target id. | ||
const frame = this._page._frameManager.frame(targetId); | ||
helper_1.assert(frame); | ||
this._page._frameManager.removeChildFramesRecursively(frame); | ||
frameSession.dispose(); | ||
this._sessions.delete(targetId); | ||
} | ||
async pageOrError() { | ||
return this._pagePromise; | ||
} | ||
didClose() { | ||
for (const session of this._sessions.values()) | ||
session.dispose(); | ||
this._page._didClose(); | ||
} | ||
async navigateFrame(frame, url, referrer) { | ||
return this._sessionForFrame(frame)._navigate(frame, url, referrer); | ||
} | ||
async exposeBinding(binding) { | ||
await this._forAllFrameSessions(frame => frame._initBinding(binding)); | ||
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(helper_1.debugError))); | ||
} | ||
async updateExtraHTTPHeaders() { | ||
await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders()); | ||
} | ||
async updateGeolocation() { | ||
await this._forAllFrameSessions(frame => frame._updateGeolocation()); | ||
} | ||
async updateOffline() { | ||
await this._forAllFrameSessions(frame => frame._updateOffline()); | ||
} | ||
async updateHttpCredentials() { | ||
await this._forAllFrameSessions(frame => frame._updateHttpCredentials()); | ||
} | ||
async setViewportSize(viewportSize) { | ||
helper_1.assert(this._page._state.viewportSize === viewportSize); | ||
await this._mainFrameSession._updateViewport(); | ||
} | ||
async updateEmulateMedia() { | ||
await this._forAllFrameSessions(frame => frame._updateEmulateMedia()); | ||
} | ||
async updateRequestInterception() { | ||
await this._forAllFrameSessions(frame => frame._updateRequestInterception()); | ||
} | ||
async setFileChooserIntercepted(enabled) { | ||
await this._forAllFrameSessions(frame => frame._setFileChooserIntercepted(enabled)); | ||
} | ||
async opener() { | ||
if (!this._opener) | ||
return null; | ||
const openerPage = await this._opener.pageOrError(); | ||
if (openerPage instanceof page_1.Page && !openerPage.isClosed()) | ||
return openerPage; | ||
return null; | ||
} | ||
async reload() { | ||
await this._mainFrameSession._client.send('Page.reload'); | ||
} | ||
async _go(delta) { | ||
const history = await this._mainFrameSession._client.send('Page.getNavigationHistory'); | ||
const entry = history.entries[history.currentIndex + delta]; | ||
if (!entry) | ||
return false; | ||
await this._mainFrameSession._client.send('Page.navigateToHistoryEntry', { entryId: entry.id }); | ||
return true; | ||
} | ||
goBack() { | ||
return this._go(-1); | ||
} | ||
goForward() { | ||
return this._go(+1); | ||
} | ||
async evaluateOnNewDocument(source) { | ||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source)); | ||
} | ||
async closePage(runBeforeUnload) { | ||
if (runBeforeUnload) | ||
await this._mainFrameSession._client.send('Page.close'); | ||
else | ||
await this._browserContext._browser._closePage(this); | ||
} | ||
canScreenshotOutsideViewport() { | ||
return false; | ||
} | ||
async setBackgroundColor(color) { | ||
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); | ||
} | ||
async takeScreenshot(format, documentRect, viewportRect, quality) { | ||
const { visualViewport } = await this._mainFrameSession._client.send('Page.getLayoutMetrics'); | ||
if (!documentRect) { | ||
documentRect = { | ||
x: visualViewport.pageX + viewportRect.x, | ||
y: visualViewport.pageY + viewportRect.y, | ||
...helper_1.helper.enclosingIntSize({ | ||
width: viewportRect.width / visualViewport.scale, | ||
height: viewportRect.height / visualViewport.scale, | ||
}) | ||
}; | ||
} | ||
await this._mainFrameSession._client.send('Page.bringToFront', {}); | ||
// When taking screenshots with documentRect (based on the page content, not viewport), | ||
// ignore current page scale. | ||
const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 }; | ||
const result = await this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip }); | ||
return Buffer.from(result.data, 'base64'); | ||
} | ||
async resetViewport() { | ||
await this._mainFrameSession._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 }); | ||
} | ||
async getContentFrame(handle) { | ||
return this._sessionForHandle(handle)._getContentFrame(handle); | ||
} | ||
async getOwnerFrame(handle) { | ||
return this._sessionForHandle(handle)._getOwnerFrame(handle); | ||
} | ||
isElementHandle(remoteObject) { | ||
return remoteObject.subtype === 'node'; | ||
} | ||
async getBoundingBox(handle) { | ||
return this._sessionForHandle(handle)._getBoundingBox(handle); | ||
} | ||
async scrollRectIntoViewIfNeeded(handle, rect) { | ||
return this._sessionForHandle(handle)._scrollRectIntoViewIfNeeded(handle, rect); | ||
} | ||
async getContentQuads(handle) { | ||
return this._sessionForHandle(handle)._getContentQuads(handle); | ||
} | ||
async layoutViewport() { | ||
const layoutMetrics = await this._mainFrameSession._client.send('Page.getLayoutMetrics'); | ||
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight }; | ||
} | ||
async setInputFiles(handle, files) { | ||
await handle.evaluate(dom.setFileInputFunction, files); | ||
} | ||
async adoptElementHandle(handle, to) { | ||
return this._sessionForHandle(handle)._adoptElementHandle(handle, to); | ||
} | ||
async getAccessibilityTree(needle) { | ||
return crAccessibility_1.getAccessibilityTree(this._mainFrameSession._client, needle); | ||
} | ||
async inputActionEpilogue() { | ||
await this._mainFrameSession._client.send('Page.enable').catch(e => { }); | ||
} | ||
async pdf(options) { | ||
return this._pdf.generate(options); | ||
} | ||
coverage() { | ||
return this._coverage; | ||
} | ||
async getFrameElement(frame) { | ||
let parent = frame.parentFrame(); | ||
if (!parent) | ||
throw new Error('Frame has been detached.'); | ||
const parentSession = this._sessionForFrame(parent); | ||
const { backendNodeId } = await parentSession._client.send('DOM.getFrameOwner', { frameId: frame._id }).catch(e => { | ||
if (e instanceof Error && e.message.includes('Frame with the given id was not found.')) | ||
e.message = 'Frame has been detached.'; | ||
throw e; | ||
}); | ||
parent = frame.parentFrame(); | ||
if (!parent) | ||
throw new Error('Frame has been detached.'); | ||
return parentSession._adoptBackendNodeId(backendNodeId, await parent._mainContext()); | ||
} | ||
} | ||
exports.CRPage = CRPage; | ||
class FrameSession { | ||
constructor(crPage, client, targetId) { | ||
this._contextIdToContext = new Map(); | ||
this._eventListeners = []; | ||
this._firstNonInitialNavigationCommittedCallback = () => { }; | ||
this._client = client; | ||
this._crPage = crPage; | ||
this._page = crPage._page; | ||
this._targetId = targetId; | ||
this._networkManager = new crNetworkManager_1.CRNetworkManager(client, this._page); | ||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); | ||
} | ||
_isMainFrame() { | ||
return this._targetId === this._crPage._targetId; | ||
} | ||
_addSessionListeners() { | ||
this._eventListeners = [ | ||
helper_1.helper.addEventListener(this._client, 'Inspector.targetCrashed', event => this._onTargetCrashed()), | ||
helper_1.helper.addEventListener(this._client, 'Log.entryAdded', event => this._onLogEntryAdded(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.javascriptDialogOpening', event => this._onDialog(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), | ||
helper_1.helper.addEventListener(this._client, 'Page.downloadWillBegin', event => this._onDownloadWillBegin(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.downloadProgress', event => this._onDownloadProgress(event)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()), | ||
helper_1.helper.addEventListener(this._client, 'Target.attachedToTarget', event => this._onAttachedToTarget(event)), | ||
helper_1.helper.addEventListener(this._client, 'Target.detachedFromTarget', event => this._onDetachedFromTarget(event)), | ||
]; | ||
} | ||
async _initialize() { | ||
let lifecycleEventsEnabled; | ||
if (!this._isMainFrame()) | ||
this._addSessionListeners(); | ||
const promises = [ | ||
this._client.send('Page.enable'), | ||
this._client.send('Page.getFrameTree').then(({ frameTree }) => { | ||
this._handleFrameTree(frameTree); | ||
this._eventListeners = [ | ||
helper_1.helper.addEventListener(this._client, 'Inspector.targetCrashed', event => this._onTargetCrashed()), | ||
helper_1.helper.addEventListener(this._client, 'Log.entryAdded', event => this._onLogEntryAdded(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), | ||
helper_1.helper.addEventListener(this._client, 'Page.javascriptDialogOpening', event => this._onDialog(event)), | ||
helper_1.helper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)), | ||
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()), | ||
helper_1.helper.addEventListener(this._client, 'Target.attachedToTarget', event => this._onAttachedToTarget(event)), | ||
helper_1.helper.addEventListener(this._client, 'Target.detachedFromTarget', event => this._onDetachedFromTarget(event)), | ||
]; | ||
for (const frame of this._page.frames()) { | ||
if (this._isMainFrame()) { | ||
this._handleFrameTree(frameTree); | ||
this._addSessionListeners(); | ||
} | ||
const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)]; | ||
for (const frame of localFrames) { | ||
// Note: frames might be removed before we send these. | ||
@@ -94,6 +308,6 @@ this._client.send('Page.createIsolatedWorld', { | ||
}).catch(helper_1.debugError); | ||
for (const binding of this._browserContext._pageBindings.values()) | ||
for (const binding of this._crPage._browserContext._pageBindings.values()) | ||
frame.evaluate(binding.source).catch(helper_1.debugError); | ||
} | ||
const isInitialEmptyPage = this._page.mainFrame().url() === ':'; | ||
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':'; | ||
if (isInitialEmptyPage) { | ||
@@ -123,3 +337,3 @@ // Ignore lifecycle events for the initial empty page. It is never the final page | ||
]; | ||
const options = this._browserContext._options; | ||
const options = this._crPage._browserContext._options; | ||
if (options.bypassCSP) | ||
@@ -129,4 +343,6 @@ promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); | ||
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); | ||
if (options.viewport) | ||
promises.push(this._updateViewport(true /* updateTouch */)); | ||
if (this._isMainFrame() && options.viewport) | ||
promises.push(this._updateViewport()); | ||
if (options.hasTouch) | ||
promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true })); | ||
if (options.javaScriptEnabled === false) | ||
@@ -140,14 +356,12 @@ promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true })); | ||
promises.push(emulateTimezone(this._client, options.timezoneId)); | ||
if (options.geolocation) | ||
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation)); | ||
promises.push(this.updateExtraHTTPHeaders()); | ||
promises.push(this.updateRequestInterception()); | ||
if (options.offline) | ||
promises.push(this._networkManager.setOffline(options.offline)); | ||
if (options.httpCredentials) | ||
promises.push(this._networkManager.authenticate(options.httpCredentials)); | ||
for (const binding of this._browserContext._pageBindings.values()) | ||
promises.push(this._updateGeolocation()); | ||
promises.push(this._updateExtraHTTPHeaders()); | ||
promises.push(this._updateRequestInterception()); | ||
promises.push(this._updateOffline()); | ||
promises.push(this._updateHttpCredentials()); | ||
promises.push(this._updateEmulateMedia()); | ||
for (const binding of this._crPage._browserContext._pageBindings.values()) | ||
promises.push(this._initBinding(binding)); | ||
for (const source of this._browserContext._evaluateOnNewDocumentSources) | ||
promises.push(this.evaluateOnNewDocument(source)); | ||
for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources) | ||
promises.push(this._evaluateOnNewDocument(source)); | ||
promises.push(this._client.send('Runtime.runIfWaitingForDebugger')); | ||
@@ -157,8 +371,7 @@ promises.push(this._firstNonInitialNavigationCommittedPromise); | ||
} | ||
didClose() { | ||
dispose() { | ||
helper_1.helper.removeEventListeners(this._eventListeners); | ||
this._networkManager.dispose(); | ||
this._page._didClose(); | ||
} | ||
async navigateFrame(frame, url, referrer) { | ||
async _navigate(frame, url, referrer) { | ||
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id }); | ||
@@ -187,2 +400,8 @@ if (response.errorText) | ||
_onFrameAttached(frameId, parentFrameId) { | ||
if (this._crPage._sessions.has(frameId) && frameId !== this._targetId) { | ||
// This is a remote -> local frame transition. | ||
const frame = this._page._frameManager.frame(frameId); | ||
this._page._frameManager.removeChildFramesRecursively(frame); | ||
return; | ||
} | ||
this._page._frameManager.frameAttached(frameId, parentFrameId); | ||
@@ -202,2 +421,7 @@ } | ||
_onFrameDetached(frameId) { | ||
if (this._crPage._sessions.has(frameId)) { | ||
// This is a local -> remote frame transtion. | ||
// We already got a new target and handled frame reattach - nothing to do here. | ||
return; | ||
} | ||
this._page._frameManager.frameDetached(frameId); | ||
@@ -230,2 +454,6 @@ } | ||
const session = crConnection_1.CRConnection.fromSession(this._client).session(event.sessionId); | ||
if (event.targetInfo.type === 'iframe') { | ||
this._crPage.addFrameSession(event.targetInfo.targetId, session); | ||
return; | ||
} | ||
if (event.targetInfo.type !== 'worker') { | ||
@@ -255,5 +483,6 @@ // Ideally, detaching should resume any target, but there is a bug in the backend. | ||
// TODO: attribute workers to the right frame. | ||
this._networkManager.instrumentNetworkEvents(session, this._page.mainFrame()); | ||
this._networkManager.instrumentNetworkEvents(session, this._page._frameManager.frame(this._targetId)); | ||
} | ||
_onDetachedFromTarget(event) { | ||
this._crPage.removeFrameSession(event.targetId); | ||
this._page._removeWorker(event.sessionId); | ||
@@ -282,6 +511,2 @@ } | ||
} | ||
async exposeBinding(binding) { | ||
await this._initBinding(binding); | ||
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(helper_1.debugError))); | ||
} | ||
async _initBinding(binding) { | ||
@@ -318,8 +543,17 @@ await Promise.all([ | ||
const utilityContext = await frame._utilityContext(); | ||
const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityContext); | ||
const handle = await this._adoptBackendNodeId(event.backendNodeId, utilityContext); | ||
this._page._onFileChooserOpened(handle); | ||
} | ||
async updateExtraHTTPHeaders() { | ||
_onDownloadWillBegin(payload) { | ||
this._crPage._browserContext._browser._downloadCreated(this._page, payload.guid, payload.url); | ||
} | ||
_onDownloadProgress(payload) { | ||
if (payload.state === 'completed') | ||
this._crPage._browserContext._browser._downloadFinished(payload.guid, ''); | ||
if (payload.state === 'canceled') | ||
this._crPage._browserContext._browser._downloadFinished(payload.guid, 'canceled'); | ||
} | ||
async _updateExtraHTTPHeaders() { | ||
const headers = network.mergeHeaders([ | ||
this._browserContext._options.extraHTTPHeaders, | ||
this._crPage._browserContext._options.extraHTTPHeaders, | ||
this._page._state.extraHTTPHeaders | ||
@@ -329,8 +563,17 @@ ]); | ||
} | ||
async setViewportSize(viewportSize) { | ||
helper_1.assert(this._page._state.viewportSize === viewportSize); | ||
await this._updateViewport(false /* updateTouch */); | ||
async _updateGeolocation() { | ||
const geolocation = this._crPage._browserContext._options.geolocation; | ||
await this._client.send('Emulation.setGeolocationOverride', geolocation || {}); | ||
} | ||
async _updateViewport(updateTouch) { | ||
const options = this._browserContext._options; | ||
async _updateOffline() { | ||
const offline = !!this._crPage._browserContext._options.offline; | ||
await this._networkManager.setOffline(offline); | ||
} | ||
async _updateHttpCredentials() { | ||
const credentials = this._crPage._browserContext._options.httpCredentials || null; | ||
await this._networkManager.authenticate(credentials); | ||
} | ||
async _updateViewport() { | ||
helper_1.assert(this._isMainFrame()); | ||
const options = this._crPage._browserContext._options; | ||
let viewport = options.viewport || { width: 0, height: 0 }; | ||
@@ -352,79 +595,19 @@ const viewportSize = this._page._state.viewportSize; | ||
]; | ||
if (updateTouch) | ||
promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: !!options.hasTouch })); | ||
await Promise.all(promises); | ||
} | ||
async setEmulateMedia(mediaType, colorScheme) { | ||
async _updateEmulateMedia() { | ||
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light'; | ||
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : []; | ||
await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features }); | ||
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features }); | ||
} | ||
async updateRequestInterception() { | ||
async _updateRequestInterception() { | ||
await this._networkManager.setRequestInterception(this._page._needsRequestInterception()); | ||
} | ||
async setFileChooserIntercepted(enabled) { | ||
async _setFileChooserIntercepted(enabled) { | ||
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => { }); // target can be closed. | ||
} | ||
async opener() { | ||
if (!this._opener) | ||
return null; | ||
const openerPage = await this._opener.pageOrError(); | ||
if (openerPage instanceof page_1.Page && !openerPage.isClosed()) | ||
return openerPage; | ||
return null; | ||
} | ||
async reload() { | ||
await this._client.send('Page.reload'); | ||
} | ||
async _go(delta) { | ||
const history = await this._client.send('Page.getNavigationHistory'); | ||
const entry = history.entries[history.currentIndex + delta]; | ||
if (!entry) | ||
return false; | ||
await this._client.send('Page.navigateToHistoryEntry', { entryId: entry.id }); | ||
return true; | ||
} | ||
goBack() { | ||
return this._go(-1); | ||
} | ||
goForward() { | ||
return this._go(+1); | ||
} | ||
async evaluateOnNewDocument(source) { | ||
async _evaluateOnNewDocument(source) { | ||
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source }); | ||
} | ||
async closePage(runBeforeUnload) { | ||
if (runBeforeUnload) | ||
await this._client.send('Page.close'); | ||
else | ||
await this._browserContext._browser._closePage(this); | ||
} | ||
canScreenshotOutsideViewport() { | ||
return false; | ||
} | ||
async setBackgroundColor(color) { | ||
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); | ||
} | ||
async takeScreenshot(format, documentRect, viewportRect, quality) { | ||
const { visualViewport } = await this._client.send('Page.getLayoutMetrics'); | ||
if (!documentRect) { | ||
documentRect = { | ||
x: visualViewport.pageX + viewportRect.x, | ||
y: visualViewport.pageY + viewportRect.y, | ||
...helper_1.helper.enclosingIntSize({ | ||
width: viewportRect.width / visualViewport.scale, | ||
height: viewportRect.height / visualViewport.scale, | ||
}) | ||
}; | ||
} | ||
await this._client.send('Page.bringToFront', {}); | ||
// When taking screenshots with documentRect (based on the page content, not viewport), | ||
// ignore current page scale. | ||
const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 }; | ||
const result = await this._client.send('Page.captureScreenshot', { format, quality, clip }); | ||
return platform.Buffer.from(result.data, 'base64'); | ||
} | ||
async resetViewport() { | ||
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 }); | ||
} | ||
async getContentFrame(handle) { | ||
async _getContentFrame(handle) { | ||
const nodeInfo = await this._client.send('DOM.describeNode', { | ||
@@ -437,3 +620,3 @@ objectId: toRemoteObject(handle).objectId | ||
} | ||
async getOwnerFrame(handle) { | ||
async _getOwnerFrame(handle) { | ||
// document.documentElement has frameId of the owner frame. | ||
@@ -459,6 +642,3 @@ const documentElement = await handle.evaluateHandle(node => { | ||
} | ||
isElementHandle(remoteObject) { | ||
return remoteObject.subtype === 'node'; | ||
} | ||
async getBoundingBox(handle) { | ||
async _getBoundingBox(handle) { | ||
const result = await this._client.send('DOM.getBoxModel', { | ||
@@ -476,3 +656,3 @@ objectId: toRemoteObject(handle).objectId | ||
} | ||
async scrollRectIntoViewIfNeeded(handle, rect) { | ||
async _scrollRectIntoViewIfNeeded(handle, rect) { | ||
await this._client.send('DOM.scrollIntoViewIfNeeded', { | ||
@@ -487,3 +667,3 @@ objectId: toRemoteObject(handle).objectId, | ||
} | ||
async getContentQuads(handle) { | ||
async _getContentQuads(handle) { | ||
const result = await this._client.send('DOM.getContentQuads', { | ||
@@ -501,16 +681,9 @@ objectId: toRemoteObject(handle).objectId | ||
} | ||
async layoutViewport() { | ||
const layoutMetrics = await this._client.send('Page.getLayoutMetrics'); | ||
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight }; | ||
} | ||
async setInputFiles(handle, files) { | ||
await handle.evaluate(dom.setFileInputFunction, files); | ||
} | ||
async adoptElementHandle(handle, to) { | ||
async _adoptElementHandle(handle, to) { | ||
const nodeInfo = await this._client.send('DOM.describeNode', { | ||
objectId: toRemoteObject(handle).objectId, | ||
}); | ||
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to); | ||
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId, to); | ||
} | ||
async adoptBackendNodeId(backendNodeId, to) { | ||
async _adoptBackendNodeId(backendNodeId, to) { | ||
const result = await this._client.send('DOM.resolveNode', { | ||
@@ -524,27 +697,3 @@ backendNodeId, | ||
} | ||
async getAccessibilityTree(needle) { | ||
return crAccessibility_1.getAccessibilityTree(this._client, needle); | ||
} | ||
async inputActionEpilogue() { | ||
await this._client.send('Page.enable').catch(e => { }); | ||
} | ||
async pdf(options) { | ||
return this._pdf.generate(options); | ||
} | ||
coverage() { | ||
return this._coverage; | ||
} | ||
async getFrameElement(frame) { | ||
const { backendNodeId } = await this._client.send('DOM.getFrameOwner', { frameId: frame._id }).catch(e => { | ||
if (e instanceof Error && e.message.includes('Frame with the given id was not found.')) | ||
e.message = 'Frame has been detached.'; | ||
throw e; | ||
}); | ||
const parent = frame.parentFrame(); | ||
if (!parent) | ||
throw new Error('Frame has been detached.'); | ||
return this.adoptBackendNodeId(backendNodeId, await parent._mainContext()); | ||
} | ||
} | ||
exports.CRPage = CRPage; | ||
function toRemoteObject(handle) { | ||
@@ -551,0 +700,0 @@ return handle._remoteObject; |
@@ -20,3 +20,4 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const fs = require("fs"); | ||
const util = require("util"); | ||
function getExceptionMessage(exceptionDetails) { | ||
@@ -67,3 +68,3 @@ if (exceptionDetails.exception) | ||
if (path) | ||
fd = await platform.openFdAsync(path, 'w'); | ||
fd = await util.promisify(fs.open)(path, 'w'); | ||
const bufs = []; | ||
@@ -73,17 +74,11 @@ while (!eof) { | ||
eof = response.eof; | ||
const buf = platform.Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); | ||
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); | ||
bufs.push(buf); | ||
if (path) | ||
await platform.writeFdAsync(fd, buf); | ||
await util.promisify(fs.write)(fd, buf); | ||
} | ||
if (path) | ||
await platform.closeFdAsync(fd); | ||
await util.promisify(fs.close)(fd); | ||
await client.send('IO.close', { handle }); | ||
let resultBuffer = null; | ||
try { | ||
resultBuffer = platform.Buffer.concat(bufs); | ||
} | ||
finally { | ||
return resultBuffer; | ||
} | ||
return Buffer.concat(bufs); | ||
} | ||
@@ -90,0 +85,0 @@ exports.readProtocolStream = readProtocolStream; |
143
lib/dom.js
@@ -18,11 +18,14 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const debug = require("debug"); | ||
const fs = require("fs"); | ||
const mime = require("mime"); | ||
const path = require("path"); | ||
const util = require("util"); | ||
const helper_1 = require("./helper"); | ||
const js = require("./javascript"); | ||
const injectedSource = require("./generated/injectedSource"); | ||
const helper_1 = require("./helper"); | ||
const platform = require("./platform"); | ||
const selectors_1 = require("./selectors"); | ||
const debugInput = debug('pw:input'); | ||
class FrameExecutionContext extends js.ExecutionContext { | ||
constructor(delegate, frame) { | ||
super(delegate); | ||
this._injectedGeneration = -1; | ||
this.frame = frame; | ||
@@ -38,3 +41,3 @@ } | ||
return this._delegate.evaluate(this, returnByValue, pageFunction, ...args); | ||
}, waitForNavigations ? undefined : { waitUntil: 'nowait' }); | ||
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { waitUntil: 'nowait' }); | ||
} | ||
@@ -47,45 +50,9 @@ _createHandle(remoteObject) { | ||
_injected() { | ||
const selectors = selectors_1.Selectors._instance(); | ||
if (this._injectedPromise && selectors._generation !== this._injectedGeneration) { | ||
this._injectedPromise.then(handle => handle.dispose()); | ||
this._injectedPromise = undefined; | ||
} | ||
if (!this._injectedPromise) { | ||
const custom = []; | ||
for (const [name, source] of selectors._engines) | ||
custom.push(`{ name: '${name}', engine: (${source}) }`); | ||
const source = ` | ||
new (${injectedSource.source})([ | ||
${custom.join(',\n')} | ||
]) | ||
`; | ||
this._injectedPromise = this._doEvaluateInternal(false /* returnByValue */, false /* waitForNavigations */, source); | ||
this._injectedGeneration = selectors._generation; | ||
this._injectedPromise = selectors_1.selectors._prepareEvaluator(this).then(evaluator => { | ||
return this.evaluateHandleInternal(evaluator => evaluator.injected, evaluator); | ||
}); | ||
} | ||
return this._injectedPromise; | ||
} | ||
async _$(selector, scope) { | ||
const handle = await this.evaluateHandleInternal(({ injected, selector, scope }) => injected.querySelector(selector, scope || document), { injected: await this._injected(), selector, scope }); | ||
if (!handle.asElement()) | ||
handle.dispose(); | ||
return handle.asElement(); | ||
} | ||
async _$array(selector, scope) { | ||
const arrayHandle = await this.evaluateHandleInternal(({ injected, selector, scope }) => injected.querySelectorAll(selector, scope || document), { injected: await this._injected(), selector, scope }); | ||
return arrayHandle; | ||
} | ||
async _$$(selector, scope) { | ||
const arrayHandle = await this._$array(selector, scope); | ||
const properties = await arrayHandle.getProperties(); | ||
arrayHandle.dispose(); | ||
const result = []; | ||
for (const property of properties.values()) { | ||
const elementHandle = property.asElement(); | ||
if (elementHandle) | ||
result.push(elementHandle); | ||
else | ||
property.dispose(); | ||
} | ||
return result; | ||
} | ||
} | ||
@@ -124,3 +91,5 @@ exports.FrameExecutionContext = FrameExecutionContext; | ||
async _scrollRectIntoViewIfNeeded(rect) { | ||
debugInput('scrolling into veiw if needed...'); | ||
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect); | ||
debugInput('...done'); | ||
} | ||
@@ -182,11 +151,14 @@ async scrollIntoViewIfNeeded() { | ||
} | ||
async _performPointerAction(action, options) { | ||
async _performPointerAction(action, options = {}) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
const { force = false } = (options || {}); | ||
if (!force) | ||
await this._waitForDisplayedAtStablePosition(options); | ||
await this._waitForDisplayedAtStablePosition(deadline); | ||
const position = options ? options.position : undefined; | ||
await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); | ||
const point = position ? await this._offsetPoint(position) : await this._clickablePoint(); | ||
point.x = (point.x * 100 | 0) / 100; | ||
point.y = (point.y * 100 | 0) / 100; | ||
if (!force) | ||
await this._waitForHitTargetAt(point, options); | ||
await this._waitForHitTargetAt(point, deadline); | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
@@ -196,6 +168,8 @@ let restoreModifiers; | ||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); | ||
debugInput('performing input action...'); | ||
await action(point); | ||
debugInput('...done'); | ||
if (restoreModifiers) | ||
await this._page.keyboard._ensureModifiers(restoreModifiers); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
@@ -212,2 +186,3 @@ hover(options) { | ||
async selectOption(values, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
let vals; | ||
@@ -231,17 +206,21 @@ if (!Array.isArray(values)) | ||
return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); | ||
}, options); | ||
}, deadline, options); | ||
} | ||
async fill(value, options) { | ||
helper_1.assert(helper_1.helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
const error = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); | ||
if (error) | ||
throw new Error(error); | ||
if (value) | ||
await this._page.keyboard.insertText(value); | ||
else | ||
await this._page.keyboard.press('Delete'); | ||
}, options, true); | ||
const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); | ||
if (typeof errorOrNeedsInput === 'string') | ||
throw new Error(errorOrNeedsInput); | ||
if (errorOrNeedsInput) { | ||
if (value) | ||
await this._page.keyboard.insertText(value); | ||
else | ||
await this._page.keyboard.press('Delete'); | ||
} | ||
}, deadline, options, true); | ||
} | ||
async setInputFiles(files) { | ||
async setInputFiles(files, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
const multiple = await this._evaluateInUtility(({ node }) => { | ||
@@ -263,5 +242,5 @@ if (node.nodeType !== Node.ELEMENT_NODE || node.tagName !== 'INPUT') | ||
const file = { | ||
name: platform.basename(item), | ||
type: platform.getMimeType(item), | ||
data: await platform.readFileAsync(item, 'base64') | ||
name: path.basename(item), | ||
type: mime.getType(item) || 'application/octet-stream', | ||
data: await util.promisify(fs.readFile)(item, 'base64') | ||
}; | ||
@@ -276,3 +255,3 @@ filePayloads.push(file); | ||
await this._page._delegate.setInputFiles(this, filePayloads); | ||
}); | ||
}, deadline, options); | ||
} | ||
@@ -290,12 +269,14 @@ async focus() { | ||
async type(text, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
await this.focus(); | ||
await this._page.keyboard.type(text, options); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
async press(key, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
await this.focus(); | ||
await this._page.keyboard.press(key, options); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
@@ -322,17 +303,21 @@ async check(options) { | ||
$(selector) { | ||
return this._context._$(selector, this); | ||
// TODO: this should be ownerFrame() instead. | ||
return selectors_1.selectors._query(this._context.frame, selector, this); | ||
} | ||
$$(selector) { | ||
return this._context._$$(selector, this); | ||
// TODO: this should be ownerFrame() instead. | ||
return selectors_1.selectors._queryAll(this._context.frame, selector, this); | ||
} | ||
async $eval(selector, pageFunction, arg) { | ||
const elementHandle = await this._context._$(selector, this); | ||
if (!elementHandle) | ||
// TODO: this should be ownerFrame() instead. | ||
const handle = await selectors_1.selectors._query(this._context.frame, selector, this); | ||
if (!handle) | ||
throw new Error(`Error: failed to find element matching selector "${selector}"`); | ||
const result = await elementHandle.evaluate(pageFunction, arg); | ||
elementHandle.dispose(); | ||
const result = await handle.evaluate(pageFunction, arg); | ||
handle.dispose(); | ||
return result; | ||
} | ||
async $$eval(selector, pageFunction, arg) { | ||
const arrayHandle = await this._context._$array(selector, this); | ||
// TODO: this should be ownerFrame() instead. | ||
const arrayHandle = await selectors_1.selectors._queryArray(this._context.frame, selector, this); | ||
const result = await arrayHandle.evaluate(pageFunction, arg); | ||
@@ -342,9 +327,12 @@ arrayHandle.dispose(); | ||
} | ||
async _waitForDisplayedAtStablePosition(options = {}) { | ||
async _waitForDisplayedAtStablePosition(deadline) { | ||
debugInput('waiting for element to be displayed and not moving...'); | ||
const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => { | ||
return injected.waitForDisplayedAtStablePosition(node, timeout); | ||
}, options.timeout || 0); | ||
await helper_1.helper.waitWithTimeout(stablePromise, 'element to be displayed and not moving', options.timeout || 0); | ||
}, helper_1.helper.timeUntilDeadline(deadline)); | ||
await helper_1.helper.waitWithDeadline(stablePromise, 'element to be displayed and not moving', deadline); | ||
debugInput('...done'); | ||
} | ||
async _waitForHitTargetAt(point, options = {}) { | ||
async _waitForHitTargetAt(point, deadline) { | ||
debugInput(`waiting for element to receive pointer events at (${point.x},${point.y}) ...`); | ||
const frame = await this.ownerFrame(); | ||
@@ -361,4 +349,5 @@ if (frame && frame.parentFrame()) { | ||
return injected.waitForHitTargetAt(node, timeout, point); | ||
}, { timeout: options.timeout || 0, point }); | ||
await helper_1.helper.waitWithTimeout(hitTargetPromise, 'element to receive mouse events', options.timeout || 0); | ||
}, { timeout: helper_1.helper.timeUntilDeadline(deadline), point }); | ||
await helper_1.helper.waitWithDeadline(hitTargetPromise, 'element to receive pointer events', deadline); | ||
debugInput('...done'); | ||
} | ||
@@ -365,0 +354,0 @@ } |
@@ -34,2 +34,3 @@ "use strict"; | ||
Dialog: 'dialog', | ||
Download: 'download', | ||
FileChooser: 'filechooser', | ||
@@ -36,0 +37,0 @@ DOMContentLoaded: 'domcontentloaded', |
@@ -25,3 +25,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
@@ -31,3 +30,3 @@ const ffConnection_1 = require("./ffConnection"); | ||
const ffPage_1 = require("./ffPage"); | ||
class FFBrowser extends platform.EventEmitter { | ||
class FFBrowser extends browser_1.BrowserBase { | ||
constructor(connection) { | ||
@@ -48,2 +47,4 @@ super(); | ||
helper_1.helper.addEventListener(this._connection, 'Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)), | ||
helper_1.helper.addEventListener(this._connection, 'Browser.downloadCreated', this._onDownloadCreated.bind(this)), | ||
helper_1.helper.addEventListener(this._connection, 'Browser.downloadFinished', this._onDownloadFinished.bind(this)), | ||
]; | ||
@@ -86,10 +87,13 @@ this._firstPagePromise = new Promise(f => this._firstPageCallback = f); | ||
bypassCSP: options.bypassCSP, | ||
ignoreHTTPSErrors: options.ignoreHTTPSErrors, | ||
javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined, | ||
viewport, | ||
locale: options.locale, | ||
removeOnDetach: true | ||
timezoneId: options.timezoneId, | ||
removeOnDetach: true, | ||
downloadOptions: { | ||
behavior: options.acceptDownloads ? 'saveToDisk' : 'cancel', | ||
downloadsDir: this._downloadsPath, | ||
}, | ||
}); | ||
// TODO: move ignoreHTTPSErrors to browser context level. | ||
if (options.ignoreHTTPSErrors) | ||
await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); | ||
const context = new FFBrowserContext(this, browserContextId, options); | ||
@@ -103,5 +107,2 @@ await context._initialize(); | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
_onDetachedFromTarget(payload) { | ||
@@ -131,12 +132,17 @@ const ffPage = this._ffPages.get(payload.targetId); | ||
} | ||
async close() { | ||
await Promise.all(this.contexts().map(context => context.close())); | ||
_onDownloadCreated(payload) { | ||
const ffPage = this._ffPages.get(payload.pageTargetId); | ||
helper_1.assert(ffPage); | ||
if (!ffPage) | ||
return; | ||
this._downloadCreated(ffPage._page, payload.uuid, payload.url); | ||
} | ||
_onDownloadFinished(payload) { | ||
const error = payload.canceled ? 'canceled' : payload.error; | ||
this._downloadFinished(payload.uuid, error); | ||
} | ||
_disconnect() { | ||
helper_1.helper.removeEventListeners(this._eventListeners); | ||
const disconnected = new Promise(f => this.once(events_1.Events.Browser.Disconnected, f)); | ||
this._connection.close(); | ||
await disconnected; | ||
} | ||
_setDebugFunction(debugFunction) { | ||
this._connection._debugProtocol = debugFunction; | ||
} | ||
} | ||
@@ -162,2 +168,4 @@ exports.FFBrowser = FFBrowser; | ||
await this.setOffline(this._options.offline); | ||
if (this._options.colorScheme) | ||
await this._setColorScheme(this._options.colorScheme); | ||
} | ||
@@ -180,2 +188,6 @@ _ffPages() { | ||
browserContextId: this._browserContextId || undefined | ||
}).catch(e => { | ||
if (e.message.includes('Failed to override timezone')) | ||
throw new Error(`Invalid timezone ID: ${this._options.timezoneId}`); | ||
throw e; | ||
}); | ||
@@ -241,2 +253,5 @@ const ffPage = this._browser._ffPages.get(targetId); | ||
} | ||
async _setColorScheme(colorScheme) { | ||
await this._browser._connection.send('Browser.setColorScheme', { browserContextId: this._browserContextId || undefined, colorScheme }); | ||
} | ||
async setHTTPCredentials(httpCredentials) { | ||
@@ -278,3 +293,3 @@ this._options.httpCredentials = httpCredentials || undefined; | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -281,0 +296,0 @@ } |
@@ -19,4 +19,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const events_1 = require("events"); | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
exports.ConnectionEvents = { | ||
@@ -28,6 +29,5 @@ Disconnected: Symbol('Disconnected'), | ||
exports.kBrowserCloseMessageId = -9999; | ||
class FFConnection extends platform.EventEmitter { | ||
class FFConnection extends events_1.EventEmitter { | ||
constructor(transport) { | ||
super(); | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._transport = transport; | ||
@@ -45,3 +45,2 @@ this._lastId = 0; | ||
this.once = super.once; | ||
this._debugProtocol.color = '34'; | ||
} | ||
@@ -59,29 +58,29 @@ async send(method, params) { | ||
_rawSend(message) { | ||
const data = JSON.stringify(message); | ||
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); | ||
this._transport.send(data); | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); | ||
this._transport.send(message); | ||
} | ||
async _onMessage(message) { | ||
this._debugProtocol('◀ RECV ' + message); | ||
const object = JSON.parse(message); | ||
if (object.id === exports.kBrowserCloseMessageId) | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('◀ RECV ' + JSON.stringify(message)); | ||
if (message.id === exports.kBrowserCloseMessageId) | ||
return; | ||
if (object.sessionId) { | ||
const session = this._sessions.get(object.sessionId); | ||
if (message.sessionId) { | ||
const session = this._sessions.get(message.sessionId); | ||
if (session) | ||
session.dispatchMessage(object); | ||
session.dispatchMessage(message); | ||
} | ||
else if (object.id) { | ||
const callback = this._callbacks.get(object.id); | ||
else if (message.id) { | ||
const callback = this._callbacks.get(message.id); | ||
// Callbacks could be all rejected if someone has called `.dispose()`. | ||
if (callback) { | ||
this._callbacks.delete(object.id); | ||
if (object.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, object)); | ||
this._callbacks.delete(message.id); | ||
if (message.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, message.error)); | ||
else | ||
callback.resolve(object.result); | ||
callback.resolve(message.result); | ||
} | ||
} | ||
else { | ||
Promise.resolve().then(() => this.emit(object.method, object.params)); | ||
Promise.resolve().then(() => this.emit(message.method, message.params)); | ||
} | ||
@@ -115,3 +114,3 @@ } | ||
}; | ||
class FFSession extends platform.EventEmitter { | ||
class FFSession extends events_1.EventEmitter { | ||
constructor(connection, targetType, sessionId, rawSend) { | ||
@@ -145,3 +144,3 @@ super(); | ||
if (object.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, object)); | ||
callback.reject(createProtocolError(callback.error, callback.method, object.error)); | ||
else | ||
@@ -165,6 +164,6 @@ callback.resolve(object.result); | ||
exports.FFSession = FFSession; | ||
function createProtocolError(error, method, object) { | ||
let message = `Protocol error (${method}): ${object.error.message}`; | ||
if ('data' in object.error) | ||
message += ` ${object.error.data}`; | ||
function createProtocolError(error, method, protocolError) { | ||
let message = `Protocol error (${method}): ${protocolError.message}`; | ||
if ('data' in protocolError) | ||
message += ` ${protocolError.data}`; | ||
return rewriteError(error, message); | ||
@@ -181,3 +180,4 @@ } | ||
return `{"id":${message.id} [evaluate injected script]}`; | ||
return JSON.stringify(message); | ||
} | ||
//# sourceMappingURL=ffConnection.js.map |
@@ -21,3 +21,2 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
class FFNetworkManager { | ||
@@ -62,3 +61,3 @@ constructor(session, page) { | ||
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); | ||
return platform.Buffer.from(response.base64body, 'base64'); | ||
return Buffer.from(response.base64body, 'base64'); | ||
}; | ||
@@ -146,3 +145,3 @@ const headers = {}; | ||
async fulfill(response) { | ||
const responseBody = response.body && helper_1.helper.isString(response.body) ? platform.Buffer.from(response.body) : (response.body || null); | ||
const responseBody = response.body && helper_1.helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null); | ||
const responseHeaders = {}; | ||
@@ -156,3 +155,3 @@ if (response.headers) { | ||
if (responseBody && !('content-length' in responseHeaders)) | ||
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody)); | ||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); | ||
await this._session.send('Network.fulfillInterceptedRequest', { | ||
@@ -159,0 +158,0 @@ requestId: this._id, |
@@ -24,3 +24,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const screenshotter_1 = require("../screenshotter"); | ||
@@ -32,2 +31,3 @@ const ffAccessibility_1 = require("./ffAccessibility"); | ||
const ffNetworkManager_1 = require("./ffNetworkManager"); | ||
const selectors_1 = require("../selectors"); | ||
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; | ||
@@ -71,20 +71,9 @@ class FFPage { | ||
session.once(ffConnection_1.FFSessionEvents.Disconnected, () => this._page._didDisconnect()); | ||
this._initialize(); | ||
} | ||
async _initialize() { | ||
try { | ||
await Promise.all([ | ||
// TODO: we should get rid of this call to resolve before any early events arrive, e.g. dialogs. | ||
this._session.send('Page.addScriptToEvaluateOnNewDocument', { | ||
script: '', | ||
worldName: UTILITY_WORLD_NAME, | ||
}), | ||
new Promise(f => this._session.once('Page.ready', f)), | ||
]); | ||
this._session.once('Page.ready', () => { | ||
this._pageCallback(this._page); | ||
} | ||
catch (e) { | ||
this._pageCallback(e); | ||
} | ||
this._initialized = true; | ||
this._initialized = true; | ||
}); | ||
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy. | ||
// Therefore, we can end up with an initialized page without utility world, although very unlikely. | ||
this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: '', worldName: UTILITY_WORLD_NAME }).catch(this._pageCallback); | ||
} | ||
@@ -253,6 +242,7 @@ _initializedPage() { | ||
} | ||
async setEmulateMedia(mediaType, colorScheme) { | ||
async updateEmulateMedia() { | ||
const colorScheme = this._page._state.colorScheme || this._browserContext._options.colorScheme || 'light'; | ||
await this._session.send('Page.setEmulatedMedia', { | ||
type: mediaType === null ? undefined : mediaType, | ||
colorScheme: colorScheme === null ? undefined : colorScheme | ||
type: this._page._state.mediaType === null ? undefined : this._page._state.mediaType, | ||
colorScheme | ||
}); | ||
@@ -319,3 +309,3 @@ } | ||
}); | ||
return platform.Buffer.from(data, 'base64'); | ||
return Buffer.from(data, 'base64'); | ||
} | ||
@@ -403,4 +393,3 @@ async resetViewport() { | ||
throw new Error('Frame has been detached.'); | ||
const context = await parent._utilityContext(); | ||
const handles = await context._$$('iframe'); | ||
const handles = await selectors_1.selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */); | ||
const items = await Promise.all(handles.map(async (handle) => { | ||
@@ -407,0 +396,0 @@ const frame = await handle.contentFrame().catch(e => null); |
@@ -19,7 +19,9 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const types = require("./types"); | ||
const helper_1 = require("./helper"); | ||
const fs = require("fs"); | ||
const util = require("util"); | ||
const errors_1 = require("./errors"); | ||
const events_1 = require("./events"); | ||
const platform = require("./platform"); | ||
const helper_1 = require("./helper"); | ||
const selectors_1 = require("./selectors"); | ||
const types = require("./types"); | ||
class FrameManager { | ||
@@ -72,6 +74,6 @@ constructor(page) { | ||
} | ||
async waitForNavigationsCreatedBy(action, options = {}, input) { | ||
async waitForNavigationsCreatedBy(action, deadline, options = {}, input) { | ||
if (options.waitUntil === 'nowait') | ||
return action(); | ||
const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }); | ||
const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline); | ||
this._pendingNavigationBarriers.add(barrier); | ||
@@ -84,3 +86,3 @@ try { | ||
// Resolve in the next task, after all waitForNavigations. | ||
await new Promise(platform.makeWaitForNextTask()); | ||
await new Promise(helper_1.helper.makeWaitForNextTask()); | ||
return result; | ||
@@ -109,4 +111,3 @@ } | ||
const frame = this._frames.get(frameId); | ||
for (const child of frame.childFrames()) | ||
this._removeFramesRecursively(child); | ||
this.removeChildFramesRecursively(frame); | ||
frame._url = url; | ||
@@ -212,5 +213,8 @@ frame._name = name; | ||
} | ||
_removeFramesRecursively(frame) { | ||
removeChildFramesRecursively(frame) { | ||
for (const child of frame.childFrames()) | ||
this._removeFramesRecursively(child); | ||
} | ||
_removeFramesRecursively(frame) { | ||
this.removeChildFramesRecursively(frame); | ||
frame._onDetached(); | ||
@@ -363,11 +367,3 @@ this._frames.delete(frame._id); | ||
async $(selector) { | ||
const utilityContext = await this._utilityContext(); | ||
const mainContext = await this._mainContext(); | ||
const handle = await utilityContext._$(selector); | ||
if (handle && handle._context !== mainContext) { | ||
const adopted = this._page._delegate.adoptElementHandle(handle, mainContext); | ||
handle.dispose(); | ||
return adopted; | ||
} | ||
return handle; | ||
return selectors_1.selectors._query(this, selector); | ||
} | ||
@@ -377,7 +373,8 @@ async waitForSelector(selector, options) { | ||
throw new Error('options.visibility is not supported, did you mean options.waitFor?'); | ||
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {}); | ||
const { waitFor = 'attached' } = (options || {}); | ||
if (!['attached', 'detached', 'visible', 'hidden'].includes(waitFor)) | ||
throw new Error(`Unsupported waitFor option "${waitFor}"`); | ||
const task = waitForSelectorTask(selector, waitFor, timeout); | ||
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, waitFor)}"`); | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
const { world, task } = selectors_1.selectors._waitForSelectorTask(selector, waitFor, deadline); | ||
const result = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selectorToString(selector, waitFor)}"`); | ||
if (!result.asElement()) { | ||
@@ -397,13 +394,11 @@ result.dispose(); | ||
async $eval(selector, pageFunction, arg) { | ||
const context = await this._mainContext(); | ||
const elementHandle = await context._$(selector); | ||
if (!elementHandle) | ||
const handle = await this.$(selector); | ||
if (!handle) | ||
throw new Error(`Error: failed to find element matching selector "${selector}"`); | ||
const result = await elementHandle.evaluate(pageFunction, arg); | ||
elementHandle.dispose(); | ||
const result = await handle.evaluate(pageFunction, arg); | ||
handle.dispose(); | ||
return result; | ||
} | ||
async $$eval(selector, pageFunction, arg) { | ||
const context = await this._mainContext(); | ||
const arrayHandle = await context._$array(selector); | ||
const arrayHandle = await selectors_1.selectors._queryArray(this, selector); | ||
const result = await arrayHandle.evaluate(pageFunction, arg); | ||
@@ -414,4 +409,3 @@ arrayHandle.dispose(); | ||
async $$(selector) { | ||
const context = await this._mainContext(); | ||
return context._$$(selector); | ||
return selectors_1.selectors._queryAll(this, selector); | ||
} | ||
@@ -472,3 +466,3 @@ async content() { | ||
if (path !== null) { | ||
let contents = await platform.readFileAsync(path, 'utf8'); | ||
let contents = await util.promisify(fs.readFile)(path, 'utf8'); | ||
contents += '//# sourceURL=' + path.replace(/\n/g, ''); | ||
@@ -513,3 +507,3 @@ return (await context.evaluateHandleInternal(addScriptContent, { content: contents, type })).asElement(); | ||
if (path !== null) { | ||
let contents = await platform.readFileAsync(path, 'utf8'); | ||
let contents = await util.promisify(fs.readFile)(path, 'utf8'); | ||
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; | ||
@@ -575,19 +569,19 @@ return (await context.evaluateHandleInternal(addStyleContent, contents)).asElement(); | ||
} | ||
async click(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.click(options); | ||
async click(selector, options = {}) { | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.click(helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async dblclick(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.dblclick(options); | ||
async dblclick(selector, options = {}) { | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.dblclick(helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async fill(selector, value, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.fill(value, options); | ||
async fill(selector, value, options = {}) { | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.fill(value, helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async focus(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
const { handle } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.focus(); | ||
@@ -597,9 +591,9 @@ handle.dispose(); | ||
async hover(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.hover(options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.hover(helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async selectOption(selector, values, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
const result = await handle.selectOption(values, options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
const result = await handle.selectOption(values, helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
@@ -609,19 +603,19 @@ return result; | ||
async type(selector, text, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.type(text, options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.type(text, helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async press(selector, key, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.press(key, options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.press(key, helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async check(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.check(options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.check(helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
} | ||
async uncheck(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.uncheck(options); | ||
const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.uncheck(helper_1.helper.optionsWithUpdatedTimeout(options, deadline)); | ||
handle.dispose(); | ||
@@ -639,9 +633,11 @@ } | ||
async _waitForSelectorInUtilityContext(selector, options) { | ||
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {}); | ||
const task = waitForSelectorTask(selector, waitFor, timeout); | ||
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, waitFor)}"`); | ||
return result.asElement(); | ||
const { waitFor = 'attached' } = options || {}; | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
const { world, task } = selectors_1.selectors._waitForSelectorTask(selector, waitFor, deadline); | ||
const result = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selectorToString(selector, waitFor)}"`); | ||
return { handle: result.asElement(), deadline }; | ||
} | ||
async waitForFunction(pageFunction, arg, options = {}) { | ||
const { polling = 'raf', timeout = this._page._timeoutSettings.timeout() } = options; | ||
const { polling = 'raf' } = options; | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
if (helper_1.helper.isString(polling)) | ||
@@ -656,7 +652,5 @@ helper_1.assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); | ||
const innerPredicate = new Function('arg', predicateBody); | ||
return injected.poll(polling, undefined, timeout, (element) => { | ||
return innerPredicate(arg); | ||
}); | ||
}, { injected: await context._injected(), predicateBody, polling, timeout, arg }); | ||
return this._scheduleRerunnableTask(task, 'main', timeout); | ||
return injected.poll(polling, timeout, () => innerPredicate(arg)); | ||
}, { injected: await context._injected(), predicateBody, polling, timeout: helper_1.helper.timeUntilDeadline(deadline), arg }); | ||
return this._scheduleRerunnableTask(task, 'main', deadline); | ||
} | ||
@@ -678,5 +672,5 @@ async title() { | ||
} | ||
_scheduleRerunnableTask(task, contextType, timeout, title) { | ||
_scheduleRerunnableTask(task, contextType, deadline, title) { | ||
const data = this._contextData.get(contextType); | ||
const rerunnableTask = new RerunnableTask(data, task, timeout, title); | ||
const rerunnableTask = new RerunnableTask(data, task, deadline, title); | ||
data.rerunnableTasks.add(rerunnableTask); | ||
@@ -718,21 +712,4 @@ if (data.context) | ||
exports.Frame = Frame; | ||
function waitForSelectorTask(selector, waitFor, timeout) { | ||
return async (context) => context.evaluateHandleInternal(({ injected, selector, waitFor, timeout }) => { | ||
const polling = (waitFor === 'attached' || waitFor === 'detached') ? 'mutation' : 'raf'; | ||
return injected.poll(polling, selector, timeout, (element) => { | ||
switch (waitFor) { | ||
case 'attached': | ||
return element || false; | ||
case 'detached': | ||
return !element; | ||
case 'visible': | ||
return element && injected.isVisible(element) ? element : false; | ||
case 'hidden': | ||
return !element || !injected.isVisible(element); | ||
} | ||
}); | ||
}, { injected: await context._injected(), selector, waitFor, timeout }); | ||
} | ||
class RerunnableTask { | ||
constructor(data, task, timeout, title) { | ||
constructor(data, task, deadline, title) { | ||
this._resolve = () => { }; | ||
@@ -750,6 +727,4 @@ this._reject = () => { }; | ||
// timeout on our end. | ||
if (timeout) { | ||
const timeoutError = new errors_1.TimeoutError(`waiting for ${title || 'function'} failed: timeout ${timeout}ms exceeded`); | ||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout); | ||
} | ||
const timeoutError = new errors_1.TimeoutError(`waiting for ${title || 'function'} failed: timeout exceeded`); | ||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), helper_1.helper.timeUntilDeadline(deadline)); | ||
} | ||
@@ -822,3 +797,3 @@ terminate(error) { | ||
class PendingNavigationBarrier { | ||
constructor(options) { | ||
constructor(options, deadline) { | ||
this._frameIds = new Map(); | ||
@@ -828,2 +803,3 @@ this._protectCount = 0; | ||
this._options = options; | ||
this._deadline = deadline; | ||
this._promise = new Promise(f => this._promiseCallback = f); | ||
@@ -838,3 +814,5 @@ this.retain(); | ||
this.retain(); | ||
await frame.waitForNavigation(this._options).catch(e => { }); | ||
const timeout = helper_1.helper.timeUntilDeadline(this._deadline); | ||
const options = { ...this._options, timeout }; | ||
await frame.waitForNavigation(options).catch(e => { }); | ||
this.release(); | ||
@@ -866,3 +844,3 @@ } | ||
if (timeout) { | ||
const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded'; | ||
const errorMessage = 'Navigation timeout exceeded'; | ||
timeoutPromise = new Promise(fulfill => this._timer = setTimeout(fulfill, timeout)) | ||
@@ -869,0 +847,0 @@ .then(() => { throw new errors_1.TimeoutError(errorMessage); }); |
@@ -19,5 +19,8 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const crypto = require("crypto"); | ||
const debug = require("debug"); | ||
const fs = require("fs"); | ||
const util = require("util"); | ||
const errors_1 = require("./errors"); | ||
const platform = require("./platform"); | ||
exports.debugError = platform.debug(`pw:error`); | ||
exports.debugError = debug(`pw:error`); | ||
class Helper { | ||
@@ -42,3 +45,3 @@ static evaluationString(fun, ...args) { | ||
else if (fun.path !== undefined) { | ||
let contents = await platform.readFileAsync(fun.path, 'utf8'); | ||
let contents = await util.promisify(fs.readFile)(fun.path, 'utf8'); | ||
if (addSourceUrl) | ||
@@ -55,3 +58,3 @@ contents += '//# sourceURL=' + fun.path.replace(/\n/g, ''); | ||
static installApiHooks(className, classType) { | ||
const log = platform.debug('pw:api'); | ||
const log = debug('pw:api'); | ||
for (const methodName of Reflect.ownKeys(classType.prototype)) { | ||
@@ -121,4 +124,3 @@ const method = Reflect.get(classType.prototype, methodName); | ||
} | ||
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) { | ||
let eventTimeout; | ||
static async waitForEvent(emitter, eventName, predicate, deadline, abortPromise) { | ||
let resolveCallback = () => { }; | ||
@@ -140,7 +142,5 @@ let rejectCallback = () => { }; | ||
}); | ||
if (timeout) { | ||
eventTimeout = setTimeout(() => { | ||
rejectCallback(new errors_1.TimeoutError(`Timeout exceeded while waiting for ${String(eventName)}`)); | ||
}, timeout); | ||
} | ||
const eventTimeout = setTimeout(() => { | ||
rejectCallback(new errors_1.TimeoutError(`Timeout exceeded while waiting for ${String(eventName)}`)); | ||
}, exports.helper.timeUntilDeadline(deadline)); | ||
function cleanup() { | ||
@@ -162,8 +162,9 @@ Helper.removeEventListeners([listener]); | ||
static async waitWithTimeout(promise, taskName, timeout) { | ||
return this.waitWithDeadline(promise, taskName, exports.helper.monotonicTime() + timeout); | ||
} | ||
static async waitWithDeadline(promise, taskName, deadline) { | ||
let reject; | ||
const timeoutError = new errors_1.TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`); | ||
const timeoutError = new errors_1.TimeoutError(`waiting for ${taskName} failed: timeout exceeded`); | ||
const timeoutPromise = new Promise((resolve, x) => reject = x); | ||
let timeoutTimer = null; | ||
if (timeout) | ||
timeoutTimer = setTimeout(() => reject(timeoutError), timeout); | ||
const timeoutTimer = setTimeout(() => reject(timeoutError), exports.helper.timeUntilDeadline(deadline)); | ||
try { | ||
@@ -269,2 +270,46 @@ return await Promise.race([promise, timeoutPromise]); | ||
} | ||
// See https://joel.tools/microtasks/ | ||
static makeWaitForNextTask() { | ||
if (parseInt(process.versions.node, 10) >= 11) | ||
return setImmediate; | ||
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order: | ||
// - https://github.com/nodejs/node/issues/22257 | ||
// | ||
// So we can't simply run setImmediate to dispatch code in a following task. | ||
// However, we can run setImmediate from-inside setImmediate to make sure we're getting | ||
// in the following task. | ||
let spinning = false; | ||
const callbacks = []; | ||
const loop = () => { | ||
const callback = callbacks.shift(); | ||
if (!callback) { | ||
spinning = false; | ||
return; | ||
} | ||
setImmediate(loop); | ||
// Make sure to call callback() as the last thing since it's | ||
// untrusted code that might throw. | ||
callback(); | ||
}; | ||
return (callback) => { | ||
callbacks.push(callback); | ||
if (!spinning) { | ||
spinning = true; | ||
setImmediate(loop); | ||
} | ||
}; | ||
} | ||
static guid() { | ||
return crypto.randomBytes(16).toString('hex'); | ||
} | ||
static monotonicTime() { | ||
const [seconds, nanoseconds] = process.hrtime(); | ||
return seconds * 1000 + (nanoseconds / 1000000 | 0); | ||
} | ||
static timeUntilDeadline(deadline) { | ||
return Math.min(deadline - this.monotonicTime(), 2147483647); // 2^31-1 safe setTimeout in Node. | ||
} | ||
static optionsWithUpdatedTimeout(options, deadline) { | ||
return { ...(options || {}), timeout: this.timeUntilDeadline(deadline) }; | ||
} | ||
} | ||
@@ -271,0 +316,0 @@ function assert(value, message) { |
@@ -108,3 +108,3 @@ "use strict"; | ||
if (!kModifiers.includes(modifier)) | ||
throw new Error('Uknown modifier ' + modifier); | ||
throw new Error('Unknown modifier ' + modifier); | ||
} | ||
@@ -111,0 +111,0 @@ const restore = Array.from(this._pressedModifiers); |
@@ -18,3 +18,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const platform = require("./platform"); | ||
const helper_1 = require("./helper"); | ||
class ExecutionContext { | ||
@@ -108,3 +108,3 @@ constructor(delegate) { | ||
const pushHandle = (handle) => { | ||
const guid = platform.guid(); | ||
const guid = helper_1.helper.guid(); | ||
guids.push(guid); | ||
@@ -111,0 +111,0 @@ handles.push(handle); |
@@ -18,4 +18,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs = require("fs"); | ||
const mime = require("mime"); | ||
const util = require("util"); | ||
const helper_1 = require("./helper"); | ||
const platform = require("./platform"); | ||
function filterCookies(cookies, urls = []) { | ||
@@ -169,4 +171,4 @@ if (!Array.isArray(urls)) | ||
headers: response.headers, | ||
contentType: platform.getMimeType(response.path), | ||
body: await platform.readFileBuffer(response.path) | ||
contentType: mime.getType(response.path) || 'application/octet-stream', | ||
body: await util.promisify(fs.readFile)(response.path) | ||
}; | ||
@@ -173,0 +175,0 @@ } |
@@ -30,4 +30,5 @@ "use strict"; | ||
const accessibility = require("./accessibility"); | ||
const platform = require("./platform"); | ||
class Page extends platform.EventEmitter { | ||
const extendedEventEmitter_1 = require("./extendedEventEmitter"); | ||
const events_2 = require("events"); | ||
class Page extends extendedEventEmitter_1.ExtendedEventEmitter { | ||
constructor(delegate, browserContext) { | ||
@@ -69,2 +70,8 @@ super(); | ||
} | ||
_abortPromiseForEvent(event) { | ||
return this._disconnectedPromise; | ||
} | ||
_computeDeadline(options) { | ||
return this._timeoutSettings.computeDeadline(options); | ||
} | ||
_didClose() { | ||
@@ -195,10 +202,4 @@ helper_1.assert(!this._closed, 'Page closed twice'); | ||
} | ||
async waitForEvent(event, optionsOrPredicate = {}) { | ||
if (typeof optionsOrPredicate === 'function') | ||
optionsOrPredicate = { predicate: optionsOrPredicate }; | ||
const { timeout = this._timeoutSettings.timeout(), predicate = () => true } = optionsOrPredicate; | ||
return helper_1.helper.waitForEvent(this, event, (...args) => !!predicate(...args), timeout, this._disconnectedPromise); | ||
} | ||
async waitForRequest(urlOrPredicate, options = {}) { | ||
const { timeout = this._timeoutSettings.timeout() } = options; | ||
const deadline = this._timeoutSettings.computeDeadline(options); | ||
return helper_1.helper.waitForEvent(this, events_1.Events.Page.Request, (request) => { | ||
@@ -208,6 +209,6 @@ if (helper_1.helper.isString(urlOrPredicate) || helper_1.helper.isRegExp(urlOrPredicate)) | ||
return urlOrPredicate(request); | ||
}, timeout, this._disconnectedPromise); | ||
}, deadline, this._disconnectedPromise); | ||
} | ||
async waitForResponse(urlOrPredicate, options = {}) { | ||
const { timeout = this._timeoutSettings.timeout() } = options; | ||
const deadline = this._timeoutSettings.computeDeadline(options); | ||
return helper_1.helper.waitForEvent(this, events_1.Events.Page.Response, (response) => { | ||
@@ -217,3 +218,3 @@ if (helper_1.helper.isString(urlOrPredicate) || helper_1.helper.isRegExp(urlOrPredicate)) | ||
return urlOrPredicate(response); | ||
}, timeout, this._disconnectedPromise); | ||
}, deadline, this._disconnectedPromise); | ||
} | ||
@@ -245,3 +246,3 @@ async goBack(options) { | ||
this._state.colorScheme = options.colorScheme; | ||
await this._delegate.setEmulateMedia(this._state.mediaType, this._state.colorScheme); | ||
await this._delegate.updateEmulateMedia(); | ||
} | ||
@@ -379,3 +380,3 @@ async setViewportSize(viewportSize) { | ||
exports.Page = Page; | ||
class Worker extends platform.EventEmitter { | ||
class Worker extends events_2.EventEmitter { | ||
constructor(url) { | ||
@@ -382,0 +383,0 @@ super(); |
@@ -19,4 +19,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs = require("fs"); | ||
const mime = require("mime"); | ||
const util = require("util"); | ||
const helper_1 = require("./helper"); | ||
const platform = require("./platform"); | ||
class Screenshotter { | ||
@@ -132,3 +134,3 @@ constructor(page) { | ||
if (options.path) | ||
await platform.writeFileAsync(options.path, buffer); | ||
await util.promisify(fs.writeFile)(options.path, buffer); | ||
return buffer; | ||
@@ -171,3 +173,3 @@ } | ||
else if (options.path) { | ||
const mimeType = platform.getMimeType(options.path); | ||
const mimeType = mime.getType(options.path); | ||
if (mimeType === 'image/png') | ||
@@ -174,0 +176,0 @@ format = 'png'; |
@@ -18,19 +18,18 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const selectorEvaluatorSource = require("./generated/selectorEvaluatorSource"); | ||
const helper_1 = require("./helper"); | ||
let selectors; | ||
const kEvaluatorSymbol = Symbol('evaluator'); | ||
class Selectors { | ||
constructor() { | ||
this._generation = 0; | ||
// Note: keep in sync with SelectorEvaluator class. | ||
this._builtinEngines = new Set(['css', 'xpath', 'text', 'id', 'data-testid', 'data-test-id', 'data-test']); | ||
this._engines = new Map(); | ||
} | ||
static _instance() { | ||
if (!selectors) | ||
selectors = new Selectors(); | ||
return selectors; | ||
} | ||
async register(name, script) { | ||
async register(name, script, options = {}) { | ||
const { contentScript = false } = options; | ||
if (!name.match(/^[a-zA-Z_0-9-]+$/)) | ||
throw new Error('Selector engine name may only contain [a-zA-Z0-9_] characters'); | ||
// Note: keep in sync with Injected class, and also keep 'zs' for future. | ||
if (['css', 'xpath', 'text', 'id', 'zs', 'data-testid', 'data-test-id', 'data-test'].includes(name)) | ||
// Note: we keep 'zs' for future use. | ||
if (this._builtinEngines.has(name) || name === 'zs') | ||
throw new Error(`"${name}" is a predefined selector engine`); | ||
@@ -40,13 +39,155 @@ const source = await helper_1.helper.evaluationScript(script, undefined, false); | ||
throw new Error(`"${name}" selector engine has been already registered`); | ||
this._engines.set(name, source); | ||
this._engines.set(name, { source, contentScript }); | ||
++this._generation; | ||
} | ||
_needsMainContext(parsed) { | ||
return parsed.some(({ name }) => { | ||
const custom = this._engines.get(name); | ||
return custom ? !custom.contentScript : false; | ||
}); | ||
} | ||
async _prepareEvaluator(context) { | ||
let data = context[kEvaluatorSymbol]; | ||
if (data && data.generation !== this._generation) { | ||
data.promise.then(handle => handle.dispose()); | ||
data = undefined; | ||
} | ||
if (!data) { | ||
const custom = []; | ||
for (const [name, { source }] of this._engines) | ||
custom.push(`{ name: '${name}', engine: (${source}) }`); | ||
const source = ` | ||
new (${selectorEvaluatorSource.source})([ | ||
${custom.join(',\n')} | ||
]) | ||
`; | ||
data = { | ||
promise: context._doEvaluateInternal(false /* returnByValue */, false /* waitForNavigations */, source), | ||
generation: this._generation | ||
}; | ||
context[kEvaluatorSymbol] = data; | ||
} | ||
return data.promise; | ||
} | ||
async _query(frame, selector, scope) { | ||
const parsed = this._parseSelector(selector); | ||
const context = this._needsMainContext(parsed) ? await frame._mainContext() : await frame._utilityContext(); | ||
const handle = await context.evaluateHandleInternal(({ evaluator, parsed, scope }) => evaluator.querySelector(parsed, scope || document), { evaluator: await this._prepareEvaluator(context), parsed, scope }); | ||
const elementHandle = handle.asElement(); | ||
if (!elementHandle) { | ||
handle.dispose(); | ||
return null; | ||
} | ||
const mainContext = await frame._mainContext(); | ||
if (elementHandle._context === mainContext) | ||
return elementHandle; | ||
const adopted = frame._page._delegate.adoptElementHandle(elementHandle, mainContext); | ||
elementHandle.dispose(); | ||
return adopted; | ||
} | ||
async _queryArray(frame, selector, scope) { | ||
const parsed = this._parseSelector(selector); | ||
const context = await frame._mainContext(); | ||
const arrayHandle = await context.evaluateHandleInternal(({ evaluator, parsed, scope }) => evaluator.querySelectorAll(parsed, scope || document), { evaluator: await this._prepareEvaluator(context), parsed, scope }); | ||
return arrayHandle; | ||
} | ||
async _queryAll(frame, selector, scope, allowUtilityContext) { | ||
const parsed = this._parseSelector(selector); | ||
const context = !allowUtilityContext || this._needsMainContext(parsed) ? await frame._mainContext() : await frame._utilityContext(); | ||
const arrayHandle = await context.evaluateHandleInternal(({ evaluator, parsed, scope }) => evaluator.querySelectorAll(parsed, scope || document), { evaluator: await this._prepareEvaluator(context), parsed, scope }); | ||
const properties = await arrayHandle.getProperties(); | ||
arrayHandle.dispose(); | ||
const result = []; | ||
for (const property of properties.values()) { | ||
const elementHandle = property.asElement(); | ||
if (elementHandle) | ||
result.push(elementHandle); | ||
else | ||
property.dispose(); | ||
} | ||
return result; | ||
} | ||
_waitForSelectorTask(selector, waitFor, deadline) { | ||
const parsed = this._parseSelector(selector); | ||
const task = async (context) => context.evaluateHandleInternal(({ evaluator, parsed, waitFor, timeout }) => { | ||
const polling = (waitFor === 'attached' || waitFor === 'detached') ? 'mutation' : 'raf'; | ||
return evaluator.injected.poll(polling, timeout, () => { | ||
const element = evaluator.querySelector(parsed, document); | ||
switch (waitFor) { | ||
case 'attached': | ||
return element || false; | ||
case 'detached': | ||
return !element; | ||
case 'visible': | ||
return element && evaluator.injected.isVisible(element) ? element : false; | ||
case 'hidden': | ||
return !element || !evaluator.injected.isVisible(element); | ||
} | ||
}); | ||
}, { evaluator: await this._prepareEvaluator(context), parsed, waitFor, timeout: helper_1.helper.timeUntilDeadline(deadline) }); | ||
return { world: this._needsMainContext(parsed) ? 'main' : 'utility', task }; | ||
} | ||
async _createSelector(name, handle) { | ||
const mainContext = await handle._page.mainFrame()._mainContext(); | ||
return mainContext.evaluateInternal(({ injected, target, name }) => { | ||
return injected.engines.get(name).create(document.documentElement, target); | ||
}, { injected: await mainContext._injected(), target: handle, name }); | ||
return mainContext.evaluateInternal(({ evaluator, target, name }) => { | ||
return evaluator.engines.get(name).create(document.documentElement, target); | ||
}, { evaluator: await this._prepareEvaluator(mainContext), target: handle, name }); | ||
} | ||
_parseSelector(selector) { | ||
let index = 0; | ||
let quote; | ||
let start = 0; | ||
const result = []; | ||
const append = () => { | ||
const part = selector.substring(start, index).trim(); | ||
const eqIndex = part.indexOf('='); | ||
let name; | ||
let body; | ||
if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-]+$/)) { | ||
name = part.substring(0, eqIndex).trim(); | ||
body = part.substring(eqIndex + 1); | ||
} | ||
else if (part.startsWith('"')) { | ||
name = 'text'; | ||
body = part; | ||
} | ||
else if (/^\(*\/\//.test(part)) { | ||
// If selector starts with '//' or '//' prefixed with multiple opening | ||
// parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817 | ||
name = 'xpath'; | ||
body = part; | ||
} | ||
else { | ||
name = 'css'; | ||
body = part; | ||
} | ||
name = name.toLowerCase(); | ||
if (!this._builtinEngines.has(name) && !this._engines.has(name)) | ||
throw new Error(`Unknown engine ${name} while parsing selector ${selector}`); | ||
result.push({ name, body }); | ||
}; | ||
while (index < selector.length) { | ||
const c = selector[index]; | ||
if (c === '\\' && index + 1 < selector.length) { | ||
index += 2; | ||
} | ||
else if (c === quote) { | ||
quote = undefined; | ||
index++; | ||
} | ||
else if (!quote && c === '>' && selector[index + 1] === '>') { | ||
append(); | ||
index += 2; | ||
start = index; | ||
} | ||
else { | ||
index++; | ||
} | ||
} | ||
append(); | ||
return result; | ||
} | ||
} | ||
exports.Selectors = Selectors; | ||
exports.selectors = new Selectors(); | ||
//# sourceMappingURL=selectors.js.map |
@@ -29,5 +29,4 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const unlinkAsync = platform.promisify(fs.unlink.bind(fs)); | ||
const chmodAsync = platform.promisify(fs.chmod.bind(fs)); | ||
const unlinkAsync = util.promisify(fs.unlink.bind(fs)); | ||
const chmodAsync = util.promisify(fs.chmod.bind(fs)); | ||
const existsAsync = (path) => new Promise(resolve => fs.stat(path, err => resolve(!err))); | ||
@@ -42,2 +41,3 @@ const DEFAULT_DOWNLOAD_HOSTS = { | ||
'linux': '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip', | ||
'mac10.13': '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', | ||
'mac10.14': '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', | ||
@@ -50,2 +50,3 @@ 'mac10.15': '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', | ||
'linux': '%s/builds/firefox/%s/firefox-linux.zip', | ||
'mac10.13': '%s/builds/firefox/%s/firefox-mac.zip', | ||
'mac10.14': '%s/builds/firefox/%s/firefox-mac.zip', | ||
@@ -58,2 +59,3 @@ 'mac10.15': '%s/builds/firefox/%s/firefox-mac.zip', | ||
'linux': '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip', | ||
'mac10.13': undefined, | ||
'mac10.14': '%s/builds/webkit/%s/minibrowser-mac-10.14.zip', | ||
@@ -68,2 +70,3 @@ 'mac10.15': '%s/builds/webkit/%s/minibrowser-mac-10.15.zip', | ||
'linux': ['chrome-linux', 'chrome'], | ||
'mac10.13': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], | ||
'mac10.14': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], | ||
@@ -76,2 +79,3 @@ 'mac10.15': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], | ||
'linux': ['firefox', 'firefox'], | ||
'mac10.13': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], | ||
'mac10.14': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], | ||
@@ -84,2 +88,3 @@ 'mac10.15': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], | ||
'linux': ['pw_run.sh'], | ||
'mac10.13': undefined, | ||
'mac10.14': ['pw_run.sh'], | ||
@@ -114,10 +119,9 @@ 'mac10.15': ['pw_run.sh'], | ||
helper_1.assert(downloadPath, '`downloadPath` must be provided'); | ||
if (await existsAsync(downloadPath)) | ||
return; | ||
const url = revisionURL(options); | ||
const zipPath = path.join(os.tmpdir(), `playwright-download-${browser}-${platform}-${revision}.zip`); | ||
if (await existsAsync(downloadPath)) | ||
throw new Error('ERROR: downloadPath folder already exists!'); | ||
try { | ||
await downloadFile(url, zipPath, progress); | ||
// await mkdirAsync(downloadPath, {recursive: true}); | ||
await extractZip(zipPath, downloadPath); | ||
await extract(zipPath, { dir: downloadPath }); | ||
} | ||
@@ -133,3 +137,5 @@ finally { | ||
const { browser, downloadPath, platform = CURRENT_HOST_PLATFORM, } = options; | ||
return path.join(downloadPath, ...RELATIVE_EXECUTABLE_PATHS[browser][platform]); | ||
const relativePath = RELATIVE_EXECUTABLE_PATHS[browser][platform]; | ||
helper_1.assert(relativePath, `Unsupported platform for ${browser}: ${platform}`); | ||
return path.join(downloadPath, ...relativePath); | ||
} | ||
@@ -180,10 +186,2 @@ exports.executablePath = executablePath; | ||
} | ||
function extractZip(zipPath, folderPath) { | ||
return new Promise((fulfill, reject) => extract(zipPath, { dir: folderPath }, err => { | ||
if (err) | ||
reject(err); | ||
else | ||
fulfill(); | ||
})); | ||
} | ||
function httpRequest(url, method, response) { | ||
@@ -190,0 +188,0 @@ let options = URL.parse(url); |
@@ -19,11 +19,34 @@ "use strict"; | ||
const child_process_1 = require("child_process"); | ||
const platform = require("../platform"); | ||
class BrowserServer extends platform.EventEmitter { | ||
constructor(process, gracefullyClose, wsEndpoint) { | ||
const events_1 = require("events"); | ||
class WebSocketWrapper { | ||
constructor(wsEndpoint, bindings) { | ||
this.wsEndpoint = wsEndpoint; | ||
this._bindings = bindings; | ||
} | ||
async checkLeaks() { | ||
let counter = 0; | ||
return new Promise((fulfill, reject) => { | ||
const check = () => { | ||
const filtered = this._bindings.filter(entry => entry.size); | ||
if (!filtered.length) { | ||
fulfill(); | ||
return; | ||
} | ||
if (++counter >= 50) { | ||
reject(new Error('Web socket leak ' + filtered.map(entry => [...entry.keys()].join(':')).join('|'))); | ||
return; | ||
} | ||
setTimeout(check, 100); | ||
}; | ||
check(); | ||
}); | ||
} | ||
} | ||
exports.WebSocketWrapper = WebSocketWrapper; | ||
class BrowserServer extends events_1.EventEmitter { | ||
constructor(process, gracefullyClose, webSocketWrapper) { | ||
super(); | ||
this._browserWSEndpoint = ''; | ||
this._process = process; | ||
this._gracefullyClose = gracefullyClose; | ||
if (wsEndpoint) | ||
this._browserWSEndpoint = wsEndpoint; | ||
this._webSocketWrapper = webSocketWrapper; | ||
} | ||
@@ -34,3 +57,3 @@ process() { | ||
wsEndpoint() { | ||
return this._browserWSEndpoint; | ||
return this._webSocketWrapper ? this._webSocketWrapper.wsEndpoint : ''; | ||
} | ||
@@ -53,4 +76,8 @@ kill() { | ||
} | ||
async _checkLeaks() { | ||
if (this._webSocketWrapper) | ||
await this._webSocketWrapper.checkLeaks(); | ||
} | ||
} | ||
exports.BrowserServer = BrowserServer; | ||
//# sourceMappingURL=browserServer.js.map |
@@ -22,7 +22,7 @@ "use strict"; | ||
const path = require("path"); | ||
const util = require("util"); | ||
const helper_1 = require("../helper"); | ||
const crBrowser_1 = require("../chromium/crBrowser"); | ||
const platform = require("../platform"); | ||
const errors_1 = require("../errors"); | ||
const processLauncher_1 = require("../server/processLauncher"); | ||
const ws = require("ws"); | ||
const processLauncher_1 = require("./processLauncher"); | ||
const crConnection_1 = require("../chromium/crConnection"); | ||
@@ -32,2 +32,3 @@ const pipeTransport_1 = require("./pipeTransport"); | ||
const events_1 = require("../events"); | ||
const transport_1 = require("../transport"); | ||
class Chromium { | ||
@@ -42,22 +43,24 @@ executablePath() { | ||
} | ||
async launch(options) { | ||
if (options && options.userDataDir) | ||
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const { browserServer, transport } = await this._launchServer(options, 'local'); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, false, options && options.slowMo); | ||
browser['__server__'] = browserServer; | ||
async launch(options = {}) { | ||
helper_1.assert(!options.userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local'); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, false, options.slowMo); | ||
browser._ownedServer = browserServer; | ||
browser._downloadsPath = downloadsPath; | ||
return browser; | ||
} | ||
async launchServer(options) { | ||
return (await this._launchServer(options, 'server', undefined, options && options.port)).browserServer; | ||
async launchServer(options = {}) { | ||
return (await this._launchServer(options, 'server')).browserServer; | ||
} | ||
async launchPersistentContext(userDataDir, options) { | ||
const { timeout = 30000 } = options || {}; | ||
const { transport } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, true); | ||
async launchPersistentContext(userDataDir, options = {}) { | ||
const { timeout = 30000, slowMo = 0 } = options; | ||
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, true, slowMo); | ||
browser._ownedServer = browserServer; | ||
await helper_1.helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout); | ||
return browser._defaultContext; | ||
} | ||
async _launchServer(options = {}, launchType, userDataDir, port) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, timeout = 30000 } = options; | ||
async _launchServer(options, launchType, userDataDir) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, port = 0, } = options; | ||
helper_1.assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); | ||
let temporaryUserDataDir = null; | ||
@@ -70,5 +73,5 @@ if (!userDataDir) { | ||
if (!ignoreDefaultArgs) | ||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0)); | ||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir)); | ||
else if (Array.isArray(ignoreDefaultArgs)) | ||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1)); | ||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1)); | ||
else | ||
@@ -79,4 +82,3 @@ chromeArguments.push(...args); | ||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); | ||
let browserServer = undefined; | ||
const { launchedProcess, gracefullyClose } = await processLauncher_1.launchProcess({ | ||
const { launchedProcess, gracefullyClose, downloadsPath } = await processLauncher_1.launchProcess({ | ||
executablePath: chromeExecutable, | ||
@@ -89,13 +91,12 @@ args: chromeArguments, | ||
dumpio, | ||
pipe: launchType !== 'server', | ||
pipe: true, | ||
tempDir: temporaryUserDataDir || undefined, | ||
attemptToGracefullyClose: async () => { | ||
if (!browserServer) | ||
return Promise.reject(); | ||
helper_1.assert(browserServer); | ||
// We try to gracefully close to prevent crash reporting and core dumps. | ||
// Note that it's fine to reuse the pipe transport, since | ||
// our connection ignores kBrowserCloseMessageId. | ||
const t = transport || await platform.connectToWebsocket(browserWSEndpoint, async (transport) => transport); | ||
const message = { method: 'Browser.close', id: crConnection_1.kBrowserCloseMessageId }; | ||
await t.send(JSON.stringify(message)); | ||
const t = transport; | ||
const message = { method: 'Browser.close', id: crConnection_1.kBrowserCloseMessageId, params: {} }; | ||
t.send(message); | ||
}, | ||
@@ -107,24 +108,15 @@ onkill: (exitCode, signal) => { | ||
}); | ||
let transport; | ||
let browserWSEndpoint; | ||
if (launchType === 'server') { | ||
const timeoutError = new errors_1.TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chromium!`); | ||
const match = await processLauncher_1.waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError); | ||
browserWSEndpoint = match[1]; | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, browserWSEndpoint); | ||
return { browserServer }; | ||
} | ||
else { | ||
// For local launch scenario close will terminate the browser process. | ||
transport = new pipeTransport_1.PipeTransport(launchedProcess.stdio[3], launchedProcess.stdio[4], () => browserServer.close()); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, null); | ||
return { browserServer, transport }; | ||
} | ||
let transport = undefined; | ||
let browserServer = undefined; | ||
const stdio = launchedProcess.stdio; | ||
transport = new pipeTransport_1.PipeTransport(stdio[3], stdio[4]); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port) : null); | ||
return { browserServer, transport, downloadsPath }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return crBrowser_1.CRBrowser.connect(transport, false, options.slowMo); | ||
}); | ||
} | ||
_defaultArgs(options = {}, launchType, userDataDir, port) { | ||
_defaultArgs(options = {}, launchType, userDataDir) { | ||
const { devtools = false, headless = !devtools, args = [], } = options; | ||
@@ -140,3 +132,3 @@ const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); | ||
chromeArguments.push(`--user-data-dir=${userDataDir}`); | ||
chromeArguments.push(launchType === 'server' ? `--remote-debugging-port=${port || 0}` : '--remote-debugging-pipe'); | ||
chromeArguments.push('--remote-debugging-pipe'); | ||
if (devtools) | ||
@@ -159,3 +151,109 @@ chromeArguments.push('--auto-open-devtools-for-tabs'); | ||
exports.Chromium = Chromium; | ||
const mkdtempAsync = platform.promisify(fs.mkdtemp); | ||
function wrapTransportWithWebSocket(transport, port) { | ||
const server = new ws.Server({ port }); | ||
const guid = helper_1.helper.guid(); | ||
const awaitingBrowserTarget = new Map(); | ||
const sessionToSocket = new Map(); | ||
const socketToBrowserSession = new Map(); | ||
const browserSessions = new Set(); | ||
let lastSequenceNumber = 1; | ||
transport.onmessage = message => { | ||
if (typeof message.id === 'number' && awaitingBrowserTarget.has(message.id)) { | ||
const freshSocket = awaitingBrowserTarget.get(message.id); | ||
awaitingBrowserTarget.delete(message.id); | ||
const sessionId = message.result.sessionId; | ||
if (freshSocket.readyState !== ws.CLOSED && freshSocket.readyState !== ws.CLOSING) { | ||
sessionToSocket.set(sessionId, freshSocket); | ||
const { queue } = socketToBrowserSession.get(freshSocket); | ||
for (const item of queue) { | ||
item.sessionId = sessionId; | ||
transport.send(item); | ||
} | ||
socketToBrowserSession.set(freshSocket, { sessionId }); | ||
browserSessions.add(sessionId); | ||
} | ||
else { | ||
transport.send({ | ||
id: ++lastSequenceNumber, | ||
method: 'Target.detachFromTarget', | ||
params: { sessionId } | ||
}); | ||
socketToBrowserSession.delete(freshSocket); | ||
} | ||
return; | ||
} | ||
// At this point everything we care about has sessionId. | ||
if (!message.sessionId) | ||
return; | ||
const socket = sessionToSocket.get(message.sessionId); | ||
if (socket && socket.readyState !== ws.CLOSING) { | ||
if (message.method === 'Target.attachedToTarget') | ||
sessionToSocket.set(message.params.sessionId, socket); | ||
if (message.method === 'Target.detachedFromTarget') | ||
sessionToSocket.delete(message.params.sessionId); | ||
// Strip session ids from the browser sessions. | ||
if (browserSessions.has(message.sessionId)) | ||
delete message.sessionId; | ||
socket.send(JSON.stringify(message)); | ||
} | ||
}; | ||
transport.onclose = () => { | ||
for (const socket of socketToBrowserSession.keys()) { | ||
socket.removeListener('close', socket.__closeListener); | ||
socket.close(undefined, 'Browser disconnected'); | ||
} | ||
server.close(); | ||
transport.onmessage = undefined; | ||
transport.onclose = undefined; | ||
}; | ||
server.on('connection', (socket, req) => { | ||
if (req.url !== '/' + guid) { | ||
socket.close(); | ||
return; | ||
} | ||
socketToBrowserSession.set(socket, { queue: [] }); | ||
transport.send({ | ||
id: ++lastSequenceNumber, | ||
method: 'Target.attachToBrowserTarget', | ||
params: {} | ||
}); | ||
awaitingBrowserTarget.set(lastSequenceNumber, socket); | ||
socket.on('message', (message) => { | ||
const parsedMessage = JSON.parse(Buffer.from(message).toString()); | ||
// If message has sessionId, pass through. | ||
if (parsedMessage.sessionId) { | ||
transport.send(parsedMessage); | ||
return; | ||
} | ||
// If message has no sessionId, look it up. | ||
const session = socketToBrowserSession.get(socket); | ||
if (session.sessionId) { | ||
// We have it, use it. | ||
parsedMessage.sessionId = session.sessionId; | ||
transport.send(parsedMessage); | ||
return; | ||
} | ||
// Pending session id, queue the message. | ||
session.queue.push(parsedMessage); | ||
}); | ||
socket.on('error', error => helper_1.debugError(error)); | ||
socket.on('close', socket.__closeListener = () => { | ||
const session = socketToBrowserSession.get(socket); | ||
if (!session || !session.sessionId) | ||
return; | ||
sessionToSocket.delete(session.sessionId); | ||
browserSessions.delete(session.sessionId); | ||
socketToBrowserSession.delete(socket); | ||
transport.send({ | ||
id: ++lastSequenceNumber, | ||
method: 'Target.detachFromTarget', | ||
params: { sessionId: session.sessionId } | ||
}); | ||
}); | ||
}); | ||
const address = server.address(); | ||
const wsEndpoint = typeof address === 'string' ? `${address}/${guid}` : `ws://127.0.0.1:${address.port}/${guid}`; | ||
return new browserServer_1.WebSocketWrapper(wsEndpoint, [awaitingBrowserTarget, sessionToSocket, socketToBrowserSession, browserSessions]); | ||
} | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
const CHROMIUM_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-'); | ||
@@ -162,0 +260,0 @@ const DEFAULT_ARGS = [ |
@@ -22,2 +22,4 @@ "use strict"; | ||
const path = require("path"); | ||
const util = require("util"); | ||
const ws = require("ws"); | ||
const errors_1 = require("../errors"); | ||
@@ -28,6 +30,6 @@ const events_1 = require("../events"); | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const browserServer_1 = require("./browserServer"); | ||
const processLauncher_1 = require("./processLauncher"); | ||
const mkdtempAsync = platform.promisify(fs.mkdtemp); | ||
const transport_1 = require("../transport"); | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
class Firefox { | ||
@@ -42,31 +44,30 @@ executablePath() { | ||
} | ||
async launch(options) { | ||
if (options && options.userDataDir) | ||
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const browserServer = await this._launchServer(options, 'local'); | ||
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, false, options && options.slowMo); | ||
async launch(options = {}) { | ||
helper_1.assert(!options.userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const { browserServer, downloadsPath } = await this._launchServer(options, 'local'); | ||
const browser = await transport_1.WebSocketTransport.connect(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, false, options.slowMo); | ||
}); | ||
// Hack: for typical launch scenario, ensure that close waits for actual process termination. | ||
browser.close = () => browserServer.close(); | ||
browser['__server__'] = browserServer; | ||
browser._ownedServer = browserServer; | ||
browser._downloadsPath = downloadsPath; | ||
return browser; | ||
} | ||
async launchServer(options) { | ||
return await this._launchServer(options, 'server', undefined, options && options.port); | ||
async launchServer(options = {}) { | ||
return (await this._launchServer(options, 'server')).browserServer; | ||
} | ||
async launchPersistentContext(userDataDir, options) { | ||
const { timeout = 30000 } = options || {}; | ||
const browserServer = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, true); | ||
async launchPersistentContext(userDataDir, options = {}) { | ||
const { timeout = 30000, slowMo = 0, } = options; | ||
const { browserServer, downloadsPath } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await transport_1.WebSocketTransport.connect(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, true, slowMo); | ||
}); | ||
browser._ownedServer = browserServer; | ||
browser._downloadsPath = downloadsPath; | ||
await helper_1.helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout); | ||
// Hack: for typical launch scenario, ensure that close waits for actual process termination. | ||
const browserContext = browser._defaultContext; | ||
browserContext.close = () => browserServer.close(); | ||
return browserContext; | ||
} | ||
async _launchServer(options = {}, launchType, userDataDir, port) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGHUP = true, handleSIGINT = true, handleSIGTERM = true, timeout = 30000, } = options; | ||
async _launchServer(options, launchType, userDataDir) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGHUP = true, handleSIGINT = true, handleSIGTERM = true, timeout = 30000, port = 0, } = options; | ||
helper_1.assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); | ||
const firefoxArguments = []; | ||
@@ -79,5 +80,5 @@ let temporaryProfileDir = null; | ||
if (!ignoreDefaultArgs) | ||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0)); | ||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0)); | ||
else if (Array.isArray(ignoreDefaultArgs)) | ||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0).filter(arg => !ignoreDefaultArgs.includes(arg))); | ||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0).filter(arg => !ignoreDefaultArgs.includes(arg))); | ||
else | ||
@@ -88,4 +89,3 @@ firefoxArguments.push(...args); | ||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); | ||
let browserServer = undefined; | ||
const { launchedProcess, gracefullyClose } = await processLauncher_1.launchProcess({ | ||
const { launchedProcess, gracefullyClose, downloadsPath } = await processLauncher_1.launchProcess({ | ||
executablePath: firefoxExecutable, | ||
@@ -105,10 +105,7 @@ args: firefoxArguments, | ||
attemptToGracefullyClose: async () => { | ||
if (!browserServer) | ||
return Promise.reject(); | ||
helper_1.assert(browserServer); | ||
// We try to gracefully close to prevent crash reporting and core dumps. | ||
// Note that it's fine to reuse the pipe transport, since | ||
// our connection ignores kBrowserCloseMessageId. | ||
const transport = await platform.connectToWebsocket(browserWSEndpoint, async (transport) => transport); | ||
const transport = await transport_1.WebSocketTransport.connect(browserWSEndpoint, async (transport) => transport); | ||
const message = { method: 'Browser.close', params: {}, id: ffConnection_1.kBrowserCloseMessageId }; | ||
await transport.send(JSON.stringify(message)); | ||
await transport.send(message); | ||
}, | ||
@@ -122,8 +119,12 @@ onkill: (exitCode, signal) => { | ||
const match = await processLauncher_1.waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError); | ||
const browserWSEndpoint = match[1]; | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, browserWSEndpoint); | ||
return browserServer; | ||
const innerEndpoint = match[1]; | ||
let browserServer = undefined; | ||
let browserWSEndpoint = undefined; | ||
const webSocketWrapper = launchType === 'server' ? (await transport_1.WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new browserServer_1.WebSocketWrapper(innerEndpoint, []); | ||
browserWSEndpoint = webSocketWrapper.wsEndpoint; | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); | ||
return { browserServer, downloadsPath }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, false, options.slowMo); | ||
@@ -135,3 +136,3 @@ }); | ||
if (devtools) | ||
throw new Error('Option "devtools" is not supported by Firefox'); | ||
console.warn('devtools parameter is not supported as a launch argument in Firefox. You can launch the devtools window manually.'); | ||
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile')); | ||
@@ -161,2 +162,117 @@ if (userDataDirArg) | ||
exports.Firefox = Firefox; | ||
function wrapTransportWithWebSocket(transport, port) { | ||
const server = new ws.Server({ port }); | ||
const guid = helper_1.helper.guid(); | ||
const idMixer = new transport_1.SequenceNumberMixer(); | ||
const pendingBrowserContextCreations = new Set(); | ||
const pendingBrowserContextDeletions = new Map(); | ||
const browserContextIds = new Map(); | ||
const sessionToSocket = new Map(); | ||
const sockets = new Set(); | ||
transport.onmessage = message => { | ||
if (typeof message.id === 'number') { | ||
// Process command response. | ||
const seqNum = message.id; | ||
const value = idMixer.take(seqNum); | ||
if (!value) | ||
return; | ||
const { id, socket } = value; | ||
if (socket.readyState === ws.CLOSING) { | ||
if (pendingBrowserContextCreations.has(id)) { | ||
transport.send({ | ||
id: ++transport_1.SequenceNumberMixer._lastSequenceNumber, | ||
method: 'Browser.removeBrowserContext', | ||
params: { browserContextId: message.result.browserContextId } | ||
}); | ||
} | ||
return; | ||
} | ||
if (pendingBrowserContextCreations.has(seqNum)) { | ||
// Browser.createBrowserContext response -> establish context attribution. | ||
browserContextIds.set(message.result.browserContextId, socket); | ||
pendingBrowserContextCreations.delete(seqNum); | ||
} | ||
const deletedContextId = pendingBrowserContextDeletions.get(seqNum); | ||
if (deletedContextId) { | ||
// Browser.removeBrowserContext response -> remove context attribution. | ||
browserContextIds.delete(deletedContextId); | ||
pendingBrowserContextDeletions.delete(seqNum); | ||
} | ||
message.id = id; | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
// Process notification response. | ||
const { method, params, sessionId } = message; | ||
if (sessionId) { | ||
const socket = sessionToSocket.get(sessionId); | ||
if (!socket || socket.readyState === ws.CLOSING) { | ||
// Drop unattributed messages on the floor. | ||
return; | ||
} | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
if (method === 'Browser.attachedToTarget') { | ||
const socket = browserContextIds.get(params.targetInfo.browserContextId); | ||
if (!socket || socket.readyState === ws.CLOSING) { | ||
// Drop unattributed messages on the floor. | ||
return; | ||
} | ||
sessionToSocket.set(params.sessionId, socket); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
if (method === 'Browser.detachedFromTarget') { | ||
const socket = sessionToSocket.get(params.sessionId); | ||
sessionToSocket.delete(params.sessionId); | ||
if (socket && socket.readyState !== ws.CLOSING) | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
}; | ||
transport.onclose = () => { | ||
for (const socket of sockets) { | ||
socket.removeListener('close', socket.__closeListener); | ||
socket.close(undefined, 'Browser disconnected'); | ||
} | ||
server.close(); | ||
transport.onmessage = undefined; | ||
transport.onclose = undefined; | ||
}; | ||
server.on('connection', (socket, req) => { | ||
if (req.url !== '/' + guid) { | ||
socket.close(); | ||
return; | ||
} | ||
sockets.add(socket); | ||
socket.on('message', (message) => { | ||
const parsedMessage = JSON.parse(Buffer.from(message).toString()); | ||
const { id, method, params } = parsedMessage; | ||
const seqNum = idMixer.generate({ id, socket }); | ||
transport.send({ ...parsedMessage, id: seqNum }); | ||
if (method === 'Browser.createBrowserContext') | ||
pendingBrowserContextCreations.add(seqNum); | ||
if (method === 'Browser.removeBrowserContext') | ||
pendingBrowserContextDeletions.set(seqNum, params.browserContextId); | ||
}); | ||
socket.on('error', error => helper_1.debugError(error)); | ||
socket.on('close', socket.__closeListener = () => { | ||
for (const [browserContextId, s] of browserContextIds) { | ||
if (s === socket) { | ||
transport.send({ | ||
id: ++transport_1.SequenceNumberMixer._lastSequenceNumber, | ||
method: 'Browser.removeBrowserContext', | ||
params: { browserContextId } | ||
}); | ||
browserContextIds.delete(browserContextId); | ||
} | ||
} | ||
sockets.delete(socket); | ||
}); | ||
}); | ||
const address = server.address(); | ||
const wsEndpoint = typeof address === 'string' ? `${address}/${guid}` : `ws://127.0.0.1:${address.port}/${guid}`; | ||
return new browserServer_1.WebSocketWrapper(wsEndpoint, [pendingBrowserContextCreations, pendingBrowserContextDeletions, browserContextIds, sessionToSocket, sockets]); | ||
} | ||
//# sourceMappingURL=firefox.js.map |
@@ -20,9 +20,7 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const platform_1 = require("../platform"); | ||
class PipeTransport { | ||
constructor(pipeWrite, pipeRead, closeCallback) { | ||
constructor(pipeWrite, pipeRead) { | ||
this._pendingMessage = ''; | ||
this._waitForNextTask = platform_1.makeWaitForNextTask(); | ||
this._waitForNextTask = helper_1.helper.makeWaitForNextTask(); | ||
this._pipeWrite = pipeWrite; | ||
this._closeCallback = closeCallback; | ||
this._eventListeners = [ | ||
@@ -42,7 +40,7 @@ helper_1.helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)), | ||
send(message) { | ||
this._pipeWrite.write(message); | ||
this._pipeWrite.write(JSON.stringify(message)); | ||
this._pipeWrite.write('\0'); | ||
} | ||
close() { | ||
this._closeCallback(); | ||
throw new Error('unimplemented'); | ||
} | ||
@@ -58,3 +56,3 @@ _dispatch(buffer) { | ||
if (this.onmessage) | ||
this.onmessage.call(null, message); | ||
this.onmessage.call(null, JSON.parse(message)); | ||
}); | ||
@@ -67,3 +65,3 @@ let start = end + 1; | ||
if (this.onmessage) | ||
this.onmessage.call(null, message); | ||
this.onmessage.call(null, JSON.parse(message)); | ||
}); | ||
@@ -70,0 +68,0 @@ start = end + 1; |
@@ -25,2 +25,3 @@ "use strict"; | ||
const firefox_1 = require("./firefox"); | ||
const selectors_1 = require("../selectors"); | ||
for (const className in api) { | ||
@@ -32,3 +33,3 @@ if (typeof api[className] === 'function') | ||
constructor(options) { | ||
this.selectors = api.Selectors._instance(); | ||
this.selectors = selectors_1.selectors; | ||
const { browsers, } = options; | ||
@@ -35,0 +36,0 @@ this.devices = deviceDescriptors_1.DeviceDescriptors; |
@@ -20,13 +20,19 @@ "use strict"; | ||
const childProcess = require("child_process"); | ||
const debug = require("debug"); | ||
const fs = require("fs"); | ||
const os = require("os"); | ||
const path = require("path"); | ||
const readline = require("readline"); | ||
const removeFolder = require("rimraf"); | ||
const util = require("util"); | ||
const helper_1 = require("../helper"); | ||
const readline = require("readline"); | ||
const platform = require("../platform"); | ||
const removeFolderAsync = platform.promisify(removeFolder); | ||
const removeFolderAsync = util.promisify(removeFolder); | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); | ||
let lastLaunchedId = 0; | ||
async function launchProcess(options) { | ||
const id = ++lastLaunchedId; | ||
const debugBrowser = platform.debug(`pw:browser:proc:[${id}]`); | ||
const debugBrowserOut = platform.debug(`pw:browser:out:[${id}]`); | ||
const debugBrowserErr = platform.debug(`pw:browser:err:[${id}]`); | ||
const debugBrowser = debug(`pw:browser:proc:[${id}]`); | ||
const debugBrowserOut = debug(`pw:browser:out:[${id}]`); | ||
const debugBrowserErr = debug(`pw:browser:err:[${id}]`); | ||
debugBrowser.color = '33'; | ||
@@ -65,2 +71,3 @@ debugBrowserOut.color = '178'; | ||
}); | ||
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER); | ||
let processClosed = false; | ||
@@ -74,10 +81,6 @@ const waitForProcessToClose = new Promise((fulfill, reject) => { | ||
// Cleanup as processes exit. | ||
if (options.tempDir) { | ||
removeFolderAsync(options.tempDir) | ||
.catch((err) => console.error(err)) | ||
.then(fulfill); | ||
} | ||
else { | ||
fulfill(); | ||
} | ||
Promise.all([ | ||
removeFolderAsync(downloadsPath), | ||
options.tempDir ? removeFolderAsync(options.tempDir) : Promise.resolve() | ||
]).catch((err) => console.error(err)).then(fulfill); | ||
}); | ||
@@ -108,3 +111,3 @@ }); | ||
debugBrowser(`<gracefully close start>`); | ||
options.attemptToGracefullyClose().catch(() => killProcess()); | ||
await options.attemptToGracefullyClose().catch(() => killProcess()); | ||
await waitForProcessToClose; | ||
@@ -136,3 +139,3 @@ debugBrowser(`<gracefully close end>`); | ||
} | ||
return { launchedProcess: spawnedProcess, gracefullyClose }; | ||
return { launchedProcess: spawnedProcess, gracefullyClose, downloadsPath }; | ||
} | ||
@@ -139,0 +142,0 @@ exports.launchProcess = launchProcess; |
@@ -24,6 +24,7 @@ "use strict"; | ||
const path = require("path"); | ||
const platform = require("../platform"); | ||
const os = require("os"); | ||
const util = require("util"); | ||
const helper_1 = require("../helper"); | ||
const wkConnection_1 = require("../webkit/wkConnection"); | ||
const transport_1 = require("../transport"); | ||
const ws = require("ws"); | ||
@@ -41,22 +42,24 @@ const browserServer_1 = require("./browserServer"); | ||
} | ||
async launch(options) { | ||
if (options && options.userDataDir) | ||
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const { browserServer, transport } = await this._launchServer(options, 'local'); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, options && options.slowMo); | ||
browser['__server__'] = browserServer; | ||
async launch(options = {}) { | ||
helper_1.assert(!options.userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); | ||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local'); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, options.slowMo, false); | ||
browser._ownedServer = browserServer; | ||
browser._downloadsPath = downloadsPath; | ||
return browser; | ||
} | ||
async launchServer(options) { | ||
return (await this._launchServer(options, 'server', undefined, options && options.port)).browserServer; | ||
async launchServer(options = {}) { | ||
return (await this._launchServer(options, 'server')).browserServer; | ||
} | ||
async launchPersistentContext(userDataDir, options) { | ||
const { timeout = 30000 } = options || {}; | ||
const { transport } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, undefined, true); | ||
async launchPersistentContext(userDataDir, options = {}) { | ||
const { timeout = 30000, slowMo = 0, } = options; | ||
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, slowMo, true); | ||
browser._ownedServer = browserServer; | ||
await helper_1.helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout); | ||
return browser._defaultContext; | ||
} | ||
async _launchServer(options = {}, launchType, userDataDir, port) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, } = options; | ||
async _launchServer(options, launchType, userDataDir) { | ||
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, port = 0, } = options; | ||
helper_1.assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); | ||
let temporaryUserDataDir = null; | ||
@@ -69,5 +72,5 @@ if (!userDataDir) { | ||
if (!ignoreDefaultArgs) | ||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0)); | ||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port)); | ||
else if (Array.isArray(ignoreDefaultArgs)) | ||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port || 0).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1)); | ||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1)); | ||
else | ||
@@ -78,5 +81,3 @@ webkitArguments.push(...args); | ||
throw new Error(`No executable path is specified.`); | ||
let transport = undefined; | ||
let browserServer = undefined; | ||
const { launchedProcess, gracefullyClose } = await processLauncher_1.launchProcess({ | ||
const { launchedProcess, gracefullyClose, downloadsPath } = await processLauncher_1.launchProcess({ | ||
executablePath: webkitExecutable, | ||
@@ -92,9 +93,7 @@ args: webkitArguments, | ||
attemptToGracefullyClose: async () => { | ||
if (!transport) | ||
return Promise.reject(); | ||
helper_1.assert(transport); | ||
// We try to gracefully close to prevent crash reporting and core dumps. | ||
// Note that it's fine to reuse the pipe transport, since | ||
// our connection ignores kBrowserCloseMessageId. | ||
const message = JSON.stringify({ method: 'Playwright.close', params: {}, id: wkConnection_1.kBrowserCloseMessageId }); | ||
transport.send(message); | ||
await transport.send({ method: 'Playwright.close', params: {}, id: wkConnection_1.kBrowserCloseMessageId }); | ||
}, | ||
@@ -107,10 +106,11 @@ onkill: (exitCode, signal) => { | ||
// For local launch scenario close will terminate the browser process. | ||
transport = new pipeTransport_1.PipeTransport(launchedProcess.stdio[3], launchedProcess.stdio[4], () => browserServer.close()); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? await wrapTransportWithWebSocket(transport, port || 0) : null); | ||
if (launchType === 'server') | ||
return { browserServer }; | ||
return { browserServer, transport }; | ||
let transport = undefined; | ||
let browserServer = undefined; | ||
const stdio = launchedProcess.stdio; | ||
transport = new pipeTransport_1.PipeTransport(stdio[3], stdio[4]); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port || 0) : null); | ||
return { browserServer, transport, downloadsPath }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return wkBrowser_1.WKBrowser.connect(transport, options.slowMo); | ||
@@ -122,3 +122,3 @@ }); | ||
if (devtools) | ||
throw new Error('Option "devtools" is not supported by WebKit'); | ||
console.warn('devtools parameter as a launch argument in WebKit is not supported. Also starting Web Inspector manually will terminate the execution in WebKit.'); | ||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir=')); | ||
@@ -141,24 +141,8 @@ if (userDataDirArg) | ||
exports.WebKit = WebKit; | ||
const mkdtempAsync = platform.promisify(fs.mkdtemp); | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-'); | ||
class SequenceNumberMixer { | ||
constructor() { | ||
this._values = new Map(); | ||
} | ||
generate(value) { | ||
const sequenceNumber = ++SequenceNumberMixer._lastSequenceNumber; | ||
this._values.set(sequenceNumber, value); | ||
return sequenceNumber; | ||
} | ||
take(sequenceNumber) { | ||
const value = this._values.get(sequenceNumber); | ||
this._values.delete(sequenceNumber); | ||
return value; | ||
} | ||
} | ||
SequenceNumberMixer._lastSequenceNumber = 1; | ||
function wrapTransportWithWebSocket(transport, port) { | ||
const server = new ws.Server({ port }); | ||
const guid = platform.guid(); | ||
const idMixer = new SequenceNumberMixer(); | ||
const guid = helper_1.helper.guid(); | ||
const idMixer = new transport_1.SequenceNumberMixer(); | ||
const pendingBrowserContextCreations = new Set(); | ||
@@ -170,38 +154,37 @@ const pendingBrowserContextDeletions = new Map(); | ||
transport.onmessage = message => { | ||
const parsedMessage = JSON.parse(message); | ||
if ('id' in parsedMessage) { | ||
if (parsedMessage.id === -9999) | ||
if (typeof message.id === 'number') { | ||
if (message.id === -9999) | ||
return; | ||
// Process command response. | ||
const value = idMixer.take(parsedMessage.id); | ||
const value = idMixer.take(message.id); | ||
if (!value) | ||
return; | ||
const { id, socket } = value; | ||
if (!socket || socket.readyState === ws.CLOSING) { | ||
if (socket.readyState === ws.CLOSING) { | ||
if (pendingBrowserContextCreations.has(id)) { | ||
transport.send(JSON.stringify({ | ||
id: ++SequenceNumberMixer._lastSequenceNumber, | ||
transport.send({ | ||
id: ++transport_1.SequenceNumberMixer._lastSequenceNumber, | ||
method: 'Playwright.deleteContext', | ||
params: { browserContextId: parsedMessage.result.browserContextId } | ||
})); | ||
params: { browserContextId: message.result.browserContextId } | ||
}); | ||
} | ||
return; | ||
} | ||
if (pendingBrowserContextCreations.has(parsedMessage.id)) { | ||
if (pendingBrowserContextCreations.has(message.id)) { | ||
// Browser.createContext response -> establish context attribution. | ||
browserContextIds.set(parsedMessage.result.browserContextId, socket); | ||
pendingBrowserContextCreations.delete(parsedMessage.id); | ||
browserContextIds.set(message.result.browserContextId, socket); | ||
pendingBrowserContextCreations.delete(message.id); | ||
} | ||
const deletedContextId = pendingBrowserContextDeletions.get(parsedMessage.id); | ||
const deletedContextId = pendingBrowserContextDeletions.get(message.id); | ||
if (deletedContextId) { | ||
// Browser.deleteContext response -> remove context attribution. | ||
browserContextIds.delete(deletedContextId); | ||
pendingBrowserContextDeletions.delete(parsedMessage.id); | ||
pendingBrowserContextDeletions.delete(message.id); | ||
} | ||
parsedMessage.id = id; | ||
socket.send(JSON.stringify(parsedMessage)); | ||
message.id = id; | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
// Process notification response. | ||
const { method, params, pageProxyId } = parsedMessage; | ||
const { method, params, pageProxyId } = message; | ||
if (pageProxyId) { | ||
@@ -213,3 +196,3 @@ const socket = pageProxyIds.get(pageProxyId); | ||
} | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -224,3 +207,3 @@ } | ||
pageProxyIds.set(params.pageProxyInfo.pageProxyId, socket); | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -232,3 +215,3 @@ } | ||
if (socket && socket.readyState !== ws.CLOSING) | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -239,6 +222,15 @@ } | ||
if (socket && socket.readyState !== ws.CLOSING) | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
} | ||
}; | ||
transport.onclose = () => { | ||
for (const socket of sockets) { | ||
socket.removeListener('close', socket.__closeListener); | ||
socket.close(undefined, 'Browser disconnected'); | ||
} | ||
server.close(); | ||
transport.onmessage = undefined; | ||
transport.onclose = undefined; | ||
}; | ||
server.on('connection', (socket, req) => { | ||
@@ -254,3 +246,3 @@ if (req.url !== '/' + guid) { | ||
const seqNum = idMixer.generate({ id, socket }); | ||
transport.send(JSON.stringify({ ...parsedMessage, id: seqNum })); | ||
transport.send({ ...parsedMessage, id: seqNum }); | ||
if (method === 'Playwright.createContext') | ||
@@ -261,2 +253,3 @@ pendingBrowserContextCreations.add(seqNum); | ||
}); | ||
socket.on('error', error => helper_1.debugError(error)); | ||
socket.on('close', socket.__closeListener = () => { | ||
@@ -269,7 +262,7 @@ for (const [pageProxyId, s] of pageProxyIds) { | ||
if (s === socket) { | ||
transport.send(JSON.stringify({ | ||
id: ++SequenceNumberMixer._lastSequenceNumber, | ||
transport.send({ | ||
id: ++transport_1.SequenceNumberMixer._lastSequenceNumber, | ||
method: 'Playwright.deleteContext', | ||
params: { browserContextId } | ||
})); | ||
}); | ||
browserContextIds.delete(browserContextId); | ||
@@ -281,16 +274,6 @@ } | ||
}); | ||
transport.onclose = () => { | ||
for (const socket of sockets) { | ||
socket.removeListener('close', socket.__closeListener); | ||
socket.close(undefined, 'Browser disconnected'); | ||
} | ||
server.close(); | ||
transport.onmessage = undefined; | ||
transport.onclose = undefined; | ||
}; | ||
const address = server.address(); | ||
if (typeof address === 'string') | ||
return address + '/' + guid; | ||
return 'ws://127.0.0.1:' + address.port + '/' + guid; | ||
const wsEndpoint = typeof address === 'string' ? `${address}/${guid}` : `ws://127.0.0.1:${address.port}/${guid}`; | ||
return new browserServer_1.WebSocketWrapper(wsEndpoint, [pendingBrowserContextCreations, pendingBrowserContextDeletions, browserContextIds, pageProxyIds, sockets]); | ||
} | ||
//# sourceMappingURL=webkit.js.map |
@@ -19,2 +19,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const helper_1 = require("./helper"); | ||
const DEFAULT_TIMEOUT = 30000; | ||
@@ -42,11 +43,19 @@ class TimeoutSettings { | ||
} | ||
timeout() { | ||
_timeout() { | ||
if (this._defaultTimeout !== null) | ||
return this._defaultTimeout; | ||
if (this._parent) | ||
return this._parent.timeout(); | ||
return this._parent._timeout(); | ||
return DEFAULT_TIMEOUT; | ||
} | ||
computeDeadline(options) { | ||
const { timeout } = options || {}; | ||
if (timeout === 0) | ||
return Number.MAX_SAFE_INTEGER; | ||
else if (typeof timeout === 'number') | ||
return helper_1.helper.monotonicTime() + timeout; | ||
return helper_1.helper.monotonicTime() + this._timeout(); | ||
} | ||
} | ||
exports.TimeoutSettings = TimeoutSettings; | ||
//# sourceMappingURL=timeoutSettings.js.map |
@@ -19,2 +19,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const debug = require("debug"); | ||
const WebSocket = require("ws"); | ||
const helper_1 = require("./helper"); | ||
class SlowMoTransport { | ||
@@ -56,3 +59,3 @@ constructor(transport, delay) { | ||
this._readPromise = new Promise(f => callback = f); | ||
this._delegate.onmessage = s => { | ||
this._delegate.onmessage = (s) => { | ||
callback(); | ||
@@ -76,2 +79,87 @@ if (this.onmessage) | ||
exports.DeferWriteTransport = DeferWriteTransport; | ||
class WebSocketTransport { | ||
constructor(url) { | ||
this._ws = new WebSocket(url, [], { | ||
perMessageDeflate: false, | ||
maxPayload: 256 * 1024 * 1024, | ||
}); | ||
// The 'ws' module in node sometimes sends us multiple messages in a single task. | ||
// In Web, all IO callbacks (e.g. WebSocket callbacks) | ||
// are dispatched into separate tasks, so there's no need | ||
// to do anything extra. | ||
const messageWrap = helper_1.helper.makeWaitForNextTask(); | ||
this._ws.addEventListener('message', event => { | ||
messageWrap(() => { | ||
if (this.onmessage) | ||
this.onmessage.call(null, JSON.parse(event.data)); | ||
}); | ||
}); | ||
this._ws.addEventListener('close', event => { | ||
if (this.onclose) | ||
this.onclose.call(null); | ||
}); | ||
// Silently ignore all errors - we don't know what to do with them. | ||
this._ws.addEventListener('error', () => { }); | ||
} | ||
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to | ||
// avoid missing incoming messages. | ||
static connect(url, onopen) { | ||
const transport = new WebSocketTransport(url); | ||
return new Promise((fulfill, reject) => { | ||
transport._ws.addEventListener('open', async () => fulfill(await onopen(transport))); | ||
transport._ws.addEventListener('error', event => reject(new Error('WebSocket error: ' + event.message))); | ||
}); | ||
} | ||
send(message) { | ||
this._ws.send(JSON.stringify(message)); | ||
} | ||
close() { | ||
this._ws.close(); | ||
} | ||
} | ||
exports.WebSocketTransport = WebSocketTransport; | ||
class SequenceNumberMixer { | ||
constructor() { | ||
this._values = new Map(); | ||
} | ||
generate(value) { | ||
const sequenceNumber = ++SequenceNumberMixer._lastSequenceNumber; | ||
this._values.set(sequenceNumber, value); | ||
return sequenceNumber; | ||
} | ||
take(sequenceNumber) { | ||
const value = this._values.get(sequenceNumber); | ||
this._values.delete(sequenceNumber); | ||
return value; | ||
} | ||
} | ||
exports.SequenceNumberMixer = SequenceNumberMixer; | ||
SequenceNumberMixer._lastSequenceNumber = 1; | ||
class InterceptingTransport { | ||
constructor(transport, interceptor) { | ||
this._delegate = transport; | ||
this._interceptor = interceptor; | ||
this._delegate.onmessage = this._onmessage.bind(this); | ||
this._delegate.onclose = this._onClose.bind(this); | ||
} | ||
_onmessage(message) { | ||
if (this.onmessage) | ||
this.onmessage(message); | ||
} | ||
_onClose() { | ||
if (this.onclose) | ||
this.onclose(); | ||
this._delegate.onmessage = undefined; | ||
this._delegate.onclose = undefined; | ||
} | ||
send(s) { | ||
this._delegate.send(this._interceptor(s)); | ||
} | ||
close() { | ||
this._delegate.close(); | ||
} | ||
} | ||
exports.InterceptingTransport = InterceptingTransport; | ||
exports.debugProtocol = debug('pw:protocol'); | ||
exports.debugProtocol.color = '34'; | ||
//# sourceMappingURL=transport.js.map |
@@ -25,3 +25,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
@@ -31,3 +30,3 @@ const wkConnection_1 = require("./wkConnection"); | ||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; | ||
class WKBrowser extends platform.EventEmitter { | ||
class WKBrowser extends browser_1.BrowserBase { | ||
constructor(transport, attachToDefaultContext) { | ||
@@ -48,2 +47,4 @@ super(); | ||
helper_1.helper.addEventListener(this._browserSession, 'Playwright.windowOpen', this._onWindowOpen.bind(this)), | ||
helper_1.helper.addEventListener(this._browserSession, 'Playwright.downloadCreated', this._onDownloadCreated.bind(this)), | ||
helper_1.helper.addEventListener(this._browserSession, 'Playwright.downloadFinished', this._onDownloadFinished.bind(this)), | ||
helper_1.helper.addEventListener(this._browserSession, wkConnection_1.kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)), | ||
@@ -78,5 +79,2 @@ ]; | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
async _waitForFirstPageTarget() { | ||
@@ -89,2 +87,11 @@ helper_1.assert(!this._wkPages.size); | ||
} | ||
_onDownloadCreated(payload) { | ||
const page = this._wkPages.get(payload.pageProxyId); | ||
if (!page) | ||
return; | ||
this._downloadCreated(page._page, payload.uuid, payload.url); | ||
} | ||
_onDownloadFinished(payload) { | ||
this._downloadFinished(payload.uuid, payload.error); | ||
} | ||
_onPageProxyCreated(event) { | ||
@@ -158,12 +165,6 @@ const { pageProxyInfo } = event; | ||
} | ||
async close() { | ||
_disconnect() { | ||
helper_1.helper.removeEventListeners(this._eventListeners); | ||
const disconnected = new Promise(f => this.once(events_1.Events.Browser.Disconnected, f)); | ||
await Promise.all(this.contexts().map(context => context.close())); | ||
this._connection.close(); | ||
await disconnected; | ||
} | ||
_setDebugFunction(debugFunction) { | ||
this._connection._debugProtocol = debugFunction; | ||
} | ||
} | ||
@@ -179,14 +180,23 @@ exports.WKBrowser = WKBrowser; | ||
async _initialize() { | ||
const browserContextId = this._browserContextId; | ||
const promises = [ | ||
this._browser._browserSession.send('Playwright.setDownloadBehavior', { | ||
behavior: this._options.acceptDownloads ? 'allow' : 'deny', | ||
downloadPath: this._browser._downloadsPath, | ||
browserContextId | ||
}) | ||
]; | ||
if (this._options.ignoreHTTPSErrors) | ||
await this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true }); | ||
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true })); | ||
if (this._options.locale) | ||
await this._browser._browserSession.send('Playwright.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] }); | ||
promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] })); | ||
if (this._options.permissions) | ||
await this.grantPermissions(this._options.permissions); | ||
promises.push(this.grantPermissions(this._options.permissions)); | ||
if (this._options.geolocation) | ||
await this.setGeolocation(this._options.geolocation); | ||
promises.push(this.setGeolocation(this._options.geolocation)); | ||
if (this._options.offline) | ||
await this.setOffline(this._options.offline); | ||
promises.push(this.setOffline(this._options.offline)); | ||
if (this._options.httpCredentials) | ||
await this.setHTTPCredentials(this._options.httpCredentials); | ||
promises.push(this.setHTTPCredentials(this._options.httpCredentials)); | ||
await Promise.all(promises); | ||
} | ||
@@ -293,3 +303,3 @@ _wkPages() { | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -296,0 +306,0 @@ } |
@@ -19,4 +19,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const debug = require("debug"); | ||
const events_1 = require("events"); | ||
const helper_1 = require("../helper"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
// WKPlaywright uses this special id to issue Browser.close command which we | ||
@@ -32,3 +34,2 @@ // should ignore. | ||
this._closed = false; | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._transport = transport; | ||
@@ -41,3 +42,2 @@ this._transport.onmessage = this._dispatchMessage.bind(this); | ||
}); | ||
this._debugProtocol.color = '34'; | ||
} | ||
@@ -48,17 +48,17 @@ nextMessageId() { | ||
rawSend(message) { | ||
const data = JSON.stringify(message); | ||
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); | ||
this._transport.send(data); | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); | ||
this._transport.send(message); | ||
} | ||
_dispatchMessage(message) { | ||
this._debugProtocol('◀ RECV ' + message); | ||
const object = JSON.parse(message); | ||
if (object.id === exports.kBrowserCloseMessageId) | ||
if (transport_1.debugProtocol.enabled) | ||
transport_1.debugProtocol('◀ RECV ' + JSON.stringify(message)); | ||
if (message.id === exports.kBrowserCloseMessageId) | ||
return; | ||
if (object.pageProxyId) { | ||
const payload = { message: object, pageProxyId: object.pageProxyId }; | ||
if (message.pageProxyId) { | ||
const payload = { message: message, pageProxyId: message.pageProxyId }; | ||
this.browserSession.dispatchMessage({ method: exports.kPageProxyMessageReceived, params: payload }); | ||
return; | ||
} | ||
this.browserSession.dispatchMessage(object); | ||
this.browserSession.dispatchMessage(message); | ||
} | ||
@@ -81,3 +81,3 @@ _onClose() { | ||
exports.WKConnection = WKConnection; | ||
class WKSession extends platform.EventEmitter { | ||
class WKSession extends events_1.EventEmitter { | ||
constructor(connection, sessionId, errorText, rawSend) { | ||
@@ -102,3 +102,3 @@ super(); | ||
const messageObj = { id, method, params }; | ||
platform.debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2)); | ||
debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2)); | ||
this._rawSend(messageObj); | ||
@@ -119,3 +119,3 @@ return new Promise((resolve, reject) => { | ||
dispatchMessage(object) { | ||
platform.debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2)); | ||
debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2)); | ||
if (object.id && this._callbacks.has(object.id)) { | ||
@@ -125,3 +125,3 @@ const callback = this._callbacks.get(object.id); | ||
if (object.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, object)); | ||
callback.reject(createProtocolError(callback.error, callback.method, object.error)); | ||
else | ||
@@ -140,6 +140,6 @@ callback.resolve(object.result); | ||
exports.WKSession = WKSession; | ||
function createProtocolError(error, method, object) { | ||
let message = `Protocol error (${method}): ${object.error.message}`; | ||
if ('data' in object.error) | ||
message += ` ${JSON.stringify(object.error.data)}`; | ||
function createProtocolError(error, method, protocolError) { | ||
let message = `Protocol error (${method}): ${protocolError.message}`; | ||
if ('data' in protocolError) | ||
message += ` ${JSON.stringify(protocolError.data)}`; | ||
return rewriteError(error, message); | ||
@@ -162,3 +162,4 @@ } | ||
return `{"id":${message.id},"method":"${message.method}","params":{"message":[evaluate injected script],"targetId":"${message.params.targetId}"},"pageProxyId":${message.pageProxyId}}`; | ||
return JSON.stringify(message); | ||
} | ||
//# sourceMappingURL=wkConnection.js.map |
@@ -21,3 +21,2 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
const errorReasons = { | ||
@@ -70,3 +69,3 @@ 'aborted': 'Cancellation', | ||
if (responseBody && !('content-length' in responseHeaders)) | ||
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody)); | ||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); | ||
await this._session.send('Network.interceptWithResponse', { | ||
@@ -102,3 +101,3 @@ requestId: this._requestId, | ||
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId }); | ||
return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); | ||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); | ||
}; | ||
@@ -105,0 +104,0 @@ return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); |
@@ -30,5 +30,7 @@ "use strict"; | ||
const wkInput_1 = require("./wkInput"); | ||
const platform = require("../platform"); | ||
const wkAccessibility_1 = require("./wkAccessibility"); | ||
const wkProvisionalPage_1 = require("./wkProvisionalPage"); | ||
const selectors_1 = require("../selectors"); | ||
const jpeg = require("jpeg-js"); | ||
const png = require("pngjs"); | ||
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; | ||
@@ -55,3 +57,3 @@ const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; | ||
this._browserContext = browserContext; | ||
this._page.on(events_1.Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame, false)); | ||
this._page.on(events_1.Events.Page.FrameDetached, (frame) => this._removeContextsForFrame(frame, false)); | ||
this._eventListeners = [ | ||
@@ -138,2 +140,3 @@ helper_1.helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), | ||
} | ||
promises.push(this.updateEmulateMedia()); | ||
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() })); | ||
@@ -435,4 +438,5 @@ if (contextOptions.offline) | ||
} | ||
async setEmulateMedia(mediaType, colorScheme) { | ||
await this._forAllSessions(session => WKPage._setEmulateMedia(session, mediaType, colorScheme)); | ||
async updateEmulateMedia() { | ||
const colorScheme = this._page._state.colorScheme || this._browserContext._options.colorScheme || 'light'; | ||
await this._forAllSessions(session => WKPage._setEmulateMedia(session, this._page._state.mediaType, colorScheme)); | ||
} | ||
@@ -549,5 +553,5 @@ async setViewportSize(viewportSize) { | ||
const prefix = 'data:image/png;base64,'; | ||
let buffer = platform.Buffer.from(result.dataURL.substr(prefix.length), 'base64'); | ||
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64'); | ||
if (format === 'jpeg') | ||
buffer = platform.pngToJpeg(buffer, quality); | ||
buffer = jpeg.encode(png.PNG.sync.read(buffer), quality).data; | ||
return buffer; | ||
@@ -644,4 +648,3 @@ } | ||
throw new Error('Frame has been detached.'); | ||
const context = await parent._utilityContext(); | ||
const handles = await context._$$('iframe'); | ||
const handles = await selectors_1.selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */); | ||
const items = await Promise.all(handles.map(async (handle) => { | ||
@@ -648,0 +651,0 @@ const frame = await handle.contentFrame().catch(e => null); |
{ | ||
"name": "playwright-core", | ||
"version": "0.12.1", | ||
"version": "0.13.0-post-next.1586393147597", | ||
"description": "A high-level API to automate web browsers", | ||
@@ -11,5 +11,5 @@ "repository": "github:Microsoft/playwright", | ||
"playwright": { | ||
"chromium_revision": "751710", | ||
"firefox_revision": "1051", | ||
"webkit_revision": "1182" | ||
"chromium_revision": "754895", | ||
"firefox_revision": "1075", | ||
"webkit_revision": "1188" | ||
}, | ||
@@ -39,3 +39,2 @@ "scripts": { | ||
"watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .", | ||
"version": "node utils/sync_package_versions.js && npm run doc", | ||
"test-types": "npm run generate-types && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json", | ||
@@ -50,5 +49,6 @@ "generate-types": "node utils/generate_types/" | ||
"debug": "^4.1.0", | ||
"extract-zip": "^1.6.6", | ||
"extract-zip": "^2.0.0", | ||
"https-proxy-agent": "^3.0.0", | ||
"jpeg-js": "^0.3.6", | ||
"mime": "^2.4.4", | ||
"pngjs": "^3.4.0", | ||
@@ -63,3 +63,4 @@ "progress": "^2.0.3", | ||
"@types/extract-zip": "^1.6.2", | ||
"@types/node": "^8.10.34", | ||
"@types/mime": "^2.0.1", | ||
"@types/node": "^10.17.17", | ||
"@types/pngjs": "^3.4.0", | ||
@@ -83,3 +84,3 @@ "@types/proxy-from-env": "^1.0.0", | ||
"ts-loader": "^6.1.2", | ||
"typescript": "^3.7.5", | ||
"typescript": "^3.8.3", | ||
"webpack": "^4.41.0", | ||
@@ -86,0 +87,0 @@ "webpack-cli": "^3.3.9" |
@@ -1,5 +0,5 @@ | ||
# Playwright | ||
[![npm version](https://img.shields.io/npm/v/playwright.svg?style=flat)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge-if-release -->[![Chromium version](https://img.shields.io/badge/chromium-83.0.4090.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge-if-release -->[![Firefox version](https://img.shields.io/badge/firefox-74.0b10-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> [![WebKit version](https://img.shields.io/badge/webkit-13.0.4-blue.svg?logo=safari)](https://webkit.org/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) | ||
# 🎭 Playwright | ||
[![npm version](https://img.shields.io/npm/v/playwright.svg?style=flat)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge-if-release -->[![Chromium version](https://img.shields.io/badge/chromium-83.0.4101.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge-if-release -->[![Firefox version](https://img.shields.io/badge/firefox-75.0b8-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> [![WebKit version](https://img.shields.io/badge/webkit-13.0.4-blue.svg?logo=safari)](https://webkit.org/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) | ||
###### [API](https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md) | [Changelog](https://github.com/microsoft/playwright/releases) | [FAQ](#faq) | [Contributing](#contributing) | ||
###### [API](https://github.com/microsoft/playwright/blob/v0.13.0/docs/api.md) | [Changelog](https://github.com/microsoft/playwright/releases) | [FAQ](#faq) | [Contributing](#contributing) | ||
@@ -11,5 +11,5 @@ | ||
| ---: | :---: | :---: | :---: | :---: | | ||
| Chromium| <!-- GEN:chromium-version-if-release-->83.0.4090.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| Chromium| <!-- GEN:chromium-version-if-release-->83.0.4101.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| WebKit | 13.0.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| Firefox | <!-- GEN:firefox-version-if-release -->74.0b10<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| Firefox | <!-- GEN:firefox-version-if-release -->75.0b8<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
- Headless is supported for all the browsers on all platforms. | ||
@@ -65,6 +65,5 @@ | ||
const context = await browser.newContext({ | ||
viewport: iPhone11.viewport, | ||
userAgent: iPhone11.userAgent, | ||
...iPhone11, | ||
geolocation: { longitude: 12.492507, latitude: 41.889938 }, | ||
permissions: { 'https://www.google.com': ['geolocation'] } | ||
permissions: ['geolocation'] | ||
}); | ||
@@ -71,0 +70,0 @@ const page = await context.newPage(); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
10
1391335
10
25
75
34303
187
23
+ Addedmime@^2.4.4
+ Added@types/node@22.13.1(transitive)
+ Added@types/yauzl@2.10.3(transitive)
+ Addedend-of-stream@1.4.4(transitive)
+ Addedextract-zip@2.0.1(transitive)
+ Addedget-stream@5.2.0(transitive)
+ Addedmime@2.6.0(transitive)
+ Addedpump@3.0.2(transitive)
+ Addedundici-types@6.20.0(transitive)
- Removedbuffer-from@1.1.2(transitive)
- Removedconcat-stream@1.6.2(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removeddebug@2.6.9(transitive)
- Removedextract-zip@1.7.0(transitive)
- Removedisarray@1.0.0(transitive)
- Removedminimist@1.2.8(transitive)
- Removedmkdirp@0.5.6(transitive)
- Removedms@2.0.0(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedtypedarray@0.0.6(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updatedextract-zip@^2.0.0