playwright-core
Advanced tools
Comparing version 0.12.1 to 0.13.0-next.1587452068410
@@ -65,3 +65,6 @@ /** | ||
} | ||
await browserFetcher.downloadBrowser({...options, progress}); | ||
await browserFetcher.downloadBrowser({...options, progress}).catch(e => { | ||
process.exitCode = 1; | ||
throw e; | ||
}); | ||
logPolitely(`${options.progressBarBrowserName} downloaded to ${options.downloadPath}`); | ||
@@ -68,0 +71,0 @@ } |
@@ -24,4 +24,8 @@ "use strict"; | ||
exports.Dialog = dialog_1.Dialog; | ||
var download_1 = require("./download"); | ||
exports.Download = download_1.Download; | ||
var dom_1 = require("./dom"); | ||
exports.ElementHandle = dom_1.ElementHandle; | ||
var fileChooser_1 = require("./fileChooser"); | ||
exports.FileChooser = fileChooser_1.FileChooser; | ||
var errors_1 = require("./errors"); | ||
@@ -28,0 +32,0 @@ exports.TimeoutError = errors_1.TimeoutError; |
@@ -18,9 +18,49 @@ "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 events_2 = require("./events"); | ||
class BrowserBase extends events_1.EventEmitter { | ||
constructor(logger) { | ||
super(); | ||
this._downloadsPath = ''; | ||
this._downloads = new Map(); | ||
this._ownedServer = null; | ||
this._logger = logger; | ||
} | ||
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)); | ||
} | ||
_isLogEnabled(log) { | ||
return this._logger._isLogEnabled(log); | ||
} | ||
_log(log, message, ...args) { | ||
return this._logger._log(log, message, ...args); | ||
} | ||
} | ||
exports.createPageInNewContext = createPageInNewContext; | ||
exports.BrowserBase = BrowserBase; | ||
//# sourceMappingURL=browser.js.map |
@@ -21,7 +21,8 @@ "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 { | ||
constructor(options) { | ||
const extendedEventEmitter_1 = require("./extendedEventEmitter"); | ||
const logger_1 = require("./logger"); | ||
class BrowserContextBase extends extendedEventEmitter_1.ExtendedEventEmitter { | ||
constructor(browserBase, options) { | ||
super(); | ||
@@ -33,14 +34,26 @@ this._timeoutSettings = new timeoutSettings_1.TimeoutSettings(); | ||
this._permissions = new Map(); | ||
this._downloads = new Set(); | ||
this._browserBase = browserBase; | ||
this._options = options; | ||
this._logger = options.logger ? new logger_1.RootLogger(options.logger) : browserBase; | ||
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 +82,8 @@ 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); | ||
_isLogEnabled(log) { | ||
return this._logger._isLogEnabled(log); | ||
} | ||
_log(log, message, ...args) { | ||
return this._logger._log(log, message, ...args); | ||
} | ||
} | ||
@@ -80,0 +90,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,6 +32,8 @@ const crConnection_1 = require("./crConnection"); | ||
const crExecutionContext_1 = require("./crExecutionContext"); | ||
class CRBrowser extends platform.EventEmitter { | ||
constructor(connection) { | ||
super(); | ||
const logger_1 = require("../logger"); | ||
class CRBrowser extends browser_1.BrowserBase { | ||
constructor(connection, logger, isPersistent) { | ||
super(logger); | ||
this._clientRootSessionPromise = null; | ||
this._defaultContext = null; | ||
this._contexts = new Map(); | ||
@@ -47,3 +48,4 @@ this._crPages = new Map(); | ||
this._session = this._connection.rootSession; | ||
this._defaultContext = new CRBrowserContext(this, null, browserContext_1.validateBrowserContextOptions({})); | ||
if (isPersistent) | ||
this._defaultContext = new CRBrowserContext(this, null, browserContext_1.validateBrowserContextOptions({})); | ||
this._connection.on(crConnection_1.ConnectionEvents.Disconnected, () => { | ||
@@ -58,25 +60,33 @@ for (const context of this._contexts.values()) | ||
} | ||
static async connect(transport, isPersistent, slowMo) { | ||
const connection = new crConnection_1.CRConnection(transport_1.SlowMoTransport.wrap(transport, slowMo)); | ||
const browser = new CRBrowser(connection); | ||
static async connect(transport, isPersistent, logger, slowMo) { | ||
const connection = new crConnection_1.CRConnection(transport_1.SlowMoTransport.wrap(transport, slowMo), logger); | ||
const browser = new CRBrowser(connection, logger, isPersistent); | ||
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; | ||
@@ -95,9 +105,22 @@ } | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }) { | ||
if (targetInfo.type === 'browser') | ||
return; | ||
const session = this._connection.session(sessionId); | ||
const context = (targetInfo.browserContextId && this._contexts.has(targetInfo.browserContextId)) ? | ||
this._contexts.get(targetInfo.browserContextId) : this._defaultContext; | ||
helper_1.assert(targetInfo.browserContextId, 'targetInfo: ' + JSON.stringify(targetInfo, null, 2)); | ||
let context = this._contexts.get(targetInfo.browserContextId) || null; | ||
if (!context) { | ||
// TODO: auto attach only to pages from our contexts. | ||
// assert(this._defaultContext); | ||
context = this._defaultContext; | ||
} | ||
if (targetInfo.type === 'other' || !context) { | ||
if (waitingForDebugger) { | ||
// Ideally, detaching should resume any target, but there is a bug in the backend. | ||
session.send('Runtime.runIfWaitingForDebugger').catch(logger_1.logError(this)).then(() => { | ||
this._session.send('Target.detachFromTarget', { sessionId }).catch(logger_1.logError(this)); | ||
}); | ||
} | ||
return; | ||
} | ||
helper_1.assert(!this._crPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); | ||
@@ -118,2 +141,6 @@ helper_1.assert(!this._backgroundPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); | ||
this._crPages.set(targetInfo.targetId, crPage); | ||
if (opener && opener._initializedPage) { | ||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) | ||
signalBarrier.addPopup(crPage.pageOrError()); | ||
} | ||
crPage.pageOrError().then(() => { | ||
@@ -137,9 +164,3 @@ this._firstPageCallback(); | ||
} | ||
helper_1.assert(targetInfo.type === 'browser' || targetInfo.type === 'other'); | ||
if (waitingForDebugger) { | ||
// Ideally, detaching should resume any target, but there is a bug in the backend. | ||
session.send('Runtime.runIfWaitingForDebugger').catch(helper_1.debugError).then(() => { | ||
this._session.send('Target.detachFromTarget', { sessionId }).catch(helper_1.debugError); | ||
}); | ||
} | ||
helper_1.assert(false, 'Unknown target type: ' + targetInfo.type); | ||
} | ||
@@ -170,7 +191,4 @@ _onDetachedFromTarget(payload) { | ||
} | ||
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; | ||
} | ||
@@ -182,3 +200,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 = [ | ||
@@ -202,10 +220,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; | ||
} | ||
@@ -220,5 +237,2 @@ isConnected() { | ||
} | ||
_setDebugFunction(debugFunction) { | ||
this._connection._debugProtocol = debugFunction; | ||
} | ||
} | ||
@@ -228,3 +242,3 @@ exports.CRBrowser = CRBrowser; | ||
constructor(browserContext, session, url) { | ||
super(url); | ||
super(browserContext, url); | ||
this._browserContext = browserContext; | ||
@@ -241,3 +255,3 @@ session.once('Runtime.executionContextCreated', event => { | ||
constructor(browser, browserContextId, options) { | ||
super(options); | ||
super(browser, options); | ||
this._browser = browser; | ||
@@ -248,8 +262,16 @@ this._browserContextId = browserContextId; | ||
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); | ||
} | ||
@@ -287,2 +309,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 }); | ||
@@ -328,3 +357,3 @@ } | ||
for (const page of this.pages()) | ||
await page._delegate._client.send('Emulation.setGeolocationOverride', geolocation || {}); | ||
await page._delegate.updateGeolocation(); | ||
} | ||
@@ -339,3 +368,3 @@ async setExtraHTTPHeaders(headers) { | ||
for (const page of this.pages()) | ||
await page._delegate._networkManager.setOffline(offline); | ||
await page._delegate.updateOffline(); | ||
} | ||
@@ -345,3 +374,3 @@ async setHTTPCredentials(httpCredentials) { | ||
for (const page of this.pages()) | ||
await page._delegate._networkManager.authenticate(httpCredentials); | ||
await page._delegate.updateHttpCredentials(); | ||
} | ||
@@ -371,2 +400,7 @@ async addInitScript(script, arg) { | ||
} | ||
async unroute(url, handler) { | ||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); | ||
for (const page of this.pages()) | ||
await page._delegate.updateRequestInterception(); | ||
} | ||
async close() { | ||
@@ -383,3 +417,3 @@ if (this._closed) | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -386,0 +420,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,4 +29,4 @@ Disconnected: Symbol('ConnectionEvents.Disconnected') | ||
exports.kBrowserCloseMessageId = -9999; | ||
class CRConnection extends platform.EventEmitter { | ||
constructor(transport) { | ||
class CRConnection extends events_1.EventEmitter { | ||
constructor(transport, logger) { | ||
super(); | ||
@@ -36,2 +37,3 @@ this._lastId = 0; | ||
this._transport = transport; | ||
this._logger = logger; | ||
this._transport.onmessage = this._onMessage.bind(this); | ||
@@ -41,4 +43,2 @@ this._transport.onclose = this._onClose.bind(this); | ||
this._sessions.set('', this.rootSession); | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._debugProtocol.color = '34'; | ||
} | ||
@@ -51,33 +51,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 (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, '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 (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, '◀ 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); | ||
} | ||
@@ -110,6 +110,7 @@ _onClose() { | ||
}; | ||
class CRSession extends platform.EventEmitter { | ||
class CRSession extends events_1.EventEmitter { | ||
constructor(connection, rootSessionId, targetType, sessionId) { | ||
super(); | ||
this._callbacks = new Map(); | ||
this._crashed = false; | ||
this._connection = connection; | ||
@@ -125,6 +126,11 @@ this._rootSessionId = rootSessionId; | ||
} | ||
_markAsCrashed() { | ||
this._crashed = true; | ||
} | ||
async send(method, params) { | ||
if (this._crashed) | ||
throw new Error('Target crashed'); | ||
if (!this._connection) | ||
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) => { | ||
@@ -139,3 +145,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 | ||
@@ -166,6 +172,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); | ||
@@ -182,3 +188,4 @@ } | ||
return `{"id":${message.id} [evaluate injected script]}`; | ||
return JSON.stringify(message); | ||
} | ||
//# sourceMappingURL=crConnection.js.map |
@@ -21,6 +21,7 @@ "use strict"; | ||
const crExecutionContext_1 = require("./crExecutionContext"); | ||
const logger_1 = require("../logger"); | ||
class CRCoverage { | ||
constructor(client) { | ||
this._jsCoverage = new JSCoverage(client); | ||
this._cssCoverage = new CSSCoverage(client); | ||
constructor(client, logger) { | ||
this._jsCoverage = new JSCoverage(client, logger); | ||
this._cssCoverage = new CSSCoverage(client, logger); | ||
} | ||
@@ -42,5 +43,6 @@ async startJSCoverage(options) { | ||
class JSCoverage { | ||
constructor(client) { | ||
constructor(client, logger) { | ||
this._reportAnonymousScripts = false; | ||
this._client = client; | ||
this._logger = logger; | ||
this._enabled = false; | ||
@@ -92,3 +94,3 @@ this._scriptIds = new Set(); | ||
// This might happen if the page has already navigated away. | ||
helper_1.debugError(e); | ||
logger_1.logError(this._logger)(e); | ||
} | ||
@@ -122,4 +124,5 @@ } | ||
class CSSCoverage { | ||
constructor(client) { | ||
constructor(client, logger) { | ||
this._client = client; | ||
this._logger = logger; | ||
this._enabled = false; | ||
@@ -166,3 +169,3 @@ this._stylesheetURLs = new Map(); | ||
// This might happen if the page has already navigated away. | ||
helper_1.debugError(e); | ||
logger_1.logError(this._logger)(e); | ||
} | ||
@@ -169,0 +172,0 @@ } |
@@ -21,3 +21,3 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
const logger_1 = require("../logger"); | ||
class CRNetworkManager { | ||
@@ -31,3 +31,3 @@ constructor(client, page) { | ||
this._protocolRequestInterceptionEnabled = false; | ||
this._requestIdToInterceptionId = new Map(); | ||
this._requestIdToRequestPausedEvent = new Map(); | ||
this._client = client; | ||
@@ -95,6 +95,6 @@ this._page = page; | ||
const requestId = event.requestId; | ||
const interceptionId = this._requestIdToInterceptionId.get(requestId); | ||
if (interceptionId) { | ||
this._onRequest(workerFrame, event, interceptionId); | ||
this._requestIdToInterceptionId.delete(requestId); | ||
const requestPausedEvent = this._requestIdToRequestPausedEvent.get(requestId); | ||
if (requestPausedEvent) { | ||
this._onRequest(workerFrame, event, requestPausedEvent); | ||
this._requestIdToRequestPausedEvent.delete(requestId); | ||
} | ||
@@ -121,3 +121,3 @@ else { | ||
authChallengeResponse: { response, username, password }, | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
} | ||
@@ -128,3 +128,3 @@ _onRequestPaused(workerFrame, event) { | ||
requestId: event.requestId | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
} | ||
@@ -134,41 +134,50 @@ if (!event.networkId || event.request.url.startsWith('data:')) | ||
const requestId = event.networkId; | ||
const interceptionId = event.requestId; | ||
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId); | ||
if (requestWillBeSentEvent) { | ||
this._onRequest(workerFrame, requestWillBeSentEvent, interceptionId); | ||
this._onRequest(workerFrame, requestWillBeSentEvent, event); | ||
this._requestIdToRequestWillBeSentEvent.delete(requestId); | ||
} | ||
else { | ||
this._requestIdToInterceptionId.set(requestId, interceptionId); | ||
this._requestIdToRequestPausedEvent.set(requestId, event); | ||
} | ||
} | ||
_onRequest(workerFrame, event, interceptionId) { | ||
if (event.request.url.startsWith('data:')) | ||
_onRequest(workerFrame, requestWillBeSentEvent, requestPausedEvent) { | ||
if (requestWillBeSentEvent.request.url.startsWith('data:')) | ||
return; | ||
let redirectedFrom = null; | ||
if (event.redirectResponse) { | ||
const request = this._requestIdToRequest.get(event.requestId); | ||
if (requestWillBeSentEvent.redirectResponse) { | ||
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId); | ||
// If we connect late to the target, we could have missed the requestWillBeSent event. | ||
if (request) { | ||
this._handleRequestRedirect(request, event.redirectResponse); | ||
this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse); | ||
redirectedFrom = request.request; | ||
} | ||
} | ||
let frame = event.frameId ? this._page._frameManager.frame(event.frameId) : workerFrame; | ||
let frame = requestWillBeSentEvent.frameId ? this._page._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame; | ||
// Check if it's main resource request interception (targetId === main frame id). | ||
if (!frame && interceptionId && event.frameId === this._page._delegate._targetId) { | ||
if (!frame && requestPausedEvent && requestWillBeSentEvent.frameId === this._page._delegate._targetId) { | ||
// Main resource request for the page is being intercepted so the Frame is not created | ||
// yet. Precreate it here for the purposes of request interception. It will be updated | ||
// later as soon as the request contnues and we receive frame tree from the page. | ||
frame = this._page._frameManager.frameAttached(event.frameId, null); | ||
frame = this._page._frameManager.frameAttached(requestWillBeSentEvent.frameId, null); | ||
} | ||
if (!frame) { | ||
if (interceptionId) | ||
this._client.send('Fetch.continueRequest', { requestId: interceptionId }).catch(helper_1.debugError); | ||
if (requestPausedEvent) | ||
this._client.send('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }).catch(logger_1.logError(this._page)); | ||
return; | ||
} | ||
const isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document'; | ||
const documentId = isNavigationRequest ? event.loaderId : undefined; | ||
const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectedFrom); | ||
this._requestIdToRequest.set(event.requestId, request); | ||
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; | ||
const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined; | ||
if (isNavigationRequest) | ||
this._page._frameManager.frameUpdatedDocumentIdForNavigation(requestWillBeSentEvent.frameId, documentId); | ||
const request = new InterceptableRequest({ | ||
client: this._client, | ||
frame, | ||
documentId, | ||
allowInterception: this._userRequestInterceptionEnabled, | ||
requestWillBeSentEvent, | ||
requestPausedEvent, | ||
redirectedFrom | ||
}); | ||
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request); | ||
this._page._frameManager.requestStarted(request.request); | ||
@@ -179,3 +188,3 @@ } | ||
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'); | ||
}; | ||
@@ -235,8 +244,11 @@ return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); | ||
class InterceptableRequest { | ||
constructor(client, frame, interceptionId, documentId, allowInterception, event, redirectedFrom) { | ||
constructor(options) { | ||
const { client, frame, documentId, allowInterception, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options; | ||
this._client = client; | ||
this._requestId = event.requestId; | ||
this._interceptionId = interceptionId; | ||
this._requestId = requestWillBeSentEvent.requestId; | ||
this._interceptionId = requestPausedEvent && requestPausedEvent.requestId; | ||
this._documentId = documentId; | ||
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url, (event.type || '').toLowerCase(), event.request.method, event.request.postData || null, headersObject(event.request.headers)); | ||
const { headers, method, url, postData = null, } = requestPausedEvent ? requestPausedEvent.request : requestWillBeSentEvent.request; | ||
const type = (requestWillBeSentEvent.type || '').toLowerCase(); | ||
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postData, headersObject(headers)); | ||
} | ||
@@ -252,7 +264,7 @@ async continue(overrides = {}) { | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page)(error); | ||
}); | ||
} | ||
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 = {}; | ||
@@ -266,3 +278,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', { | ||
@@ -277,3 +289,3 @@ requestId: this._interceptionId, | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page)(error); | ||
}); | ||
@@ -290,3 +302,3 @@ } | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page)(error); | ||
}); | ||
@@ -293,0 +305,0 @@ } |
@@ -34,11 +34,9 @@ "use strict"; | ||
const console_1 = require("../console"); | ||
const platform = require("../platform"); | ||
const errors_1 = require("../errors"); | ||
const logger_1 = require("../logger"); | ||
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; | ||
@@ -49,40 +47,265 @@ this._opener = opener; | ||
this._pdf = new crPdf_1.CRPDF(client); | ||
this._coverage = new crCoverage_1.CRCoverage(client); | ||
this._coverage = new crCoverage_1.CRCoverage(client, browserContext); | ||
this._browserContext = browserContext; | ||
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); | ||
if (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(logger_1.logError(this._page)))); | ||
} | ||
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._evaluateInUtility(({ injected, node }, files) => injected.setInputFiles(node, files), dom.toFileTransferPayload(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._firstNonInitialNavigationCommittedFulfill = () => { }; | ||
this._firstNonInitialNavigationCommittedReject = (e) => { }; | ||
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, r) => { | ||
this._firstNonInitialNavigationCommittedFulfill = f; | ||
this._firstNonInitialNavigationCommittedReject = r; | ||
}); | ||
client.once(crConnection_1.CRSessionEvents.Disconnected, () => { | ||
this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); | ||
}); | ||
} | ||
_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. | ||
@@ -93,7 +316,7 @@ this._client.send('Page.createIsolatedWorld', { | ||
worldName: UTILITY_WORLD_NAME, | ||
}).catch(helper_1.debugError); | ||
for (const binding of this._browserContext._pageBindings.values()) | ||
frame.evaluate(binding.source).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
for (const binding of this._crPage._browserContext._pageBindings.values()) | ||
frame.evaluate(binding.source).catch(logger_1.logError(this._page)); | ||
} | ||
const isInitialEmptyPage = this._page.mainFrame().url() === ':'; | ||
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':'; | ||
if (isInitialEmptyPage) { | ||
@@ -108,3 +331,3 @@ // Ignore lifecycle events for the initial empty page. It is never the final page | ||
else { | ||
this._firstNonInitialNavigationCommittedCallback(); | ||
this._firstNonInitialNavigationCommittedFulfill(); | ||
this._eventListeners.push(helper_1.helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); | ||
@@ -124,3 +347,3 @@ } | ||
]; | ||
const options = this._browserContext._options; | ||
const options = this._crPage._browserContext._options; | ||
if (options.bypassCSP) | ||
@@ -130,4 +353,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) | ||
@@ -141,14 +366,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')); | ||
@@ -158,8 +381,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 }); | ||
@@ -188,2 +410,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); | ||
@@ -194,6 +422,6 @@ } | ||
if (!initial) | ||
this._firstNonInitialNavigationCommittedCallback(); | ||
this._firstNonInitialNavigationCommittedFulfill(); | ||
} | ||
_onFrameRequestedNavigation(payload) { | ||
this._page._frameManager.frameRequestedNavigation(payload.frameId); | ||
this._page._frameManager.frameRequestedNavigation(payload.frameId, ''); | ||
} | ||
@@ -204,2 +432,7 @@ _onFrameNavigatedWithinDocument(frameId, url) { | ||
_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); | ||
@@ -232,6 +465,10 @@ } | ||
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') { | ||
// Ideally, detaching should resume any target, but there is a bug in the backend. | ||
session.send('Runtime.runIfWaitingForDebugger').catch(helper_1.debugError).then(() => { | ||
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(helper_1.debugError); | ||
session.send('Runtime.runIfWaitingForDebugger').catch(logger_1.logError(this._page)).then(() => { | ||
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(logger_1.logError(this._page)); | ||
}); | ||
@@ -241,3 +478,3 @@ return; | ||
const url = event.targetInfo.url; | ||
const worker = new page_1.Worker(url); | ||
const worker = new page_1.Worker(this._page, url); | ||
this._page._addWorker(event.sessionId, worker); | ||
@@ -251,3 +488,3 @@ session.once('Runtime.executionContextCreated', async (event) => { | ||
session.send('Runtime.runIfWaitingForDebugger'), | ||
]).catch(helper_1.debugError); // This might fail if the target is closed before we initialize. | ||
]).catch(logger_1.logError(this._page)); // This might fail if the target is closed before we initialize. | ||
session.on('Runtime.consoleAPICalled', event => { | ||
@@ -259,5 +496,6 @@ const args = event.args.map(o => worker._existingExecutionContext._createHandle(o)); | ||
// 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); | ||
@@ -286,6 +524,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) { | ||
@@ -309,3 +543,4 @@ await Promise.all([ | ||
} | ||
_onTargetCrashed() { | ||
async _onTargetCrashed() { | ||
this._client._markAsCrashed(); | ||
this._page._didCrash(); | ||
@@ -323,8 +558,17 @@ } | ||
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 | ||
@@ -334,8 +578,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 }; | ||
@@ -357,79 +610,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', { | ||
@@ -442,3 +635,3 @@ objectId: toRemoteObject(handle).objectId | ||
} | ||
async getOwnerFrame(handle) { | ||
async _getOwnerFrame(handle) { | ||
// document.documentElement has frameId of the owner frame. | ||
@@ -464,9 +657,6 @@ 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', { | ||
objectId: toRemoteObject(handle).objectId | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result) | ||
@@ -481,3 +671,3 @@ return null; | ||
} | ||
async scrollRectIntoViewIfNeeded(handle, rect) { | ||
async _scrollRectIntoViewIfNeeded(handle, rect) { | ||
await this._client.send('DOM.scrollIntoViewIfNeeded', { | ||
@@ -487,2 +677,4 @@ objectId: toRemoteObject(handle).objectId, | ||
}).catch(e => { | ||
if (e instanceof Error && e.message.includes('Node is detached from document')) | ||
throw new errors_1.NotConnectedError(); | ||
if (e instanceof Error && e.message.includes('Node does not have a layout object')) | ||
@@ -493,6 +685,6 @@ e.message = 'Node is either not visible or not an HTMLElement'; | ||
} | ||
async getContentQuads(handle) { | ||
async _getContentQuads(handle) { | ||
const result = await this._client.send('DOM.getContentQuads', { | ||
objectId: toRemoteObject(handle).objectId | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result) | ||
@@ -507,20 +699,13 @@ return null; | ||
} | ||
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', { | ||
backendNodeId, | ||
executionContextId: to._delegate._contextId, | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result || result.object.subtype === 'null') | ||
@@ -530,27 +715,3 @@ throw new Error('Unable to adopt element handle from a different document'); | ||
} | ||
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) { | ||
@@ -557,0 +718,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); | ||
} | ||
@@ -100,6 +95,19 @@ exports.readProtocolStream = readProtocolStream; | ||
function exceptionToError(exceptionDetails) { | ||
const message = getExceptionMessage(exceptionDetails); | ||
const messageWithStack = getExceptionMessage(exceptionDetails); | ||
const lines = messageWithStack.split('\n'); | ||
const firstStackTraceLine = lines.findIndex(line => line.startsWith(' at')); | ||
let message = ''; | ||
let stack = ''; | ||
if (firstStackTraceLine === -1) { | ||
message = messageWithStack; | ||
} | ||
else { | ||
message = lines.slice(0, firstStackTraceLine).join('\n'); | ||
stack = messageWithStack; | ||
} | ||
const match = message.match(/^[a-zA-Z0-0_]*Error: (.*)$/); | ||
if (match) | ||
message = match[1]; | ||
const err = new Error(message); | ||
// Don't report clientside error with a node stack attached | ||
err.stack = 'Error: ' + err.message; // Stack is supposed to contain error message as the first line. | ||
err.stack = stack; | ||
return err; | ||
@@ -106,0 +114,0 @@ } |
252
lib/dom.js
@@ -18,11 +18,18 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
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 errors_1 = require("./errors"); | ||
const logger_1 = require("./logger"); | ||
exports.inputLog = { | ||
name: 'input', | ||
color: 'cyan' | ||
}; | ||
class FrameExecutionContext extends js.ExecutionContext { | ||
constructor(delegate, frame) { | ||
super(delegate); | ||
this._injectedGeneration = -1; | ||
super(delegate, frame._page); | ||
this.frame = frame; | ||
@@ -36,5 +43,5 @@ } | ||
async _doEvaluateInternal(returnByValue, waitForNavigations, pageFunction, ...args) { | ||
return await this.frame._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
return await this.frame._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
return this._delegate.evaluate(this, returnByValue, pageFunction, ...args); | ||
}, waitForNavigations ? undefined : { waitUntil: 'nowait' }); | ||
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { noWaitAfter: true }); | ||
} | ||
@@ -47,45 +54,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; | ||
} | ||
} | ||
@@ -110,2 +81,5 @@ exports.FrameExecutionContext = FrameExecutionContext; | ||
return null; | ||
const frame = this._page._frameManager.frame(frameId); | ||
if (frame) | ||
return frame; | ||
for (const page of this._page._browserContext.pages()) { | ||
@@ -124,4 +98,33 @@ const frame = page._frameManager.frame(frameId); | ||
} | ||
async getAttribute(name) { | ||
return this._evaluateInUtility(({ node }, name) => { | ||
if (node.nodeType !== Node.ELEMENT_NODE) | ||
throw new Error('Not an element'); | ||
const element = node; | ||
return element.getAttribute(name); | ||
}, name); | ||
} | ||
async textContent() { | ||
return this._evaluateInUtility(({ node }) => node.textContent, {}); | ||
} | ||
async innerText() { | ||
return this._evaluateInUtility(({ node }) => { | ||
if (node.nodeType !== Node.ELEMENT_NODE) | ||
throw new Error('Not an element'); | ||
const element = node; | ||
return element.innerText; | ||
}, {}); | ||
} | ||
async innerHTML() { | ||
return this._evaluateInUtility(({ node }) => { | ||
if (node.nodeType !== Node.ELEMENT_NODE) | ||
throw new Error('Not an element'); | ||
const element = node; | ||
return element.innerHTML; | ||
}, {}); | ||
} | ||
async _scrollRectIntoViewIfNeeded(rect) { | ||
this._page._log(exports.inputLog, 'scrolling into view if needed...'); | ||
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect); | ||
this._page._log(exports.inputLog, '...done'); | ||
} | ||
@@ -169,3 +172,3 @@ async scrollIntoViewIfNeeded() { | ||
this.boundingBox(), | ||
this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(helper_1.debugError), | ||
this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(logger_1.logError(this._context._logger)), | ||
]); | ||
@@ -184,19 +187,25 @@ const point = { x: offset.x, y: offset.y }; | ||
} | ||
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; | ||
await this._page.mouse.move(point.x, point.y); // Force any hover effects before waiting for hit target. | ||
if (!force) | ||
await this._waitForHitTargetAt(point, options); | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
await this._waitForHitTargetAt(point, deadline); | ||
await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
let restoreModifiers; | ||
if (options && options.modifiers) | ||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); | ||
this._page._log(exports.inputLog, 'performing input action...'); | ||
await action(point); | ||
this._page._log(exports.inputLog, '...done'); | ||
if (restoreModifiers) | ||
await this._page.keyboard._ensureModifiers(restoreModifiers); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
@@ -213,2 +222,3 @@ hover(options) { | ||
async selectOption(values, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
let vals; | ||
@@ -230,25 +240,36 @@ if (!Array.isArray(values)) | ||
} | ||
return await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); | ||
}, options); | ||
return await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
const injectedResult = await this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); | ||
return handleInjectedResult(injectedResult, ''); | ||
}, 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) + '"'); | ||
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 deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
const injectedResult = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); | ||
const needsInput = handleInjectedResult(injectedResult, ''); | ||
if (needsInput) { | ||
if (value) | ||
await this._page.keyboard.insertText(value); | ||
else | ||
await this._page.keyboard.press('Delete'); | ||
} | ||
}, deadline, options, true); | ||
} | ||
async setInputFiles(files) { | ||
const multiple = await this._evaluateInUtility(({ node }) => { | ||
async selectText() { | ||
const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.selectText(node), {}); | ||
handleInjectedResult(injectedResult, ''); | ||
} | ||
async setInputFiles(files, options) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
const injectedResult = await this._evaluateInUtility(({ node }) => { | ||
if (node.nodeType !== Node.ELEMENT_NODE || node.tagName !== 'INPUT') | ||
throw new Error('Node is not an HTMLInputElement'); | ||
return { status: 'error', error: 'Node is not an HTMLInputElement' }; | ||
if (!node.isConnected) | ||
return { status: 'notconnected' }; | ||
const input = node; | ||
return input.multiple; | ||
return { status: 'success', value: input.multiple }; | ||
}, {}); | ||
const multiple = handleInjectedResult(injectedResult, ''); | ||
let ff; | ||
@@ -264,5 +285,5 @@ if (!Array.isArray(files)) | ||
const file = { | ||
name: platform.basename(item), | ||
type: platform.getMimeType(item), | ||
data: await platform.readFileAsync(item, 'base64') | ||
name: path.basename(item), | ||
mimeType: mime.getType(item) || 'application/octet-stream', | ||
buffer: await util.promisify(fs.readFile)(item) | ||
}; | ||
@@ -275,27 +296,23 @@ filePayloads.push(file); | ||
} | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
await this._page._delegate.setInputFiles(this, filePayloads); | ||
}); | ||
}, deadline, options); | ||
} | ||
async focus() { | ||
const errorMessage = await this._evaluateInUtility(({ node }) => { | ||
if (!node['focus']) | ||
return 'Node is not an HTML or SVG element.'; | ||
node.focus(); | ||
return false; | ||
}, {}); | ||
if (errorMessage) | ||
throw new Error(errorMessage); | ||
const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.focusNode(node), {}); | ||
handleInjectedResult(injectedResult, ''); | ||
} | ||
async type(text, options) { | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
await this.focus(); | ||
await this._page.keyboard.type(text, options); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
async press(key, options) { | ||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
await this._page._frameManager.waitForSignalsCreatedBy(async () => { | ||
await this.focus(); | ||
await this._page.keyboard.press(key, options); | ||
}, options, true); | ||
}, deadline, options, true); | ||
} | ||
@@ -321,18 +338,18 @@ async check(options) { | ||
} | ||
$(selector) { | ||
return this._context._$(selector, this); | ||
async $(selector) { | ||
return selectors_1.selectors._query(this._context.frame, selector, this); | ||
} | ||
$$(selector) { | ||
return this._context._$$(selector, this); | ||
async $$(selector) { | ||
return selectors_1.selectors._queryAll(this._context.frame, selector, this); | ||
} | ||
async $eval(selector, pageFunction, arg) { | ||
const elementHandle = await this._context._$(selector, this); | ||
if (!elementHandle) | ||
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); | ||
const arrayHandle = await selectors_1.selectors._queryArray(this._context.frame, selector, this); | ||
const result = await arrayHandle.evaluate(pageFunction, arg); | ||
@@ -342,9 +359,14 @@ arrayHandle.dispose(); | ||
} | ||
async _waitForDisplayedAtStablePosition(options = {}) { | ||
async _waitForDisplayedAtStablePosition(deadline) { | ||
this._page._log(exports.inputLog, '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)); | ||
const timeoutMessage = 'element to be displayed and not moving'; | ||
const injectedResult = await helper_1.helper.waitWithDeadline(stablePromise, timeoutMessage, deadline); | ||
handleInjectedResult(injectedResult, timeoutMessage); | ||
this._page._log(exports.inputLog, '...done'); | ||
} | ||
async _waitForHitTargetAt(point, options = {}) { | ||
async _waitForHitTargetAt(point, deadline) { | ||
this._page._log(exports.inputLog, `waiting for element to receive pointer events at (${point.x},${point.y}) ...`); | ||
const frame = await this.ownerFrame(); | ||
@@ -361,19 +383,27 @@ 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 }); | ||
const timeoutMessage = 'element to receive pointer events'; | ||
const injectedResult = await helper_1.helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline); | ||
handleInjectedResult(injectedResult, timeoutMessage); | ||
this._page._log(exports.inputLog, '...done'); | ||
} | ||
} | ||
exports.ElementHandle = ElementHandle; | ||
exports.setFileInputFunction = async (element, payloads) => { | ||
const files = await Promise.all(payloads.map(async (file) => { | ||
const result = await fetch(`data:${file.type};base64,${file.data}`); | ||
return new File([await result.blob()], file.name, { type: file.type }); | ||
function toFileTransferPayload(files) { | ||
return files.map(file => ({ | ||
name: file.name, | ||
type: file.mimeType, | ||
data: file.buffer.toString('base64') | ||
})); | ||
const dt = new DataTransfer(); | ||
for (const file of files) | ||
dt.items.add(file); | ||
element.files = dt.files; | ||
element.dispatchEvent(new Event('input', { 'bubbles': true })); | ||
element.dispatchEvent(new Event('change', { 'bubbles': true })); | ||
}; | ||
} | ||
exports.toFileTransferPayload = toFileTransferPayload; | ||
function handleInjectedResult(injectedResult, timeoutMessage) { | ||
if (injectedResult.status === 'notconnected') | ||
throw new errors_1.NotConnectedError(); | ||
if (injectedResult.status === 'timeout') | ||
throw new errors_1.TimeoutError(`waiting for ${timeoutMessage} failed: timeout exceeded`); | ||
if (injectedResult.status === 'error') | ||
throw new Error(injectedResult.error); | ||
return injectedResult.value; | ||
} | ||
//# sourceMappingURL=dom.js.map |
@@ -26,2 +26,8 @@ "use strict"; | ||
} | ||
class NotConnectedError extends CustomError { | ||
constructor() { | ||
super('Element is not attached to the DOM'); | ||
} | ||
} | ||
exports.NotConnectedError = NotConnectedError; | ||
class TimeoutError extends CustomError { | ||
@@ -28,0 +34,0 @@ } |
@@ -32,4 +32,6 @@ "use strict"; | ||
Close: 'close', | ||
Crash: 'crash', | ||
Console: 'console', | ||
Dialog: 'dialog', | ||
Download: 'download', | ||
FileChooser: 'filechooser', | ||
@@ -36,0 +38,0 @@ DOMContentLoaded: 'domcontentloaded', |
@@ -25,3 +25,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
@@ -31,9 +30,11 @@ const ffConnection_1 = require("./ffConnection"); | ||
const ffPage_1 = require("./ffPage"); | ||
class FFBrowser extends platform.EventEmitter { | ||
constructor(connection) { | ||
super(); | ||
class FFBrowser extends browser_1.BrowserBase { | ||
constructor(connection, logger, isPersistent) { | ||
super(logger); | ||
this._defaultContext = null; | ||
this._firstPageCallback = () => { }; | ||
this._connection = connection; | ||
this._ffPages = new Map(); | ||
this._defaultContext = new FFBrowserContext(this, null, browserContext_1.validateBrowserContextOptions({})); | ||
if (isPersistent) | ||
this._defaultContext = new FFBrowserContext(this, null, browserContext_1.validateBrowserContextOptions({})); | ||
this._contexts = new Map(); | ||
@@ -48,8 +49,10 @@ this._connection.on(ffConnection_1.ConnectionEvents.Disconnected, () => { | ||
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)), | ||
]; | ||
this._firstPagePromise = new Promise(f => this._firstPageCallback = f); | ||
} | ||
static async connect(transport, attachToDefaultContext, slowMo) { | ||
const connection = new ffConnection_1.FFConnection(transport_1.SlowMoTransport.wrap(transport, slowMo)); | ||
const browser = new FFBrowser(connection); | ||
static async connect(transport, logger, attachToDefaultContext, slowMo) { | ||
const connection = new ffConnection_1.FFConnection(transport_1.SlowMoTransport.wrap(transport, slowMo), logger); | ||
const browser = new FFBrowser(connection, logger, attachToDefaultContext); | ||
await connection.send('Browser.enable', { attachToDefaultContext }); | ||
@@ -65,3 +68,3 @@ return browser; | ||
if (options.viewport) { | ||
// TODO: remove isMobile/hasTouch from the protocol? | ||
// TODO: remove isMobile from the protocol? | ||
if (options.isMobile) | ||
@@ -87,10 +90,13 @@ throw new Error('options.isMobile is not supported in Firefox'); | ||
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); | ||
@@ -104,5 +110,2 @@ await context._initialize(); | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
_onDetachedFromTarget(payload) { | ||
@@ -117,2 +120,3 @@ const ffPage = this._ffPages.get(payload.targetId); | ||
const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; | ||
helper_1.assert(context, `Unknown context id:${browserContextId}, _defaultContext: ${this._defaultContext}`); | ||
const session = this._connection.createSession(payload.sessionId, type); | ||
@@ -122,2 +126,6 @@ const opener = openerId ? this._ffPages.get(openerId) : null; | ||
this._ffPages.set(targetId, ffPage); | ||
if (opener && opener._initializedPage) { | ||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) | ||
signalBarrier.addPopup(ffPage.pageOrError()); | ||
} | ||
ffPage.pageOrError().then(async () => { | ||
@@ -134,12 +142,17 @@ this._firstPageCallback(); | ||
} | ||
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; | ||
} | ||
} | ||
@@ -149,3 +162,3 @@ exports.FFBrowser = FFBrowser; | ||
constructor(browser, browserContextId, options) { | ||
super(options); | ||
super(browser, options); | ||
this._browser = browser; | ||
@@ -166,2 +179,4 @@ this._browserContextId = browserContextId; | ||
await this.setOffline(this._options.offline); | ||
if (this._options.colorScheme) | ||
await this._setColorScheme(this._options.colorScheme); | ||
} | ||
@@ -178,3 +193,3 @@ _ffPages() { | ||
pages() { | ||
return this._ffPages().map(ffPage => ffPage._initializedPage()).filter(pageOrNull => !!pageOrNull); | ||
return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull); | ||
} | ||
@@ -185,2 +200,6 @@ async newPage() { | ||
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; | ||
}); | ||
@@ -246,2 +265,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) { | ||
@@ -272,2 +294,7 @@ this._options.httpCredentials = httpCredentials || undefined; | ||
} | ||
async unroute(url, handler) { | ||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); | ||
if (this._routes.length === 0) | ||
await this._browser._connection.send('Browser.setRequestInterception', { browserContextId: this._browserContextId || undefined, enabled: false }); | ||
} | ||
async close() { | ||
@@ -284,3 +311,3 @@ if (this._closed) | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -287,0 +314,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,7 +29,7 @@ Disconnected: Symbol('Disconnected'), | ||
exports.kBrowserCloseMessageId = -9999; | ||
class FFConnection extends platform.EventEmitter { | ||
constructor(transport) { | ||
class FFConnection extends events_1.EventEmitter { | ||
constructor(transport, logger) { | ||
super(); | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._transport = transport; | ||
this._logger = logger; | ||
this._lastId = 0; | ||
@@ -45,3 +46,2 @@ this._callbacks = new Map(); | ||
this.once = super.once; | ||
this._debugProtocol.color = '34'; | ||
} | ||
@@ -59,29 +59,29 @@ async send(method, params) { | ||
_rawSend(message) { | ||
const data = JSON.stringify(message); | ||
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); | ||
this._transport.send(data); | ||
if (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, '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 (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, '◀ 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,6 +115,7 @@ } | ||
}; | ||
class FFSession extends platform.EventEmitter { | ||
class FFSession extends events_1.EventEmitter { | ||
constructor(connection, targetType, sessionId, rawSend) { | ||
super(); | ||
this._disposed = false; | ||
this._crashed = false; | ||
this._callbacks = new Map(); | ||
@@ -131,3 +132,8 @@ this._connection = connection; | ||
} | ||
markAsCrashed() { | ||
this._crashed = true; | ||
} | ||
async send(method, params) { | ||
if (this._crashed) | ||
throw new Error('Page crashed'); | ||
if (this._disposed) | ||
@@ -146,3 +152,3 @@ throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); | ||
if (object.error) | ||
callback.reject(createProtocolError(callback.error, callback.method, object)); | ||
callback.reject(createProtocolError(callback.error, callback.method, object.error)); | ||
else | ||
@@ -166,6 +172,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); | ||
@@ -182,3 +188,4 @@ } | ||
return `{"id":${message.id} [evaluate injected script]}`; | ||
return JSON.stringify(message); | ||
} | ||
//# sourceMappingURL=ffConnection.js.map |
@@ -126,8 +126,8 @@ "use strict"; | ||
function checkException(exceptionDetails) { | ||
if (exceptionDetails) { | ||
if (exceptionDetails.value) | ||
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value)); | ||
else | ||
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack); | ||
} | ||
if (!exceptionDetails) | ||
return; | ||
if (exceptionDetails.value) | ||
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value)); | ||
else | ||
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack); | ||
} | ||
@@ -134,0 +134,0 @@ function deserializeValue({ unserializableValue, value }) { |
@@ -21,3 +21,3 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
const logger_1 = require("../logger"); | ||
class FFNetworkManager { | ||
@@ -62,3 +62,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'); | ||
}; | ||
@@ -141,8 +141,6 @@ const headers = {}; | ||
postData: postData ? Buffer.from(postData).toString('base64') : undefined | ||
}).catch(error => { | ||
helper_1.debugError(error); | ||
}); | ||
}).catch(logger_1.logError(this.request._page)); | ||
} | ||
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 +154,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', { | ||
@@ -164,5 +162,3 @@ requestId: this._id, | ||
base64body: responseBody ? responseBody.toString('base64') : undefined, | ||
}).catch(error => { | ||
helper_1.debugError(error); | ||
}); | ||
}).catch(logger_1.logError(this.request._page)); | ||
} | ||
@@ -173,5 +169,3 @@ async abort(errorCode) { | ||
errorCode, | ||
}).catch(error => { | ||
helper_1.debugError(error); | ||
}); | ||
}).catch(logger_1.logError(this.request._page)); | ||
} | ||
@@ -178,0 +172,0 @@ } |
@@ -24,3 +24,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const screenshotter_1 = require("../screenshotter"); | ||
@@ -32,7 +31,11 @@ const ffAccessibility_1 = require("./ffAccessibility"); | ||
const ffNetworkManager_1 = require("./ffNetworkManager"); | ||
const selectors_1 = require("../selectors"); | ||
const errors_1 = require("../errors"); | ||
const logger_1 = require("../logger"); | ||
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; | ||
class FFPage { | ||
constructor(session, browserContext, opener) { | ||
this.cspErrorsAsynchronousForInlineScipts = true; | ||
this._pageCallback = () => { }; | ||
this._initialized = false; | ||
this._initializedPage = null; | ||
this._workers = new Map(); | ||
@@ -54,3 +57,3 @@ this._session = session; | ||
helper_1.helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)), | ||
helper_1.helper.addEventListener(this._session, 'Page.navigationStarted', event => this._onNavigationStarted(event.frameId)), | ||
helper_1.helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), | ||
helper_1.helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), | ||
@@ -60,2 +63,3 @@ helper_1.helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), | ||
helper_1.helper.addEventListener(this._session, 'Page.linkClicked', event => this._onLinkClicked(event.phase)), | ||
helper_1.helper.addEventListener(this._session, 'Page.willOpenNewWindowAsynchronously', this._onWillOpenNewWindowAsynchronously.bind(this)), | ||
helper_1.helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), | ||
@@ -73,24 +77,10 @@ helper_1.helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), | ||
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._initializedPage = this._page; | ||
}); | ||
// 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); | ||
} | ||
_initializedPage() { | ||
return this._initialized ? this._page : null; | ||
} | ||
async pageOrError() { | ||
@@ -132,7 +122,15 @@ return this._pagePromise; | ||
} | ||
_onNavigationStarted(frameId) { | ||
this._page._frameManager.frameRequestedNavigation(frameId); | ||
_onWillOpenNewWindowAsynchronously() { | ||
for (const barrier of this._page._frameManager._signalBarriers) | ||
barrier.expectPopup(); | ||
} | ||
_onNavigationStarted(params) { | ||
this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId); | ||
} | ||
_onNavigationAborted(params) { | ||
const frame = this._page._frameManager.frame(params.frameId); | ||
if (params.errorText === 'Will download to file') { | ||
for (const barrier of this._page._frameManager._signalBarriers) | ||
barrier.expectDownload(); | ||
} | ||
for (const task of frame._frameTasks) | ||
@@ -165,3 +163,4 @@ task.onNewDocument(params.navigationId, new Error(params.errorText)); | ||
_onUncaughtError(params) { | ||
const error = new Error(params.message); | ||
const message = params.message.startsWith('Error: ') ? params.message.substring(7) : params.message; | ||
const error = new Error(message); | ||
error.stack = params.stack; | ||
@@ -177,3 +176,3 @@ this._page.emit(events_1.Events.Page.PageError, error); | ||
this._page.emit(events_1.Events.Page.Dialog, new dialog.Dialog(params.type, params.message, async (accept, promptText) => { | ||
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(helper_1.debugError); | ||
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(logger_1.logError(this._page)); | ||
}, params.defaultValue)); | ||
@@ -193,3 +192,3 @@ } | ||
const workerId = event.workerId; | ||
const worker = new page_1.Worker(event.url); | ||
const worker = new page_1.Worker(this._page, event.url); | ||
const workerSession = new ffConnection_1.FFSession(this._session._connection, 'worker', workerId, (message) => { | ||
@@ -232,2 +231,3 @@ this._session.send('Page.sendMessageToWorker', { | ||
async _onCrashed(event) { | ||
this._session.markAsCrashed(); | ||
this._page._didCrash(); | ||
@@ -260,6 +260,7 @@ } | ||
} | ||
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 | ||
}); | ||
@@ -326,3 +327,3 @@ } | ||
}); | ||
return platform.Buffer.from(data, 'base64'); | ||
return Buffer.from(data, 'base64'); | ||
} | ||
@@ -374,2 +375,6 @@ async resetViewport() { | ||
rect, | ||
}).catch(e => { | ||
if (e instanceof Error && e.message.includes('Node is detached from document')) | ||
throw new errors_1.NotConnectedError(); | ||
throw e; | ||
}); | ||
@@ -381,3 +386,3 @@ } | ||
objectId: toRemoteObject(handle).objectId, | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result) | ||
@@ -391,3 +396,3 @@ return null; | ||
async setInputFiles(handle, files) { | ||
await handle.evaluate(dom.setFileInputFunction, files); | ||
await handle._evaluateInUtility(({ injected, node }, files) => injected.setInputFiles(node, files), dom.toFileTransferPayload(files)); | ||
} | ||
@@ -413,4 +418,3 @@ async adoptElementHandle(handle, to) { | ||
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) => { | ||
@@ -417,0 +421,0 @@ const frame = await handle.contentFrame().catch(e => null); |
@@ -19,7 +19,11 @@ "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 dom = require("./dom"); | ||
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"); | ||
const hints_1 = require("./hints"); | ||
class FrameManager { | ||
@@ -29,3 +33,3 @@ constructor(page) { | ||
this._consoleMessageTags = new Map(); | ||
this._pendingNavigationBarriers = new Set(); | ||
this._signalBarriers = new Set(); | ||
this._page = page; | ||
@@ -73,7 +77,7 @@ this._mainFrame = undefined; | ||
} | ||
async waitForNavigationsCreatedBy(action, options = {}, input) { | ||
if (options.waitUntil === 'nowait') | ||
async waitForSignalsCreatedBy(action, deadline, options = {}, input) { | ||
if (options.noWaitAfter) | ||
return action(); | ||
const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }); | ||
this._pendingNavigationBarriers.add(barrier); | ||
const barrier = new SignalBarrier(options, deadline); | ||
this._signalBarriers.add(barrier); | ||
try { | ||
@@ -85,31 +89,39 @@ const result = await action(); | ||
// Resolve in the next task, after all waitForNavigations. | ||
await new Promise(platform.makeWaitForNextTask()); | ||
await new Promise(helper_1.helper.makeWaitForNextTask()); | ||
return result; | ||
} | ||
finally { | ||
this._pendingNavigationBarriers.delete(barrier); | ||
this._signalBarriers.delete(barrier); | ||
} | ||
} | ||
frameWillPotentiallyRequestNavigation() { | ||
for (const barrier of this._pendingNavigationBarriers) | ||
for (const barrier of this._signalBarriers) | ||
barrier.retain(); | ||
} | ||
frameDidPotentiallyRequestNavigation() { | ||
for (const barrier of this._pendingNavigationBarriers) | ||
for (const barrier of this._signalBarriers) | ||
barrier.release(); | ||
} | ||
frameRequestedNavigation(frameId) { | ||
frameRequestedNavigation(frameId, documentId) { | ||
const frame = this._frames.get(frameId); | ||
if (!frame) | ||
return; | ||
for (const barrier of this._pendingNavigationBarriers) | ||
barrier.addFrame(frame); | ||
for (const barrier of this._signalBarriers) | ||
barrier.addFrameNavigation(frame); | ||
frame._pendingDocumentId = documentId; | ||
} | ||
frameUpdatedDocumentIdForNavigation(frameId, documentId) { | ||
const frame = this._frames.get(frameId); | ||
if (!frame) | ||
return; | ||
frame._pendingDocumentId = documentId; | ||
} | ||
frameCommittedNewDocumentNavigation(frameId, url, name, documentId, initial) { | ||
const frame = this._frames.get(frameId); | ||
for (const child of frame.childFrames()) | ||
this._removeFramesRecursively(child); | ||
this.removeChildFramesRecursively(frame); | ||
frame._url = url; | ||
frame._name = name; | ||
helper_1.assert(!frame._pendingDocumentId || frame._pendingDocumentId === documentId); | ||
frame._lastDocumentId = documentId; | ||
frame._pendingDocumentId = ''; | ||
for (const task of frame._frameTasks) | ||
@@ -164,8 +176,5 @@ task.onNewDocument(documentId); | ||
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId)); | ||
this._stopNetworkIdleTimer(frame, 'networkidle0'); | ||
frame._stopNetworkIdleTimer(); | ||
if (frame._inflightRequests.size === 0) | ||
this._startNetworkIdleTimer(frame, 'networkidle0'); | ||
this._stopNetworkIdleTimer(frame, 'networkidle2'); | ||
if (frame._inflightRequests.size <= 2) | ||
this._startNetworkIdleTimer(frame, 'networkidle2'); | ||
frame._startNetworkIdleTimer(); | ||
} | ||
@@ -191,4 +200,5 @@ requestStarted(request) { | ||
if (request._documentId) { | ||
const isCurrentDocument = request.frame()._lastDocumentId === request._documentId; | ||
if (!isCurrentDocument) { | ||
const isPendingDocument = request.frame()._pendingDocumentId === request._documentId; | ||
if (isPendingDocument) { | ||
request.frame()._pendingDocumentId = ''; | ||
let errorText = request.failure().errorText; | ||
@@ -214,5 +224,8 @@ if (canceled) | ||
} | ||
_removeFramesRecursively(frame) { | ||
removeChildFramesRecursively(frame) { | ||
for (const child of frame.childFrames()) | ||
this._removeFramesRecursively(child); | ||
} | ||
_removeFramesRecursively(frame) { | ||
this.removeChildFramesRecursively(frame); | ||
frame._onDetached(); | ||
@@ -230,5 +243,3 @@ this._frames.delete(frame._id); | ||
if (frame._inflightRequests.size === 0) | ||
this._startNetworkIdleTimer(frame, 'networkidle0'); | ||
if (frame._inflightRequests.size === 2) | ||
this._startNetworkIdleTimer(frame, 'networkidle2'); | ||
frame._startNetworkIdleTimer(); | ||
} | ||
@@ -241,20 +252,4 @@ _inflightRequestStarted(request) { | ||
if (frame._inflightRequests.size === 1) | ||
this._stopNetworkIdleTimer(frame, 'networkidle0'); | ||
if (frame._inflightRequests.size === 3) | ||
this._stopNetworkIdleTimer(frame, 'networkidle2'); | ||
frame._stopNetworkIdleTimer(); | ||
} | ||
_startNetworkIdleTimer(frame, event) { | ||
helper_1.assert(!frame._networkIdleTimers.has(event)); | ||
if (frame._firedLifecycleEvents.has(event)) | ||
return; | ||
frame._networkIdleTimers.set(event, setTimeout(() => { | ||
this.frameLifecycleEvent(frame._id, event); | ||
}, 500)); | ||
} | ||
_stopNetworkIdleTimer(frame, event) { | ||
const timeoutId = frame._networkIdleTimers.get(event); | ||
if (timeoutId) | ||
clearTimeout(timeoutId); | ||
frame._networkIdleTimers.delete(event); | ||
} | ||
interceptConsoleMessage(message) { | ||
@@ -276,2 +271,3 @@ if (message.type() !== 'debug') | ||
this._lastDocumentId = ''; | ||
this._pendingDocumentId = ''; | ||
this._frameTasks = new Set(); | ||
@@ -284,3 +280,2 @@ this._url = ''; | ||
this._inflightRequests = new Set(); | ||
this._networkIdleTimers = new Map(); | ||
this._setContentCounter = 0; | ||
@@ -330,2 +325,5 @@ this._detachedCallback = () => { }; | ||
async waitForNavigation(options = {}) { | ||
return this._waitForNavigation(options); | ||
} | ||
async _waitForNavigation(options = {}) { | ||
const frameTask = new FrameTask(this, options); | ||
@@ -338,3 +336,4 @@ let documentId; | ||
const request = documentId ? frameTask.request(documentId) : null; | ||
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil); | ||
if (options.waitUntil !== 'commit') | ||
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil); | ||
frameTask.done(); | ||
@@ -371,11 +370,3 @@ return request ? request._finalRequest().response() : null; | ||
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); | ||
} | ||
@@ -385,7 +376,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()) { | ||
@@ -405,13 +397,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); | ||
@@ -422,4 +412,3 @@ arrayHandle.dispose(); | ||
async $$(selector) { | ||
const context = await this._mainContext(); | ||
return context._$$(selector); | ||
return selectors_1.selectors._queryAll(this, selector); | ||
} | ||
@@ -479,8 +468,16 @@ async content() { | ||
return (await context.evaluateHandleInternal(addScriptUrl, { url, type })).asElement(); | ||
let result; | ||
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, ''); | ||
return (await context.evaluateHandleInternal(addScriptContent, { content: contents, type })).asElement(); | ||
result = (await context.evaluateHandleInternal(addScriptContent, { content: contents, type })).asElement(); | ||
} | ||
return (await context.evaluateHandleInternal(addScriptContent, { content: content, type })).asElement(); | ||
else { | ||
result = (await context.evaluateHandleInternal(addScriptContent, { content: content, type })).asElement(); | ||
} | ||
// Another round trip to the browser to ensure that we receive CSP error messages | ||
// (if any) logged asynchronously in a separate task on the content main thread. | ||
if (this._page._delegate.cspErrorsAsynchronousForInlineScipts) | ||
await context.evaluateInternal(() => true); | ||
return result; | ||
}); | ||
@@ -494,3 +491,3 @@ async function addScriptUrl(options) { | ||
script.onload = res; | ||
script.onerror = rej; | ||
script.onerror = e => rej(typeof e === 'string' ? new Error(e) : new Error(`Failed to load script at ${script.src}`)); | ||
}); | ||
@@ -522,3 +519,3 @@ document.head.appendChild(script); | ||
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, '') + '*/'; | ||
@@ -584,58 +581,64 @@ return (await context.evaluateHandleInternal(addStyleContent, contents)).asElement(); | ||
} | ||
async click(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.click(options); | ||
handle.dispose(); | ||
async _retryWithSelectorIfNotConnected(selector, options, action) { | ||
const deadline = this._page._timeoutSettings.computeDeadline(options); | ||
while (!helper_1.helper.isPastDeadline(deadline)) { | ||
try { | ||
const { world, task } = selectors_1.selectors._waitForSelectorTask(selector, 'attached', deadline); | ||
const handle = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selector}"`); | ||
const element = handle.asElement(); | ||
try { | ||
return await action(element, deadline); | ||
} | ||
finally { | ||
element.dispose(); | ||
} | ||
} | ||
catch (e) { | ||
if (!(e instanceof errors_1.NotConnectedError)) | ||
throw e; | ||
this._page._log(dom.inputLog, 'Element was detached from the DOM, retrying'); | ||
} | ||
} | ||
throw new errors_1.TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`); | ||
} | ||
async dblclick(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.dblclick(options); | ||
handle.dispose(); | ||
async click(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.click(helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async fill(selector, value, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.fill(value, options); | ||
handle.dispose(); | ||
async dblclick(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.dblclick(helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async focus(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.focus(); | ||
handle.dispose(); | ||
async fill(selector, value, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.fill(value, helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async hover(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.hover(options); | ||
handle.dispose(); | ||
async focus(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.focus()); | ||
} | ||
async selectOption(selector, values, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
const result = await handle.selectOption(values, options); | ||
handle.dispose(); | ||
return result; | ||
async hover(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.hover(helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async type(selector, text, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.type(text, options); | ||
handle.dispose(); | ||
async selectOption(selector, values, options = {}) { | ||
return await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.selectOption(values, helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async press(selector, key, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.press(key, options); | ||
handle.dispose(); | ||
async setInputFiles(selector, files, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.setInputFiles(files, helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async check(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.check(options); | ||
handle.dispose(); | ||
async type(selector, text, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.type(text, helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async uncheck(selector, options) { | ||
const handle = await this._waitForSelectorInUtilityContext(selector, options); | ||
await handle.uncheck(options); | ||
handle.dispose(); | ||
async press(selector, key, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.press(key, helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async check(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.check(helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async uncheck(selector, options = {}) { | ||
await this._retryWithSelectorIfNotConnected(selector, options, (handle, deadline) => handle.uncheck(helper_1.helper.optionsWithUpdatedTimeout(options, deadline))); | ||
} | ||
async waitFor(selectorOrFunctionOrTimeout, options = {}, arg) { | ||
if (helper_1.helper.isString(selectorOrFunctionOrTimeout)) | ||
return this.waitForSelector(selectorOrFunctionOrTimeout, options); | ||
if (helper_1.helper.isNumber(selectorOrFunctionOrTimeout)) | ||
if (helper_1.helper.isNumber(selectorOrFunctionOrTimeout)) { | ||
hints_1.waitForTimeWasUsed(this._page); | ||
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); | ||
} | ||
if (typeof selectorOrFunctionOrTimeout === 'function') | ||
@@ -645,10 +648,5 @@ return this.waitForFunction(selectorOrFunctionOrTimeout, arg, options); | ||
} | ||
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(); | ||
} | ||
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)) | ||
@@ -663,7 +661,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); | ||
} | ||
@@ -685,5 +681,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); | ||
@@ -723,23 +719,17 @@ if (data.context) | ||
} | ||
_startNetworkIdleTimer() { | ||
helper_1.assert(!this._networkIdleTimer); | ||
if (this._firedLifecycleEvents.has('networkidle')) | ||
return; | ||
this._networkIdleTimer = setTimeout(() => { this._page._frameManager.frameLifecycleEvent(this._id, 'networkidle'); }, 500); | ||
} | ||
_stopNetworkIdleTimer() { | ||
if (this._networkIdleTimer) | ||
clearTimeout(this._networkIdleTimer); | ||
this._networkIdleTimer = undefined; | ||
} | ||
} | ||
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 = () => { }; | ||
@@ -757,6 +747,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)); | ||
} | ||
@@ -828,8 +816,11 @@ terminate(error) { | ||
} | ||
class PendingNavigationBarrier { | ||
constructor(options) { | ||
class SignalBarrier { | ||
constructor(options, deadline) { | ||
this._frameIds = new Map(); | ||
this._protectCount = 0; | ||
this._expectedPopups = 0; | ||
this._expectedDownloads = 0; | ||
this._promiseCallback = () => { }; | ||
this._options = options; | ||
this._deadline = deadline; | ||
this._promise = new Promise(f => this._promiseCallback = f); | ||
@@ -842,7 +833,30 @@ this.retain(); | ||
} | ||
async addFrame(frame) { | ||
async addFrameNavigation(frame) { | ||
this.retain(); | ||
await frame.waitForNavigation(this._options).catch(e => { }); | ||
const options = helper_1.helper.optionsWithUpdatedTimeout(this._options, this._deadline); | ||
await frame._waitForNavigation({ ...options, waitUntil: 'commit' }).catch(e => { }); | ||
this.release(); | ||
} | ||
async expectPopup() { | ||
++this._expectedPopups; | ||
} | ||
async unexpectPopup() { | ||
--this._expectedPopups; | ||
this._maybeResolve(); | ||
} | ||
async addPopup(pageOrError) { | ||
if (this._expectedPopups) | ||
--this._expectedPopups; | ||
this.retain(); | ||
await pageOrError; | ||
this.release(); | ||
} | ||
async expectDownload() { | ||
++this._expectedDownloads; | ||
} | ||
async addDownload() { | ||
if (this._expectedDownloads) | ||
--this._expectedDownloads; | ||
this._maybeResolve(); | ||
} | ||
retain() { | ||
@@ -856,6 +870,7 @@ ++this._protectCount; | ||
async _maybeResolve() { | ||
if (!this._protectCount && !this._frameIds.size) | ||
if (!this._protectCount && !this._expectedPopups && !this._expectedDownloads && !this._frameIds.size) | ||
this._promiseCallback(); | ||
} | ||
} | ||
exports.SignalBarrier = SignalBarrier; | ||
class FrameTask { | ||
@@ -873,3 +888,3 @@ constructor(frame, options, url) { | ||
if (timeout) { | ||
const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded'; | ||
const errorMessage = 'Navigation timeout exceeded'; | ||
timeoutPromise = new Promise(fulfill => this._timer = setTimeout(fulfill, timeout)) | ||
@@ -876,0 +891,0 @@ .then(() => { throw new errors_1.TimeoutError(errorMessage); }); |
@@ -19,5 +19,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const crypto = require("crypto"); | ||
const fs = require("fs"); | ||
const util = require("util"); | ||
const errors_1 = require("./errors"); | ||
const platform = require("./platform"); | ||
exports.debugError = platform.debug(`pw:error`); | ||
class Helper { | ||
@@ -42,3 +43,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 +56,2 @@ contents += '//# sourceURL=' + fun.path.replace(/\n/g, ''); | ||
static installApiHooks(className, classType) { | ||
const log = platform.debug('pw:api'); | ||
for (const methodName of Reflect.ownKeys(classType.prototype)) { | ||
@@ -62,3 +62,3 @@ const method = Reflect.get(classType.prototype, methodName); | ||
const isAsync = method.constructor.name === 'AsyncFunction'; | ||
if (!isAsync && !log.enabled) | ||
if (!isAsync) | ||
continue; | ||
@@ -68,22 +68,2 @@ Reflect.set(classType.prototype, methodName, function (...args) { | ||
Error.captureStackTrace(syncStack); | ||
if (log.enabled) { | ||
const frames = syncStack.stack.substring('Error\n'.length) | ||
.split('\n') | ||
.map((f) => f.replace(/\s+at\s/, '').trim()); | ||
const userCall = frames.length <= 1 || !frames[1].includes('playwright/lib'); | ||
if (userCall) { | ||
const match = /([^/\\]+)(:\d+:\d+)[)]?$/.exec(frames[1]); | ||
let location = ''; | ||
if (match) { | ||
const fileName = exports.helper.trimMiddle(match[1], 20 - match[2].length); | ||
location = `\u001b[33m[${fileName}${match[2]}]\u001b[39m `; | ||
} | ||
if (args.length) | ||
log(`${location}${className}.${methodName} %o`, args); | ||
else | ||
log(`${location}${className}.${methodName}`); | ||
} | ||
} | ||
if (!isAsync) | ||
return method.call(this, ...args); | ||
return method.call(this, ...args).catch((e) => { | ||
@@ -123,4 +103,3 @@ const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1); | ||
} | ||
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) { | ||
let eventTimeout; | ||
static async waitForEvent(emitter, eventName, predicate, deadline, abortPromise) { | ||
let resolveCallback = () => { }; | ||
@@ -142,7 +121,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() { | ||
@@ -152,3 +129,3 @@ Helper.removeEventListeners([listener]); | ||
} | ||
const result = await Promise.race([promise, abortPromise]).then(r => { | ||
return await Promise.race([promise, abortPromise]).then(r => { | ||
cleanup(); | ||
@@ -160,13 +137,11 @@ return r; | ||
}); | ||
if (result instanceof Error) | ||
throw result; | ||
return result; | ||
} | ||
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 { | ||
@@ -272,2 +247,49 @@ 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 isPastDeadline(deadline) { | ||
return deadline !== Number.MAX_SAFE_INTEGER && this.monotonicTime() >= deadline; | ||
} | ||
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) }; | ||
} | ||
} | ||
@@ -274,0 +296,0 @@ function assert(value, message) { |
113
lib/input.js
@@ -38,38 +38,9 @@ "use strict"; | ||
_keyDescriptionForString(keyString) { | ||
let description = usKeyboardLayout.get(keyString); | ||
helper_1.assert(description, `Unknown key: "${keyString}"`); | ||
const shift = this._pressedModifiers.has('Shift'); | ||
const description = { | ||
key: '', | ||
keyCode: 0, | ||
keyCodeWithoutLocation: 0, | ||
code: '', | ||
text: '', | ||
location: 0 | ||
}; | ||
const definition = keyboardLayout.keyDefinitions[keyString]; | ||
helper_1.assert(definition, `Unknown key: "${keyString}"`); | ||
if (definition.key) | ||
description.key = definition.key; | ||
if (shift && definition.shiftKey) | ||
description.key = definition.shiftKey; | ||
if (definition.keyCode) | ||
description.keyCode = definition.keyCode; | ||
if (shift && definition.shiftKeyCode) | ||
description.keyCode = definition.shiftKeyCode; | ||
if (definition.code) | ||
description.code = definition.code; | ||
if (definition.location) | ||
description.location = definition.location; | ||
if (description.key.length === 1) | ||
description.text = description.key; | ||
if (definition.text) | ||
description.text = definition.text; | ||
if (shift && definition.shiftText) | ||
description.text = definition.shiftText; | ||
description = shift && description.shifted ? description.shifted : description; | ||
// if any modifiers besides shift are pressed, no text should be sent | ||
if (this._pressedModifiers.size > 1 || (!this._pressedModifiers.has('Shift') && this._pressedModifiers.size === 1)) | ||
description.text = ''; | ||
if (definition.keyCodeWithoutLocation) | ||
description.keyCodeWithoutLocation = definition.keyCodeWithoutLocation; | ||
else | ||
description.keyCodeWithoutLocation = description.keyCode; | ||
return { ...description, text: '' }; | ||
return description; | ||
@@ -90,3 +61,3 @@ } | ||
for (const char of text) { | ||
if (keyboardLayout.keyDefinitions[char]) { | ||
if (usKeyboardLayout.has(char)) { | ||
await this.press(char, { delay }); | ||
@@ -102,2 +73,21 @@ } | ||
async press(key, options = {}) { | ||
function split(keyString) { | ||
const keys = []; | ||
let building = ''; | ||
for (const char of keyString) { | ||
if (char === '+' && building) { | ||
keys.push(building); | ||
building = ''; | ||
} | ||
else { | ||
building += char; | ||
} | ||
} | ||
keys.push(building); | ||
return keys; | ||
} | ||
const tokens = split(key); | ||
key = tokens[tokens.length - 1]; | ||
for (let i = 0; i < tokens.length - 1; ++i) | ||
await this.down(tokens[i]); | ||
await this.down(key); | ||
@@ -107,2 +97,4 @@ if (options.delay) | ||
await this.up(key); | ||
for (let i = tokens.length - 2; i >= 0; --i) | ||
await this.up(tokens[i]); | ||
} | ||
@@ -112,3 +104,3 @@ async _ensureModifiers(modifiers) { | ||
if (!kModifiers.includes(modifier)) | ||
throw new Error('Uknown modifier ' + modifier); | ||
throw new Error('Unknown modifier ' + modifier); | ||
} | ||
@@ -193,2 +185,53 @@ const restore = Array.from(this._pressedModifiers); | ||
exports.Mouse = Mouse; | ||
const aliases = new Map([ | ||
['ShiftLeft', ['Shift']], | ||
['ControlLeft', ['Control']], | ||
['AltLeft', ['Alt']], | ||
['MetaLeft', ['Meta']], | ||
['Enter', ['\n', '\r']], | ||
]); | ||
const usKeyboardLayout = buildLayoutClosure(keyboardLayout.USKeyboardLayout); | ||
function buildLayoutClosure(layout) { | ||
const result = new Map(); | ||
for (const code in layout) { | ||
const definition = layout[code]; | ||
const description = { | ||
key: definition.key || '', | ||
keyCode: definition.keyCode || 0, | ||
keyCodeWithoutLocation: definition.keyCodeWithoutLocation || definition.keyCode || 0, | ||
code, | ||
text: definition.text || '', | ||
location: definition.location || 0, | ||
}; | ||
if (definition.key.length === 1) | ||
description.text = description.key; | ||
// Generate shifted definition. | ||
let shiftedDescription; | ||
if (definition.shiftKey) { | ||
helper_1.assert(definition.shiftKey.length === 1); | ||
shiftedDescription = { ...description }; | ||
shiftedDescription.key = definition.shiftKey; | ||
shiftedDescription.text = definition.shiftKey; | ||
if (definition.shiftKeyCode) | ||
shiftedDescription.keyCode = definition.shiftKeyCode; | ||
} | ||
// Map from code: Digit3 -> { ... descrption, shifted } | ||
result.set(code, { ...description, shifted: shiftedDescription }); | ||
// Map from aliases: Shift -> non-shiftable definition | ||
if (aliases.has(code)) { | ||
for (const alias of aliases.get(code)) | ||
result.set(alias, description); | ||
} | ||
// Do not use numpad when converting keys to codes. | ||
if (definition.location) | ||
continue; | ||
// Map from key, no shifted | ||
if (description.key.length === 1) | ||
result.set(description.key, description); | ||
// Map from shiftKey, no shifted | ||
if (shiftedDescription) | ||
result.set(shiftedDescription.key, { ...shiftedDescription, shifted: undefined }); | ||
} | ||
return result; | ||
} | ||
//# sourceMappingURL=input.js.map |
@@ -18,6 +18,7 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const platform = require("./platform"); | ||
const helper_1 = require("./helper"); | ||
class ExecutionContext { | ||
constructor(delegate) { | ||
constructor(delegate, logger) { | ||
this._delegate = delegate; | ||
this._logger = logger; | ||
} | ||
@@ -108,3 +109,3 @@ _doEvaluateInternal(returnByValue, waitForNavigations, pageFunction, ...args) { | ||
const pushHandle = (handle) => { | ||
const guid = platform.guid(); | ||
const guid = helper_1.helper.guid(); | ||
guids.push(guid); | ||
@@ -111,0 +112,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 = []) { | ||
@@ -78,2 +80,3 @@ if (!Array.isArray(urls)) | ||
this._frame = frame; | ||
this._page = frame._page; | ||
this._redirectedFrom = redirectedFrom; | ||
@@ -108,3 +111,3 @@ if (redirectedFrom) | ||
headers() { | ||
return this._headers; | ||
return { ...this._headers }; | ||
} | ||
@@ -171,4 +174,4 @@ response() { | ||
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) | ||
}; | ||
@@ -214,3 +217,3 @@ } | ||
headers() { | ||
return this._headers; | ||
return { ...this._headers }; | ||
} | ||
@@ -217,0 +220,0 @@ finished() { |
@@ -30,4 +30,7 @@ "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"); | ||
const fileChooser_1 = require("./fileChooser"); | ||
const logger_1 = require("./logger"); | ||
class Page extends extendedEventEmitter_1.ExtendedEventEmitter { | ||
constructor(delegate, browserContext) { | ||
@@ -69,2 +72,8 @@ super(); | ||
} | ||
_abortPromiseForEvent(event) { | ||
return this._disconnectedPromise; | ||
} | ||
_computeDeadline(options) { | ||
return this._timeoutSettings.computeDeadline(options); | ||
} | ||
_didClose() { | ||
@@ -77,6 +86,3 @@ helper_1.assert(!this._closed, 'Page closed twice'); | ||
_didCrash() { | ||
const error = new Error('Page crashed!'); | ||
// Do not report node.js stack. | ||
error.stack = 'Error: ' + error.message; // Stack is supposed to contain error message as the first line. | ||
this.emit('error', error); | ||
this.emit(events_1.Events.Page.Crash); | ||
} | ||
@@ -94,3 +100,3 @@ _didDisconnect() { | ||
} | ||
const fileChooser = { element: handle, multiple }; | ||
const fileChooser = new fileChooser_1.FileChooser(this, handle, multiple); | ||
this.emit(events_1.Events.Page.FileChooser, fileChooser); | ||
@@ -197,10 +203,4 @@ } | ||
} | ||
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) => { | ||
@@ -210,6 +210,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) => { | ||
@@ -219,3 +219,3 @@ if (helper_1.helper.isString(urlOrPredicate) || helper_1.helper.isRegExp(urlOrPredicate)) | ||
return urlOrPredicate(response); | ||
}, timeout, this._disconnectedPromise); | ||
}, deadline, this._disconnectedPromise); | ||
} | ||
@@ -247,3 +247,3 @@ async goBack(options) { | ||
this._state.colorScheme = options.colorScheme; | ||
await this._delegate.setEmulateMedia(this._state.mediaType, this._state.colorScheme); | ||
await this._delegate.updateEmulateMedia(); | ||
} | ||
@@ -270,2 +270,6 @@ async setViewportSize(viewportSize) { | ||
} | ||
async unroute(url, handler) { | ||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); | ||
await this._delegate.updateRequestInterception(); | ||
} | ||
_requestStarted(request) { | ||
@@ -328,2 +332,5 @@ this.emit(events_1.Events.Page.Request, request); | ||
} | ||
async setInputFiles(selector, files, options) { | ||
return this.mainFrame().setInputFiles(selector, files, options); | ||
} | ||
async type(selector, text, options) { | ||
@@ -381,8 +388,15 @@ return this.mainFrame().type(selector, text, options); | ||
} | ||
_isLogEnabled(log) { | ||
return this._browserContext._isLogEnabled(log); | ||
} | ||
_log(log, message, ...args) { | ||
return this._browserContext._log(log, message, ...args); | ||
} | ||
} | ||
exports.Page = Page; | ||
class Worker extends platform.EventEmitter { | ||
constructor(url) { | ||
class Worker extends events_2.EventEmitter { | ||
constructor(logger, url) { | ||
super(); | ||
this._existingExecutionContext = null; | ||
this._logger = logger; | ||
this._url = url; | ||
@@ -393,3 +407,3 @@ this._executionContextCallback = () => { }; | ||
_createExecutionContext(delegate) { | ||
this._existingExecutionContext = new js.ExecutionContext(delegate); | ||
this._existingExecutionContext = new js.ExecutionContext(delegate, this._logger); | ||
this._executionContextCallback(this._existingExecutionContext); | ||
@@ -430,3 +444,3 @@ } | ||
} | ||
context.evaluateInternal(expression).catch(helper_1.debugError); | ||
context.evaluateInternal(expression).catch(logger_1.logError(page)); | ||
function deliverResult(name, seq, result) { | ||
@@ -433,0 +447,0 @@ window[name]['callbacks'].get(seq).resolve(result); |
@@ -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,26 @@ "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', 'css:light', | ||
'xpath', 'xpath:light', | ||
'text', 'text:light', | ||
'id', 'id:light', | ||
'data-testid', 'data-testid:light', | ||
'data-test-id', 'data-test-id:light', | ||
'data-test', 'data-test:light' | ||
]); | ||
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' || name === 'zs:light') | ||
throw new Error(`"${name}" is a predefined selector engine`); | ||
@@ -40,13 +47,156 @@ 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) { | ||
helper_1.assert(helper_1.helper.isString(selector), `selector must be a string`); | ||
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,6 +88,7 @@ 'mac10.15': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], | ||
'linux': ['pw_run.sh'], | ||
'mac10.13': undefined, | ||
'mac10.14': ['pw_run.sh'], | ||
'mac10.15': ['pw_run.sh'], | ||
'win32': ['MiniBrowser.exe'], | ||
'win64': ['MiniBrowser.exe'], | ||
'win32': ['Playwright.exe'], | ||
'win64': ['Playwright.exe'], | ||
}, | ||
@@ -114,10 +119,9 @@ }; | ||
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,4 @@ const pipeTransport_1 = require("./pipeTransport"); | ||
const events_1 = require("../events"); | ||
const transport_1 = require("../transport"); | ||
const logger_1 = require("../logger"); | ||
class Chromium { | ||
@@ -42,22 +44,25 @@ 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, logger } = await this._launchServer(options, 'local'); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, false, logger, 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, logger } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await crBrowser_1.CRBrowser.connect(transport, true, logger, 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 = [], 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.'); | ||
const logger = new logger_1.RootLogger(options.logger); | ||
let temporaryUserDataDir = null; | ||
@@ -70,5 +75,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 +84,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, | ||
@@ -88,14 +92,13 @@ args: chromeArguments, | ||
handleSIGHUP, | ||
dumpio, | ||
pipe: launchType !== 'server', | ||
logger, | ||
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 +110,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], logger); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null); | ||
return { browserServer, transport, downloadsPath, logger }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return crBrowser_1.CRBrowser.connect(transport, false, options.slowMo); | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return crBrowser_1.CRBrowser.connect(transport, false, new logger_1.RootLogger(options.logger), options.slowMo); | ||
}); | ||
} | ||
_defaultArgs(options = {}, launchType, userDataDir, port) { | ||
_defaultArgs(options = {}, launchType, userDataDir) { | ||
const { devtools = false, headless = !devtools, args = [], } = options; | ||
@@ -134,3 +128,3 @@ const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); | ||
throw new Error('Pass userDataDir parameter instead of specifying --user-data-dir argument'); | ||
if (args.find(arg => arg.startsWith('--remote-debugging-'))) | ||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) | ||
throw new Error('Playwright manages remote debugging connection itself.'); | ||
@@ -141,3 +135,3 @@ if (launchType !== 'persistent' && args.find(arg => !arg.startsWith('-'))) | ||
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) | ||
@@ -160,3 +154,109 @@ chromeArguments.push('--auto-open-devtools-for-tabs'); | ||
exports.Chromium = Chromium; | ||
const mkdtempAsync = platform.promisify(fs.mkdtemp); | ||
function wrapTransportWithWebSocket(transport, logger, 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', logger_1.logError(logger)); | ||
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-'); | ||
@@ -163,0 +263,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,7 @@ 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 logger_1 = require("../logger"); | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
class Firefox { | ||
@@ -42,31 +45,31 @@ 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, logger } = await this._launchServer(options, 'local'); | ||
const browser = await transport_1.WebSocketTransport.connect(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, logger, 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, logger } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await transport_1.WebSocketTransport.connect(browserServer.wsEndpoint(), transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, logger, 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 = [], 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 logger = new logger_1.RootLogger(options.logger); | ||
const firefoxArguments = []; | ||
@@ -79,5 +82,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 +91,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, | ||
@@ -101,14 +103,11 @@ args: firefoxArguments, | ||
handleSIGHUP, | ||
dumpio, | ||
logger, | ||
pipe: false, | ||
tempDir: temporaryProfileDir || 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 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,9 +121,14 @@ 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, logger, port))) : new browserServer_1.WebSocketWrapper(innerEndpoint, []); | ||
browserWSEndpoint = webSocketWrapper.wsEndpoint; | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); | ||
return { browserServer, downloadsPath, logger }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, false, options.slowMo); | ||
const logger = new logger_1.RootLogger(options.logger); | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return ffBrowser_1.FFBrowser.connect(transport, logger, false, options.slowMo); | ||
}); | ||
@@ -135,3 +139,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')); | ||
@@ -155,4 +159,9 @@ if (userDataDirArg) | ||
firefoxArguments.push(...args); | ||
if (args.every(arg => arg.startsWith('-'))) | ||
firefoxArguments.push('about:blank'); | ||
if (launchType === 'persistent') { | ||
if (args.every(arg => arg.startsWith('-'))) | ||
firefoxArguments.push('about:blank'); | ||
} | ||
else { | ||
firefoxArguments.push('-silent'); | ||
} | ||
return firefoxArguments; | ||
@@ -162,2 +171,117 @@ } | ||
exports.Firefox = Firefox; | ||
function wrapTransportWithWebSocket(transport, logger, 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', logger_1.logError(logger)); | ||
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,8 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const platform_1 = require("../platform"); | ||
const logger_1 = require("../logger"); | ||
class PipeTransport { | ||
constructor(pipeWrite, pipeRead, closeCallback) { | ||
constructor(pipeWrite, pipeRead, logger) { | ||
this._pendingMessage = ''; | ||
this._waitForNextTask = platform_1.makeWaitForNextTask(); | ||
this._waitForNextTask = helper_1.helper.makeWaitForNextTask(); | ||
this._pipeWrite = pipeWrite; | ||
this._closeCallback = closeCallback; | ||
this._eventListeners = [ | ||
@@ -35,4 +34,4 @@ helper_1.helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)), | ||
}), | ||
helper_1.helper.addEventListener(pipeRead, 'error', helper_1.debugError), | ||
helper_1.helper.addEventListener(pipeWrite, 'error', helper_1.debugError), | ||
helper_1.helper.addEventListener(pipeRead, 'error', logger_1.logError(logger)), | ||
helper_1.helper.addEventListener(pipeWrite, 'error', logger_1.logError(logger)), | ||
]; | ||
@@ -43,7 +42,7 @@ this.onmessage = undefined; | ||
send(message) { | ||
this._pipeWrite.write(message); | ||
this._pipeWrite.write(JSON.stringify(message)); | ||
this._pipeWrite.write('\0'); | ||
} | ||
close() { | ||
this._closeCallback(); | ||
throw new Error('unimplemented'); | ||
} | ||
@@ -59,3 +58,3 @@ _dispatch(buffer) { | ||
if (this.onmessage) | ||
this.onmessage.call(null, message); | ||
this.onmessage.call(null, JSON.parse(message)); | ||
}); | ||
@@ -68,3 +67,3 @@ let start = end + 1; | ||
if (this.onmessage) | ||
this.onmessage.call(null, message); | ||
this.onmessage.call(null, JSON.parse(message)); | ||
}); | ||
@@ -71,0 +70,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,17 +20,26 @@ "use strict"; | ||
const childProcess = require("child_process"); | ||
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); | ||
let lastLaunchedId = 0; | ||
const removeFolderAsync = util.promisify(removeFolder); | ||
const mkdtempAsync = util.promisify(fs.mkdtemp); | ||
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); | ||
const browserLog = { | ||
name: 'browser', | ||
}; | ||
const browserStdOutLog = { | ||
name: 'browser:out', | ||
}; | ||
const browserStdErrLog = { | ||
name: 'browser:err', | ||
severity: 'warning' | ||
}; | ||
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}]`); | ||
debugBrowser.color = '33'; | ||
debugBrowserOut.color = '178'; | ||
debugBrowserErr.color = '160'; | ||
const logger = options.logger; | ||
const stdio = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe']; | ||
logger._log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`); | ||
const spawnedProcess = childProcess.spawn(options.executablePath, options.args, { | ||
@@ -44,3 +53,2 @@ // On non-windows platforms, `detached: true` makes child process a leader of a new | ||
}); | ||
debugBrowser(`<launching> ${options.executablePath} ${options.args.join(' ')}`); | ||
if (!spawnedProcess.pid) { | ||
@@ -54,18 +62,16 @@ let reject; | ||
} | ||
logger._log(browserLog, `<launched> pid=${spawnedProcess.pid}`); | ||
const stdout = readline.createInterface({ input: spawnedProcess.stdout }); | ||
stdout.on('line', (data) => { | ||
debugBrowserOut(data); | ||
if (options.dumpio) | ||
console.log(`\x1b[33m[out]\x1b[0m ${data}`); // eslint-disable-line no-console | ||
logger._log(browserStdOutLog, data); | ||
}); | ||
const stderr = readline.createInterface({ input: spawnedProcess.stderr }); | ||
stderr.on('line', (data) => { | ||
debugBrowserErr(data); | ||
if (options.dumpio) | ||
console.log(`\x1b[31m[err]\x1b[0m ${data}`); // eslint-disable-line no-console | ||
logger._log(browserStdErrLog, data); | ||
}); | ||
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER); | ||
let processClosed = false; | ||
const waitForProcessToClose = new Promise((fulfill, reject) => { | ||
spawnedProcess.once('exit', (exitCode, signal) => { | ||
debugBrowser(`<process did exit ${exitCode}, ${signal}>`); | ||
logger._log(browserLog, `<process did exit ${exitCode}, ${signal}>`); | ||
processClosed = true; | ||
@@ -75,10 +81,6 @@ helper_1.helper.removeEventListeners(listeners); | ||
// 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); | ||
}); | ||
@@ -103,3 +105,3 @@ }); | ||
if (gracefullyClosing) { | ||
debugBrowser(`<forecefully close>`); | ||
logger._log(browserLog, `<forecefully close>`); | ||
killProcess(); | ||
@@ -109,10 +111,10 @@ return; | ||
gracefullyClosing = true; | ||
debugBrowser(`<gracefully close start>`); | ||
options.attemptToGracefullyClose().catch(() => killProcess()); | ||
logger._log(browserLog, `<gracefully close start>`); | ||
await options.attemptToGracefullyClose().catch(() => killProcess()); | ||
await waitForProcessToClose; | ||
debugBrowser(`<gracefully close end>`); | ||
logger._log(browserLog, `<gracefully close end>`); | ||
} | ||
// This method has to be sync to be used as 'exit' event handler. | ||
function killProcess() { | ||
debugBrowser(`<kill>`); | ||
logger._log(browserLog, `<kill>`); | ||
helper_1.helper.removeEventListeners(listeners); | ||
@@ -138,3 +140,3 @@ if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) { | ||
} | ||
return { launchedProcess: spawnedProcess, gracefullyClose }; | ||
return { launchedProcess: spawnedProcess, gracefullyClose, downloadsPath }; | ||
} | ||
@@ -141,0 +143,0 @@ exports.launchProcess = launchProcess; |
@@ -24,9 +24,11 @@ "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"); | ||
const browserServer_1 = require("./browserServer"); | ||
const events_1 = require("../events"); | ||
const logger_1 = require("../logger"); | ||
class WebKit { | ||
@@ -41,22 +43,25 @@ 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 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, logger } = await this._launchServer(options, 'local'); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, logger, 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, logger } = await this._launchServer(options, 'persistent', userDataDir); | ||
const browser = await wkBrowser_1.WKBrowser.connect(transport, logger, 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 = [], 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.'); | ||
const logger = new logger_1.RootLogger(options.logger); | ||
let temporaryUserDataDir = null; | ||
@@ -69,5 +74,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 +83,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, | ||
@@ -88,13 +91,11 @@ args: webkitArguments, | ||
handleSIGHUP, | ||
dumpio, | ||
logger, | ||
pipe: true, | ||
tempDir: temporaryUserDataDir || undefined, | ||
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,11 +108,12 @@ 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], logger); | ||
browserServer = new browserServer_1.BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null); | ||
return { browserServer, transport, downloadsPath, logger }; | ||
} | ||
async connect(options) { | ||
return await platform.connectToWebsocket(options.wsEndpoint, transport => { | ||
return wkBrowser_1.WKBrowser.connect(transport, options.slowMo); | ||
return await transport_1.WebSocketTransport.connect(options.wsEndpoint, transport => { | ||
return wkBrowser_1.WKBrowser.connect(transport, new logger_1.RootLogger(options.logger), options.slowMo); | ||
}); | ||
@@ -122,3 +124,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 +143,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) { | ||
function wrapTransportWithWebSocket(transport, logger, 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 +156,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 +198,3 @@ const socket = pageProxyIds.get(pageProxyId); | ||
} | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -224,3 +209,3 @@ } | ||
pageProxyIds.set(params.pageProxyInfo.pageProxyId, socket); | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -232,3 +217,3 @@ } | ||
if (socket && socket.readyState !== ws.CLOSING) | ||
socket.send(message); | ||
socket.send(JSON.stringify(message)); | ||
return; | ||
@@ -239,6 +224,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 +248,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 +255,3 @@ pendingBrowserContextCreations.add(seqNum); | ||
}); | ||
socket.on('error', logger_1.logError(logger)); | ||
socket.on('close', socket.__closeListener = () => { | ||
@@ -269,7 +264,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 +276,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,4 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const WebSocket = require("ws"); | ||
const helper_1 = require("./helper"); | ||
class SlowMoTransport { | ||
@@ -56,3 +58,3 @@ constructor(transport, delay) { | ||
this._readPromise = new Promise(f => callback = f); | ||
this._delegate.onmessage = s => { | ||
this._delegate.onmessage = (s) => { | ||
callback(); | ||
@@ -76,2 +78,90 @@ 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.protocolLog = { | ||
name: 'protocol', | ||
severity: 'verbose', | ||
color: 'green' | ||
}; | ||
//# sourceMappingURL=transport.js.map |
@@ -18,5 +18,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.kLifecycleEvents = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']); | ||
exports.kLifecycleEvents = new Set(['load', 'domcontentloaded', 'networkidle']); | ||
exports.mediaTypes = new Set(['screen', 'print']); | ||
exports.colorSchemes = new Set(['dark', 'light', 'no-preference']); | ||
//# sourceMappingURL=types.js.map |
@@ -20,374 +20,117 @@ "use strict"; | ||
exports.keypadLocation = 3; | ||
exports.keyDefinitions = { | ||
'0': { 'keyCode': 48, 'key': '0', 'code': 'Digit0' }, | ||
'1': { 'keyCode': 49, 'key': '1', 'code': 'Digit1' }, | ||
'2': { 'keyCode': 50, 'key': '2', 'code': 'Digit2' }, | ||
'3': { 'keyCode': 51, 'key': '3', 'code': 'Digit3' }, | ||
'4': { 'keyCode': 52, 'key': '4', 'code': 'Digit4' }, | ||
'5': { 'keyCode': 53, 'key': '5', 'code': 'Digit5' }, | ||
'6': { 'keyCode': 54, 'key': '6', 'code': 'Digit6' }, | ||
'7': { 'keyCode': 55, 'key': '7', 'code': 'Digit7' }, | ||
'8': { 'keyCode': 56, 'key': '8', 'code': 'Digit8' }, | ||
'9': { 'keyCode': 57, 'key': '9', 'code': 'Digit9' }, | ||
'Power': { 'key': 'Power', 'code': 'Power' }, | ||
'Eject': { 'key': 'Eject', 'code': 'Eject' }, | ||
'Abort': { 'keyCode': 3, 'code': 'Abort', 'key': 'Cancel' }, | ||
'Help': { 'keyCode': 6, 'code': 'Help', 'key': 'Help' }, | ||
'Backspace': { 'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace' }, | ||
'Tab': { 'keyCode': 9, 'code': 'Tab', 'key': 'Tab' }, | ||
'Numpad5': { 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3 }, | ||
'NumpadEnter': { 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3 }, | ||
'Enter': { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }, | ||
'\r': { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }, | ||
'\n': { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }, | ||
'ShiftLeft': { 'keyCode': 160, 'keyCodeWithoutLocation': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1, 'windowsVirtualKeyCode': 160 }, | ||
'ShiftRight': { 'keyCode': 161, 'keyCodeWithoutLocation': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2, 'windowsVirtualKeyCode': 161 }, | ||
'ControlLeft': { 'keyCode': 162, 'keyCodeWithoutLocation': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1, 'windowsVirtualKeyCode': 162 }, | ||
'ControlRight': { 'keyCode': 163, 'keyCodeWithoutLocation': 17, 'code': 'ControlRight', 'key': 'Control', 'location': 2, 'windowsVirtualKeyCode': 163 }, | ||
'AltLeft': { 'keyCode': 164, 'keyCodeWithoutLocation': 18, 'code': 'AltLeft', 'key': 'Alt', 'location': 1, 'windowsVirtualKeyCode': 164 }, | ||
'AltRight': { 'keyCode': 165, 'keyCodeWithoutLocation': 18, 'code': 'AltRight', 'key': 'Alt', 'location': 2, 'windowsVirtualKeyCode': 165 }, | ||
'Pause': { 'keyCode': 19, 'code': 'Pause', 'key': 'Pause' }, | ||
'CapsLock': { 'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock' }, | ||
'Escape': { 'keyCode': 27, 'code': 'Escape', 'key': 'Escape' }, | ||
'Convert': { 'keyCode': 28, 'code': 'Convert', 'key': 'Convert' }, | ||
'NonConvert': { 'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert' }, | ||
'Space': { 'keyCode': 32, 'code': 'Space', 'key': ' ' }, | ||
'Numpad9': { 'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3 }, | ||
'PageUp': { 'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp' }, | ||
'Numpad3': { 'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3 }, | ||
'PageDown': { 'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown' }, | ||
'End': { 'keyCode': 35, 'code': 'End', 'key': 'End' }, | ||
'Numpad1': { 'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3 }, | ||
'Home': { 'keyCode': 36, 'code': 'Home', 'key': 'Home' }, | ||
'Numpad7': { 'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3 }, | ||
'ArrowLeft': { 'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft' }, | ||
'Numpad4': { 'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3 }, | ||
'Numpad8': { 'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3 }, | ||
'ArrowUp': { 'keyCode': 38, 'code': 'ArrowUp', 'key': 'ArrowUp' }, | ||
'ArrowRight': { 'keyCode': 39, 'code': 'ArrowRight', 'key': 'ArrowRight' }, | ||
'Numpad6': { 'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3 }, | ||
'Numpad2': { 'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3 }, | ||
'ArrowDown': { 'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown' }, | ||
'Select': { 'keyCode': 41, 'code': 'Select', 'key': 'Select' }, | ||
'Open': { 'keyCode': 43, 'code': 'Open', 'key': 'Execute' }, | ||
'PrintScreen': { 'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen' }, | ||
'Insert': { 'keyCode': 45, 'code': 'Insert', 'key': 'Insert' }, | ||
'Numpad0': { 'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3 }, | ||
'Delete': { 'keyCode': 46, 'code': 'Delete', 'key': 'Delete' }, | ||
'NumpadDecimal': { 'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': '\u0000', 'shiftKey': '.', 'location': 3 }, | ||
'Digit0': { 'keyCode': 48, 'code': 'Digit0', 'shiftKey': ')', 'key': '0' }, | ||
'Digit1': { 'keyCode': 49, 'code': 'Digit1', 'shiftKey': '!', 'key': '1' }, | ||
'Digit2': { 'keyCode': 50, 'code': 'Digit2', 'shiftKey': '@', 'key': '2' }, | ||
'Digit3': { 'keyCode': 51, 'code': 'Digit3', 'shiftKey': '#', 'key': '3' }, | ||
'Digit4': { 'keyCode': 52, 'code': 'Digit4', 'shiftKey': '$', 'key': '4' }, | ||
'Digit5': { 'keyCode': 53, 'code': 'Digit5', 'shiftKey': '%', 'key': '5' }, | ||
'Digit6': { 'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6' }, | ||
'Digit7': { 'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7' }, | ||
'Digit8': { 'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8' }, | ||
'Digit9': { 'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9' }, | ||
'KeyA': { 'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a' }, | ||
'KeyB': { 'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b' }, | ||
'KeyC': { 'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c' }, | ||
'KeyD': { 'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd' }, | ||
'KeyE': { 'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e' }, | ||
'KeyF': { 'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f' }, | ||
'KeyG': { 'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g' }, | ||
'KeyH': { 'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h' }, | ||
'KeyI': { 'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i' }, | ||
'KeyJ': { 'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j' }, | ||
'KeyK': { 'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k' }, | ||
'KeyL': { 'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l' }, | ||
'KeyM': { 'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm' }, | ||
'KeyN': { 'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n' }, | ||
'KeyO': { 'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o' }, | ||
'KeyP': { 'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p' }, | ||
'KeyQ': { 'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q' }, | ||
'KeyR': { 'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r' }, | ||
'KeyS': { 'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's' }, | ||
'KeyT': { 'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't' }, | ||
'KeyU': { 'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u' }, | ||
'KeyV': { 'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v' }, | ||
'KeyW': { 'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w' }, | ||
'KeyX': { 'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x' }, | ||
'KeyY': { 'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y' }, | ||
'KeyZ': { 'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z' }, | ||
'MetaLeft': { 'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta', 'location': 1 }, | ||
'MetaRight': { 'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta', 'location': 2 }, | ||
'ContextMenu': { 'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu' }, | ||
'NumpadMultiply': { 'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3 }, | ||
'NumpadAdd': { 'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3 }, | ||
'NumpadSubtract': { 'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3 }, | ||
'NumpadDivide': { 'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3 }, | ||
'F1': { 'keyCode': 112, 'code': 'F1', 'key': 'F1' }, | ||
'F2': { 'keyCode': 113, 'code': 'F2', 'key': 'F2' }, | ||
'F3': { 'keyCode': 114, 'code': 'F3', 'key': 'F3' }, | ||
'F4': { 'keyCode': 115, 'code': 'F4', 'key': 'F4' }, | ||
'F5': { 'keyCode': 116, 'code': 'F5', 'key': 'F5' }, | ||
'F6': { 'keyCode': 117, 'code': 'F6', 'key': 'F6' }, | ||
'F7': { 'keyCode': 118, 'code': 'F7', 'key': 'F7' }, | ||
'F8': { 'keyCode': 119, 'code': 'F8', 'key': 'F8' }, | ||
'F9': { 'keyCode': 120, 'code': 'F9', 'key': 'F9' }, | ||
'F10': { 'keyCode': 121, 'code': 'F10', 'key': 'F10' }, | ||
'F11': { 'keyCode': 122, 'code': 'F11', 'key': 'F11' }, | ||
'F12': { 'keyCode': 123, 'code': 'F12', 'key': 'F12' }, | ||
'F13': { 'keyCode': 124, 'code': 'F13', 'key': 'F13' }, | ||
'F14': { 'keyCode': 125, 'code': 'F14', 'key': 'F14' }, | ||
'F15': { 'keyCode': 126, 'code': 'F15', 'key': 'F15' }, | ||
'F16': { 'keyCode': 127, 'code': 'F16', 'key': 'F16' }, | ||
'F17': { 'keyCode': 128, 'code': 'F17', 'key': 'F17' }, | ||
'F18': { 'keyCode': 129, 'code': 'F18', 'key': 'F18' }, | ||
'F19': { 'keyCode': 130, 'code': 'F19', 'key': 'F19' }, | ||
'F20': { 'keyCode': 131, 'code': 'F20', 'key': 'F20' }, | ||
'F21': { 'keyCode': 132, 'code': 'F21', 'key': 'F21' }, | ||
'F22': { 'keyCode': 133, 'code': 'F22', 'key': 'F22' }, | ||
'F23': { 'keyCode': 134, 'code': 'F23', 'key': 'F23' }, | ||
'F24': { 'keyCode': 135, 'code': 'F24', 'key': 'F24' }, | ||
'NumLock': { 'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock' }, | ||
'ScrollLock': { 'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock' }, | ||
'AudioVolumeMute': { 'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute' }, | ||
'AudioVolumeDown': { 'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown' }, | ||
'AudioVolumeUp': { 'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp' }, | ||
'MediaTrackNext': { 'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext' }, | ||
'MediaTrackPrevious': { 'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious' }, | ||
'MediaStop': { 'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop' }, | ||
'MediaPlayPause': { 'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause' }, | ||
'Semicolon': { 'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';' }, | ||
'Equal': { 'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '=' }, | ||
'NumpadEqual': { 'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3 }, | ||
'Comma': { 'keyCode': 188, 'code': 'Comma', 'shiftKey': '\<', 'key': ',' }, | ||
'Minus': { 'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-' }, | ||
'Period': { 'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.' }, | ||
'Slash': { 'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/' }, | ||
'Backquote': { 'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`' }, | ||
'BracketLeft': { 'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '[' }, | ||
'Backslash': { 'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\' }, | ||
'BracketRight': { 'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']' }, | ||
'Quote': { 'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\'' }, | ||
'AltGraph': { 'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph' }, | ||
'Props': { 'keyCode': 247, 'code': 'Props', 'key': 'CrSel' }, | ||
'Cancel': { 'keyCode': 3, 'key': 'Cancel', 'code': 'Abort' }, | ||
'Clear': { 'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3 }, | ||
'Shift': { 'keyCode': 160, 'keyCodeWithoutLocation': 16, 'key': 'Shift', 'code': 'ShiftLeft', 'location': 1, 'windowsVirtualKeyCode': 160 }, | ||
'Control': { 'keyCode': 162, 'keyCodeWithoutLocation': 17, 'key': 'Control', 'code': 'ControlLeft', 'location': 1, 'windowsVirtualKeyCode': 162 }, | ||
'Alt': { 'keyCode': 164, 'keyCodeWithoutLocation': 18, 'key': 'Alt', 'code': 'AltLeft', 'location': 1, 'windowsVirtualKeyCode': 164 }, | ||
'Accept': { 'keyCode': 30, 'key': 'Accept' }, | ||
'ModeChange': { 'keyCode': 31, 'key': 'ModeChange' }, | ||
' ': { 'keyCode': 32, 'key': ' ', 'code': 'Space' }, | ||
'Print': { 'keyCode': 42, 'key': 'Print' }, | ||
'Execute': { 'keyCode': 43, 'key': 'Execute', 'code': 'Open' }, | ||
'\u0000': { 'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3 }, | ||
'a': { 'keyCode': 65, 'key': 'a', 'code': 'KeyA' }, | ||
'b': { 'keyCode': 66, 'key': 'b', 'code': 'KeyB' }, | ||
'c': { 'keyCode': 67, 'key': 'c', 'code': 'KeyC' }, | ||
'd': { 'keyCode': 68, 'key': 'd', 'code': 'KeyD' }, | ||
'e': { 'keyCode': 69, 'key': 'e', 'code': 'KeyE' }, | ||
'f': { 'keyCode': 70, 'key': 'f', 'code': 'KeyF' }, | ||
'g': { 'keyCode': 71, 'key': 'g', 'code': 'KeyG' }, | ||
'h': { 'keyCode': 72, 'key': 'h', 'code': 'KeyH' }, | ||
'i': { 'keyCode': 73, 'key': 'i', 'code': 'KeyI' }, | ||
'j': { 'keyCode': 74, 'key': 'j', 'code': 'KeyJ' }, | ||
'k': { 'keyCode': 75, 'key': 'k', 'code': 'KeyK' }, | ||
'l': { 'keyCode': 76, 'key': 'l', 'code': 'KeyL' }, | ||
'm': { 'keyCode': 77, 'key': 'm', 'code': 'KeyM' }, | ||
'n': { 'keyCode': 78, 'key': 'n', 'code': 'KeyN' }, | ||
'o': { 'keyCode': 79, 'key': 'o', 'code': 'KeyO' }, | ||
'p': { 'keyCode': 80, 'key': 'p', 'code': 'KeyP' }, | ||
'q': { 'keyCode': 81, 'key': 'q', 'code': 'KeyQ' }, | ||
'r': { 'keyCode': 82, 'key': 'r', 'code': 'KeyR' }, | ||
's': { 'keyCode': 83, 'key': 's', 'code': 'KeyS' }, | ||
't': { 'keyCode': 84, 'key': 't', 'code': 'KeyT' }, | ||
'u': { 'keyCode': 85, 'key': 'u', 'code': 'KeyU' }, | ||
'v': { 'keyCode': 86, 'key': 'v', 'code': 'KeyV' }, | ||
'w': { 'keyCode': 87, 'key': 'w', 'code': 'KeyW' }, | ||
'x': { 'keyCode': 88, 'key': 'x', 'code': 'KeyX' }, | ||
'y': { 'keyCode': 89, 'key': 'y', 'code': 'KeyY' }, | ||
'z': { 'keyCode': 90, 'key': 'z', 'code': 'KeyZ' }, | ||
'Meta': { 'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft', 'location': 1 }, | ||
'*': { 'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3 }, | ||
'+': { 'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3 }, | ||
'-': { 'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3 }, | ||
'/': { 'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3 }, | ||
';': { 'keyCode': 186, 'key': ';', 'code': 'Semicolon' }, | ||
'=': { 'keyCode': 187, 'key': '=', 'code': 'Equal' }, | ||
',': { 'keyCode': 188, 'key': ',', 'code': 'Comma' }, | ||
'.': { 'keyCode': 190, 'key': '.', 'code': 'Period' }, | ||
'`': { 'keyCode': 192, 'key': '`', 'code': 'Backquote' }, | ||
'[': { 'keyCode': 219, 'key': '[', 'code': 'BracketLeft' }, | ||
'\\': { 'keyCode': 220, 'key': '\\', 'code': 'Backslash' }, | ||
']': { 'keyCode': 221, 'key': ']', 'code': 'BracketRight' }, | ||
'\'': { 'keyCode': 222, 'key': '\'', 'code': 'Quote' }, | ||
'Attn': { 'keyCode': 246, 'key': 'Attn' }, | ||
'CrSel': { 'keyCode': 247, 'key': 'CrSel', 'code': 'Props' }, | ||
'ExSel': { 'keyCode': 248, 'key': 'ExSel' }, | ||
'EraseEof': { 'keyCode': 249, 'key': 'EraseEof' }, | ||
'Play': { 'keyCode': 250, 'key': 'Play' }, | ||
'ZoomOut': { 'keyCode': 251, 'key': 'ZoomOut' }, | ||
')': { 'keyCode': 48, 'key': ')', 'code': 'Digit0' }, | ||
'!': { 'keyCode': 49, 'key': '!', 'code': 'Digit1' }, | ||
'@': { 'keyCode': 50, 'key': '@', 'code': 'Digit2' }, | ||
'#': { 'keyCode': 51, 'key': '#', 'code': 'Digit3' }, | ||
'$': { 'keyCode': 52, 'key': '$', 'code': 'Digit4' }, | ||
'%': { 'keyCode': 53, 'key': '%', 'code': 'Digit5' }, | ||
'^': { 'keyCode': 54, 'key': '^', 'code': 'Digit6' }, | ||
'&': { 'keyCode': 55, 'key': '&', 'code': 'Digit7' }, | ||
'(': { 'keyCode': 57, 'key': '\(', 'code': 'Digit9' }, | ||
'A': { 'keyCode': 65, 'key': 'A', 'code': 'KeyA' }, | ||
'B': { 'keyCode': 66, 'key': 'B', 'code': 'KeyB' }, | ||
'C': { 'keyCode': 67, 'key': 'C', 'code': 'KeyC' }, | ||
'D': { 'keyCode': 68, 'key': 'D', 'code': 'KeyD' }, | ||
'E': { 'keyCode': 69, 'key': 'E', 'code': 'KeyE' }, | ||
'F': { 'keyCode': 70, 'key': 'F', 'code': 'KeyF' }, | ||
'G': { 'keyCode': 71, 'key': 'G', 'code': 'KeyG' }, | ||
'H': { 'keyCode': 72, 'key': 'H', 'code': 'KeyH' }, | ||
'I': { 'keyCode': 73, 'key': 'I', 'code': 'KeyI' }, | ||
'J': { 'keyCode': 74, 'key': 'J', 'code': 'KeyJ' }, | ||
'K': { 'keyCode': 75, 'key': 'K', 'code': 'KeyK' }, | ||
'L': { 'keyCode': 76, 'key': 'L', 'code': 'KeyL' }, | ||
'M': { 'keyCode': 77, 'key': 'M', 'code': 'KeyM' }, | ||
'N': { 'keyCode': 78, 'key': 'N', 'code': 'KeyN' }, | ||
'O': { 'keyCode': 79, 'key': 'O', 'code': 'KeyO' }, | ||
'P': { 'keyCode': 80, 'key': 'P', 'code': 'KeyP' }, | ||
'Q': { 'keyCode': 81, 'key': 'Q', 'code': 'KeyQ' }, | ||
'R': { 'keyCode': 82, 'key': 'R', 'code': 'KeyR' }, | ||
'S': { 'keyCode': 83, 'key': 'S', 'code': 'KeyS' }, | ||
'T': { 'keyCode': 84, 'key': 'T', 'code': 'KeyT' }, | ||
'U': { 'keyCode': 85, 'key': 'U', 'code': 'KeyU' }, | ||
'V': { 'keyCode': 86, 'key': 'V', 'code': 'KeyV' }, | ||
'W': { 'keyCode': 87, 'key': 'W', 'code': 'KeyW' }, | ||
'X': { 'keyCode': 88, 'key': 'X', 'code': 'KeyX' }, | ||
'Y': { 'keyCode': 89, 'key': 'Y', 'code': 'KeyY' }, | ||
'Z': { 'keyCode': 90, 'key': 'Z', 'code': 'KeyZ' }, | ||
':': { 'keyCode': 186, 'key': ':', 'code': 'Semicolon' }, | ||
'<': { 'keyCode': 188, 'key': '\<', 'code': 'Comma' }, | ||
'_': { 'keyCode': 189, 'key': '_', 'code': 'Minus' }, | ||
'>': { 'keyCode': 190, 'key': '>', 'code': 'Period' }, | ||
'?': { 'keyCode': 191, 'key': '?', 'code': 'Slash' }, | ||
'~': { 'keyCode': 192, 'key': '~', 'code': 'Backquote' }, | ||
'{': { 'keyCode': 219, 'key': '{', 'code': 'BracketLeft' }, | ||
'|': { 'keyCode': 220, 'key': '|', 'code': 'Backslash' }, | ||
'}': { 'keyCode': 221, 'key': '}', 'code': 'BracketRight' }, | ||
'"': { 'keyCode': 222, 'key': '"', 'code': 'Quote' }, | ||
'SoftLeft': { 'key': 'SoftLeft', 'code': 'SoftLeft', 'location': 4 }, | ||
'SoftRight': { 'key': 'SoftRight', 'code': 'SoftRight', 'location': 4 }, | ||
'Camera': { 'keyCode': 44, 'key': 'Camera', 'code': 'Camera', 'location': 4 }, | ||
'Call': { 'key': 'Call', 'code': 'Call', 'location': 4 }, | ||
'EndCall': { 'keyCode': 95, 'key': 'EndCall', 'code': 'EndCall', 'location': 4 }, | ||
'VolumeDown': { 'keyCode': 182, 'key': 'VolumeDown', 'code': 'VolumeDown', 'location': 4 }, | ||
'VolumeUp': { 'keyCode': 183, 'key': 'VolumeUp', 'code': 'VolumeUp', 'location': 4 }, | ||
exports.USKeyboardLayout = { | ||
// Functions row | ||
'Escape': { 'keyCode': 27, 'key': 'Escape' }, | ||
'F1': { 'keyCode': 112, 'key': 'F1' }, | ||
'F2': { 'keyCode': 113, 'key': 'F2' }, | ||
'F3': { 'keyCode': 114, 'key': 'F3' }, | ||
'F4': { 'keyCode': 115, 'key': 'F4' }, | ||
'F5': { 'keyCode': 116, 'key': 'F5' }, | ||
'F6': { 'keyCode': 117, 'key': 'F6' }, | ||
'F7': { 'keyCode': 118, 'key': 'F7' }, | ||
'F8': { 'keyCode': 119, 'key': 'F8' }, | ||
'F9': { 'keyCode': 120, 'key': 'F9' }, | ||
'F10': { 'keyCode': 121, 'key': 'F10' }, | ||
'F11': { 'keyCode': 122, 'key': 'F11' }, | ||
'F12': { 'keyCode': 123, 'key': 'F12' }, | ||
// Numbers row | ||
'Backquote': { 'keyCode': 192, 'shiftKey': '~', 'key': '`' }, | ||
'Digit1': { 'keyCode': 49, 'shiftKey': '!', 'key': '1' }, | ||
'Digit2': { 'keyCode': 50, 'shiftKey': '@', 'key': '2' }, | ||
'Digit3': { 'keyCode': 51, 'shiftKey': '#', 'key': '3' }, | ||
'Digit4': { 'keyCode': 52, 'shiftKey': '$', 'key': '4' }, | ||
'Digit5': { 'keyCode': 53, 'shiftKey': '%', 'key': '5' }, | ||
'Digit6': { 'keyCode': 54, 'shiftKey': '^', 'key': '6' }, | ||
'Digit7': { 'keyCode': 55, 'shiftKey': '&', 'key': '7' }, | ||
'Digit8': { 'keyCode': 56, 'shiftKey': '*', 'key': '8' }, | ||
'Digit9': { 'keyCode': 57, 'shiftKey': '\(', 'key': '9' }, | ||
'Digit0': { 'keyCode': 48, 'shiftKey': ')', 'key': '0' }, | ||
'Minus': { 'keyCode': 189, 'shiftKey': '_', 'key': '-' }, | ||
'Equal': { 'keyCode': 187, 'shiftKey': '+', 'key': '=' }, | ||
'Backslash': { 'keyCode': 220, 'shiftKey': '|', 'key': '\\' }, | ||
'Backspace': { 'keyCode': 8, 'key': 'Backspace' }, | ||
// First row | ||
'Tab': { 'keyCode': 9, 'key': 'Tab' }, | ||
'KeyQ': { 'keyCode': 81, 'shiftKey': 'Q', 'key': 'q' }, | ||
'KeyW': { 'keyCode': 87, 'shiftKey': 'W', 'key': 'w' }, | ||
'KeyE': { 'keyCode': 69, 'shiftKey': 'E', 'key': 'e' }, | ||
'KeyR': { 'keyCode': 82, 'shiftKey': 'R', 'key': 'r' }, | ||
'KeyT': { 'keyCode': 84, 'shiftKey': 'T', 'key': 't' }, | ||
'KeyY': { 'keyCode': 89, 'shiftKey': 'Y', 'key': 'y' }, | ||
'KeyU': { 'keyCode': 85, 'shiftKey': 'U', 'key': 'u' }, | ||
'KeyI': { 'keyCode': 73, 'shiftKey': 'I', 'key': 'i' }, | ||
'KeyO': { 'keyCode': 79, 'shiftKey': 'O', 'key': 'o' }, | ||
'KeyP': { 'keyCode': 80, 'shiftKey': 'P', 'key': 'p' }, | ||
'BracketLeft': { 'keyCode': 219, 'shiftKey': '{', 'key': '[' }, | ||
'BracketRight': { 'keyCode': 221, 'shiftKey': '}', 'key': ']' }, | ||
// Second row | ||
'CapsLock': { 'keyCode': 20, 'key': 'CapsLock' }, | ||
'KeyA': { 'keyCode': 65, 'shiftKey': 'A', 'key': 'a' }, | ||
'KeyS': { 'keyCode': 83, 'shiftKey': 'S', 'key': 's' }, | ||
'KeyD': { 'keyCode': 68, 'shiftKey': 'D', 'key': 'd' }, | ||
'KeyF': { 'keyCode': 70, 'shiftKey': 'F', 'key': 'f' }, | ||
'KeyG': { 'keyCode': 71, 'shiftKey': 'G', 'key': 'g' }, | ||
'KeyH': { 'keyCode': 72, 'shiftKey': 'H', 'key': 'h' }, | ||
'KeyJ': { 'keyCode': 74, 'shiftKey': 'J', 'key': 'j' }, | ||
'KeyK': { 'keyCode': 75, 'shiftKey': 'K', 'key': 'k' }, | ||
'KeyL': { 'keyCode': 76, 'shiftKey': 'L', 'key': 'l' }, | ||
'Semicolon': { 'keyCode': 186, 'shiftKey': ':', 'key': ';' }, | ||
'Quote': { 'keyCode': 222, 'shiftKey': '"', 'key': '\'' }, | ||
'Enter': { 'keyCode': 13, 'key': 'Enter', 'text': '\r' }, | ||
// Third row | ||
'ShiftLeft': { 'keyCode': 160, 'keyCodeWithoutLocation': 16, 'key': 'Shift', 'location': 1 }, | ||
'KeyZ': { 'keyCode': 90, 'shiftKey': 'Z', 'key': 'z' }, | ||
'KeyX': { 'keyCode': 88, 'shiftKey': 'X', 'key': 'x' }, | ||
'KeyC': { 'keyCode': 67, 'shiftKey': 'C', 'key': 'c' }, | ||
'KeyV': { 'keyCode': 86, 'shiftKey': 'V', 'key': 'v' }, | ||
'KeyB': { 'keyCode': 66, 'shiftKey': 'B', 'key': 'b' }, | ||
'KeyN': { 'keyCode': 78, 'shiftKey': 'N', 'key': 'n' }, | ||
'KeyM': { 'keyCode': 77, 'shiftKey': 'M', 'key': 'm' }, | ||
'Comma': { 'keyCode': 188, 'shiftKey': '\<', 'key': ',' }, | ||
'Period': { 'keyCode': 190, 'shiftKey': '>', 'key': '.' }, | ||
'Slash': { 'keyCode': 191, 'shiftKey': '?', 'key': '/' }, | ||
'ShiftRight': { 'keyCode': 161, 'keyCodeWithoutLocation': 16, 'key': 'Shift', 'location': 2 }, | ||
// Last row | ||
'ControlLeft': { 'keyCode': 162, 'keyCodeWithoutLocation': 17, 'key': 'Control', 'location': 1 }, | ||
'MetaLeft': { 'keyCode': 91, 'key': 'Meta', 'location': 1 }, | ||
'AltLeft': { 'keyCode': 164, 'keyCodeWithoutLocation': 18, 'key': 'Alt', 'location': 1 }, | ||
'Space': { 'keyCode': 32, 'key': ' ' }, | ||
'AltRight': { 'keyCode': 165, 'keyCodeWithoutLocation': 18, 'key': 'Alt', 'location': 2 }, | ||
'AltGraph': { 'keyCode': 225, 'key': 'AltGraph' }, | ||
'MetaRight': { 'keyCode': 92, 'key': 'Meta', 'location': 2 }, | ||
'ContextMenu': { 'keyCode': 93, 'key': 'ContextMenu' }, | ||
'ControlRight': { 'keyCode': 163, 'keyCodeWithoutLocation': 17, 'key': 'Control', 'location': 2 }, | ||
// Center block | ||
'PrintScreen': { 'keyCode': 44, 'key': 'PrintScreen' }, | ||
'ScrollLock': { 'keyCode': 145, 'key': 'ScrollLock' }, | ||
'Pause': { 'keyCode': 19, 'key': 'Pause' }, | ||
'PageUp': { 'keyCode': 33, 'key': 'PageUp' }, | ||
'PageDown': { 'keyCode': 34, 'key': 'PageDown' }, | ||
'Insert': { 'keyCode': 45, 'key': 'Insert' }, | ||
'Delete': { 'keyCode': 46, 'key': 'Delete' }, | ||
'Home': { 'keyCode': 36, 'key': 'Home' }, | ||
'End': { 'keyCode': 35, 'key': 'End' }, | ||
'ArrowLeft': { 'keyCode': 37, 'key': 'ArrowLeft' }, | ||
'ArrowUp': { 'keyCode': 38, 'key': 'ArrowUp' }, | ||
'ArrowRight': { 'keyCode': 39, 'key': 'ArrowRight' }, | ||
'ArrowDown': { 'keyCode': 40, 'key': 'ArrowDown' }, | ||
// Numpad | ||
'NumLock': { 'keyCode': 144, 'key': 'NumLock' }, | ||
'NumpadDivide': { 'keyCode': 111, 'key': '/', 'location': 3 }, | ||
'NumpadMultiply': { 'keyCode': 106, 'key': '*', 'location': 3 }, | ||
'NumpadSubtract': { 'keyCode': 109, 'key': '-', 'location': 3 }, | ||
'Numpad7': { 'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'shiftKey': '7', 'location': 3 }, | ||
'Numpad8': { 'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'shiftKey': '8', 'location': 3 }, | ||
'Numpad9': { 'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'shiftKey': '9', 'location': 3 }, | ||
'Numpad4': { 'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'shiftKey': '4', 'location': 3 }, | ||
'Numpad5': { 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'shiftKey': '5', 'location': 3 }, | ||
'Numpad6': { 'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'shiftKey': '6', 'location': 3 }, | ||
'NumpadAdd': { 'keyCode': 107, 'key': '+', 'location': 3 }, | ||
'Numpad1': { 'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'shiftKey': '1', 'location': 3 }, | ||
'Numpad2': { 'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'shiftKey': '2', 'location': 3 }, | ||
'Numpad3': { 'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'shiftKey': '3', 'location': 3 }, | ||
'Numpad0': { 'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'shiftKey': '0', 'location': 3 }, | ||
'NumpadDecimal': { 'keyCode': 46, 'shiftKeyCode': 110, 'key': '\u0000', 'shiftKey': '.', 'location': 3 }, | ||
'NumpadEnter': { 'keyCode': 13, 'key': 'Enter', 'text': '\r', 'location': 3 }, | ||
}; | ||
exports.macEditingCommands = { | ||
'Backspace': 'deleteBackward:', | ||
'Tab': 'insertTab:', | ||
'Enter': 'insertNewline:', | ||
'NumpadEnter': 'insertNewline:', | ||
'Escape': 'cancelOperation:', | ||
'ArrowUp': 'moveUp:', | ||
'ArrowDown': 'moveDown:', | ||
'ArrowLeft': 'moveLeft:', | ||
'ArrowRight': 'moveRight:', | ||
'F5': 'complete:', | ||
'Delete': 'deleteForward:', | ||
'Home': 'scrollToBeginningOfDocument:', | ||
'End': 'scrollToEndOfDocument:', | ||
'PageUp': 'scrollPageUp:', | ||
'PageDown': 'scrollPageDown:', | ||
'Shift+Backspace': 'deleteBackward:', | ||
'Shift+Enter': 'insertNewline:', | ||
'Shift+NumpadEnter': 'insertNewline:', | ||
'Shift+Tab': 'insertBacktab:', | ||
'Shift+Escape': 'cancelOperation:', | ||
'Shift+ArrowUp': 'moveUpAndModifySelection:', | ||
'Shift+ArrowDown': 'moveDownAndModifySelection:', | ||
'Shift+ArrowLeft': 'moveLeftAndModifySelection:', | ||
'Shift+ArrowRight': 'moveRightAndModifySelection:', | ||
'Shift+F5': 'complete:', | ||
'Shift+Delete': 'deleteForward:', | ||
'Shift+Home': 'moveToBeginningOfDocumentAndModifySelection:', | ||
'Shift+End': 'moveToEndOfDocumentAndModifySelection:', | ||
'Shift+PageUp': 'pageUpAndModifySelection:', | ||
'Shift+PageDown': 'pageDownAndModifySelection:', | ||
'Shift+Numpad5': 'delete:', | ||
'Control+Tab': 'selectNextKeyView:', | ||
'Control+Enter': 'insertLineBreak:', | ||
'Control+NumpadEnter': 'insertLineBreak:', | ||
'Control+Quote': 'insertSingleQuoteIgnoringSubstitution:', | ||
'Control+KeyA': 'moveToBeginningOfParagraph:', | ||
'Control+KeyB': 'moveBackward:', | ||
'Control+KeyD': 'deleteForward:', | ||
'Control+KeyE': 'moveToEndOfParagraph:', | ||
'Control+KeyF': 'moveForward:', | ||
'Control+KeyH': 'deleteBackward:', | ||
'Control+KeyK': 'deleteToEndOfParagraph:', | ||
'Control+KeyL': 'centerSelectionInVisibleArea:', | ||
'Control+KeyN': 'moveDown:', | ||
'Control+KeyO': ['insertNewlineIgnoringFieldEditor:', 'moveBackward:'], | ||
'Control+KeyP': 'moveUp:', | ||
'Control+KeyT': 'transpose:', | ||
'Control+KeyV': 'pageDown:', | ||
'Control+KeyY': 'yank:', | ||
'Control+Backspace': 'deleteBackwardByDecomposingPreviousCharacter:', | ||
'Control+ArrowUp': 'scrollPageUp:', | ||
'Control+ArrowDown': 'scrollPageDown:', | ||
'Control+ArrowLeft': 'moveToLeftEndOfLine:', | ||
'Control+ArrowRight': 'moveToRightEndOfLine:', | ||
'Shift+Control+Enter': 'insertLineBreak:', | ||
'Shift+Control+NumpadEnter': 'insertLineBreak:', | ||
'Shift+Control+Tab': 'selectPreviousKeyView:', | ||
'Shift+Control+Quote': 'insertDoubleQuoteIgnoringSubstitution:', | ||
'Shift+Control+KeyA': 'moveToBeginningOfParagraphAndModifySelection:', | ||
'Shift+Control+KeyB': 'moveBackwardAndModifySelection:', | ||
'Shift+Control+KeyE': 'moveToEndOfParagraphAndModifySelection:', | ||
'Shift+Control+KeyF': 'moveForwardAndModifySelection:', | ||
'Shift+Control+KeyN': 'moveDownAndModifySelection:', | ||
'Shift+Control+KeyP': 'moveUpAndModifySelection:', | ||
'Shift+Control+KeyV': 'pageDownAndModifySelection:', | ||
'Shift+Control+Backspace': 'deleteBackwardByDecomposingPreviousCharacter:', | ||
'Shift+Control+ArrowUp': 'scrollPageUp:', | ||
'Shift+Control+ArrowDown': 'scrollPageDown:', | ||
'Shift+Control+ArrowLeft': 'moveToLeftEndOfLineAndModifySelection:', | ||
'Shift+Control+ArrowRight': 'moveToRightEndOfLineAndModifySelection:', | ||
'Alt+Backspace': 'deleteWordBackward:', | ||
'Alt+Tab': 'insertTabIgnoringFieldEditor:', | ||
'Alt+Enter': 'insertNewlineIgnoringFieldEditor:', | ||
'Alt+NumpadEnter': 'insertNewlineIgnoringFieldEditor:', | ||
'Alt+Escape': 'complete:', | ||
'Alt+ArrowUp': ['moveBackward:', 'moveToBeginningOfParagraph:'], | ||
'Alt+ArrowDown': ['moveForward:', 'moveToEndOfParagraph:'], | ||
'Alt+ArrowLeft': 'moveWordLeft:', | ||
'Alt+ArrowRight': 'moveWordRight:', | ||
'Alt+Delete': 'deleteWordForward:', | ||
'Alt+PageUp': 'pageUp:', | ||
'Alt+PageDown': 'pageDown:', | ||
'Shift+Alt+Backspace': 'deleteWordBackward:', | ||
'Shift+Alt+Tab': 'insertTabIgnoringFieldEditor:', | ||
'Shift+Alt+Enter': 'insertNewlineIgnoringFieldEditor:', | ||
'Shift+Alt+NumpadEnter': 'insertNewlineIgnoringFieldEditor:', | ||
'Shift+Alt+Escape': 'complete:', | ||
'Shift+Alt+ArrowUp': 'moveParagraphBackwardAndModifySelection:', | ||
'Shift+Alt+ArrowDown': 'moveParagraphForwardAndModifySelection:', | ||
'Shift+Alt+ArrowLeft': 'moveWordLeftAndModifySelection:', | ||
'Shift+Alt+ArrowRight': 'moveWordRightAndModifySelection:', | ||
'Shift+Alt+Delete': 'deleteWordForward:', | ||
'Shift+Alt+PageUp': 'pageUp:', | ||
'Shift+Alt+PageDown': 'pageDown:', | ||
'Control+Alt+KeyB': 'moveWordBackward:', | ||
'Control+Alt+KeyF': 'moveWordForward:', | ||
'Control+Alt+Backspace': 'deleteWordBackward:', | ||
'Shift+Control+Alt+KeyB': 'moveWordBackwardAndModifySelection:', | ||
'Shift+Control+Alt+KeyF': 'moveWordForwardAndModifySelection:', | ||
'Shift+Control+Alt+Backspace': 'deleteWordBackward:', | ||
'Meta+NumpadSubtract': 'cancel:', | ||
'Meta+Backspace': 'deleteToBeginningOfLine:', | ||
'Meta+ArrowUp': 'moveToBeginningOfDocument:', | ||
'Meta+ArrowDown': 'moveToEndOfDocument:', | ||
'Meta+ArrowLeft': 'moveToLeftEndOfLine:', | ||
'Meta+ArrowRight': 'moveToRightEndOfLine:', | ||
'Shift+Meta+NumpadSubtract': 'cancel:', | ||
'Shift+Meta+Backspace': 'deleteToBeginningOfLine:', | ||
'Shift+Meta+ArrowUp': 'moveToBeginningOfDocumentAndModifySelection:', | ||
'Shift+Meta+ArrowDown': 'moveToEndOfDocumentAndModifySelection:', | ||
'Shift+Meta+ArrowLeft': 'moveToLeftEndOfLineAndModifySelection:', | ||
'Shift+Meta+ArrowRight': 'moveToRightEndOfLineAndModifySelection:', | ||
'Meta+KeyA': 'selectAll:', | ||
}; | ||
//# sourceMappingURL=usKeyboardLayout.js.map |
@@ -25,3 +25,2 @@ "use strict"; | ||
const page_1 = require("../page"); | ||
const platform = require("../platform"); | ||
const transport_1 = require("../transport"); | ||
@@ -31,13 +30,13 @@ 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 { | ||
constructor(transport, attachToDefaultContext) { | ||
super(); | ||
class WKBrowser extends browser_1.BrowserBase { | ||
constructor(transport, logger, attachToDefaultContext) { | ||
super(logger); | ||
this._defaultContext = null; | ||
this._contexts = new Map(); | ||
this._wkPages = new Map(); | ||
this._popupOpeners = []; | ||
this._firstPageCallback = () => { }; | ||
this._connection = new wkConnection_1.WKConnection(transport, this._onDisconnect.bind(this)); | ||
this._attachToDefaultContext = attachToDefaultContext; | ||
this._connection = new wkConnection_1.WKConnection(transport, logger, this._onDisconnect.bind(this)); | ||
this._browserSession = this._connection.browserSession; | ||
this._defaultContext = new WKBrowserContext(this, undefined, browserContext_1.validateBrowserContextOptions({})); | ||
if (attachToDefaultContext) | ||
this._defaultContext = new WKBrowserContext(this, undefined, browserContext_1.validateBrowserContextOptions({})); | ||
this._eventListeners = [ | ||
@@ -47,3 +46,4 @@ helper_1.helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)), | ||
helper_1.helper.addEventListener(this._browserSession, 'Playwright.provisionalLoadFailed', event => this._onProvisionalLoadFailed(event)), | ||
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)), | ||
@@ -53,4 +53,4 @@ ]; | ||
} | ||
static async connect(transport, slowMo = 0, attachToDefaultContext = false) { | ||
const browser = new WKBrowser(transport_1.SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext); | ||
static async connect(transport, logger, slowMo = 0, attachToDefaultContext = false) { | ||
const browser = new WKBrowser(transport_1.SlowMoTransport.wrap(transport, slowMo), logger, attachToDefaultContext); | ||
return browser; | ||
@@ -79,5 +79,2 @@ } | ||
} | ||
async newPage(options) { | ||
return browser_1.createPageInNewContext(this, options); | ||
} | ||
async _waitForFirstPageTarget() { | ||
@@ -87,5 +84,11 @@ helper_1.assert(!this._wkPages.size); | ||
} | ||
_onWindowOpen(payload) { | ||
this._popupOpeners.push(payload.pageProxyId); | ||
_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) { | ||
@@ -102,6 +105,6 @@ const { pageProxyInfo } = event; | ||
} | ||
if (!context && !this._attachToDefaultContext) | ||
return; | ||
if (!context) | ||
context = this._defaultContext; | ||
if (!context) | ||
return; | ||
const pageProxySession = new wkConnection_1.WKSession(this._connection, pageProxyId, `The page has been closed.`, (message) => { | ||
@@ -111,15 +114,8 @@ this._connection.rawSend({ ...message, pageProxyId }); | ||
const opener = pageProxyInfo.openerId ? this._wkPages.get(pageProxyInfo.openerId) : undefined; | ||
let hasInitialAboutBlank = false; | ||
if (pageProxyInfo.openerId) { | ||
const openerIndex = this._popupOpeners.indexOf(pageProxyInfo.openerId); | ||
if (openerIndex !== -1) { | ||
this._popupOpeners.splice(openerIndex, 1); | ||
// When this page is a result of window.open($url) call, we should have it's opener | ||
// in the list of popup openers. In this case we know there is an initial | ||
// about:blank navigation, followed by a navigation to $url. | ||
hasInitialAboutBlank = true; | ||
} | ||
const wkPage = new wkPage_1.WKPage(context, pageProxySession, opener || null); | ||
this._wkPages.set(pageProxyId, wkPage); | ||
if (opener && opener._initializedPage) { | ||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) | ||
signalBarrier.addPopup(wkPage.pageOrError()); | ||
} | ||
const wkPage = new wkPage_1.WKPage(context, pageProxySession, opener || null, hasInitialAboutBlank); | ||
this._wkPages.set(pageProxyId, wkPage); | ||
wkPage.pageOrError().then(async () => { | ||
@@ -161,12 +157,6 @@ this._firstPageCallback(); | ||
} | ||
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; | ||
} | ||
} | ||
@@ -176,3 +166,3 @@ exports.WKBrowser = WKBrowser; | ||
constructor(browser, browserContextId, options) { | ||
super(options); | ||
super(browser, options); | ||
this._browser = browser; | ||
@@ -183,14 +173,23 @@ this._browserContextId = browserContextId; | ||
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); | ||
} | ||
@@ -201,3 +200,3 @@ _wkPages() { | ||
pages() { | ||
return this._wkPages().map(wkPage => wkPage._initializedPage()).filter(pageOrNull => !!pageOrNull); | ||
return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull); | ||
} | ||
@@ -287,2 +286,7 @@ async newPage() { | ||
} | ||
async unroute(url, handler) { | ||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); | ||
for (const page of this.pages()) | ||
await page._delegate.updateRequestInterception(); | ||
} | ||
async close() { | ||
@@ -299,3 +303,3 @@ if (this._closed) | ||
this._browser._contexts.delete(this._browserContextId); | ||
this._didCloseInternal(); | ||
await this._didCloseInternal(); | ||
} | ||
@@ -302,0 +306,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"); | ||
// WKPlaywright uses this special id to issue Browser.close command which we | ||
@@ -29,7 +30,7 @@ // should ignore. | ||
class WKConnection { | ||
constructor(transport, onDisconnect) { | ||
constructor(transport, logger, onDisconnect) { | ||
this._lastId = 0; | ||
this._closed = false; | ||
this._debugProtocol = platform.debug('pw:protocol'); | ||
this._transport = transport; | ||
this._logger = logger; | ||
this._transport.onmessage = this._dispatchMessage.bind(this); | ||
@@ -41,3 +42,2 @@ this._transport.onclose = this._onClose.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 (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); | ||
this._transport.send(message); | ||
} | ||
_dispatchMessage(message) { | ||
this._debugProtocol('◀ RECV ' + message); | ||
const object = JSON.parse(message); | ||
if (object.id === exports.kBrowserCloseMessageId) | ||
if (this._logger._isLogEnabled(transport_1.protocolLog)) | ||
this._logger._log(transport_1.protocolLog, '◀ 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) { | ||
@@ -87,2 +87,3 @@ super(); | ||
this._callbacks = new Map(); | ||
this._crashed = false; | ||
this.connection = connection; | ||
@@ -99,2 +100,4 @@ this.sessionId = sessionId; | ||
async send(method, params) { | ||
if (this._crashed) | ||
throw new Error('Target crashed'); | ||
if (this._disposed) | ||
@@ -104,3 +107,2 @@ throw new Error(`Protocol error (${method}): ${this.errorText}`); | ||
const messageObj = { id, method, params }; | ||
platform.debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2)); | ||
this._rawSend(messageObj); | ||
@@ -111,2 +113,5 @@ return new Promise((resolve, reject) => { | ||
} | ||
markAsCrashed() { | ||
this._crashed = true; | ||
} | ||
isDisposed() { | ||
@@ -122,3 +127,2 @@ return this._disposed; | ||
dispatchMessage(object) { | ||
platform.debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2)); | ||
if (object.id && this._callbacks.has(object.id)) { | ||
@@ -128,3 +132,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 | ||
@@ -143,6 +147,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); | ||
@@ -165,3 +169,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,3 @@ "use strict"; | ||
const helper_1 = require("../helper"); | ||
const usKeyboardLayout_1 = require("../usKeyboardLayout"); | ||
const macEditingCommands_1 = require("../macEditingCommands"); | ||
function toModifiersMask(modifiers) { | ||
@@ -51,3 +51,3 @@ // From Source/WebKit/Shared/WebEvent.h | ||
const shortcut = parts.join('+'); | ||
let commands = usKeyboardLayout_1.macEditingCommands[shortcut]; | ||
let commands = macEditingCommands_1.macEditingCommands[shortcut]; | ||
if (helper_1.helper.isString(commands)) | ||
@@ -54,0 +54,0 @@ commands = [commands]; |
@@ -21,3 +21,3 @@ "use strict"; | ||
const network = require("../network"); | ||
const platform = require("../platform"); | ||
const logger_1 = require("../logger"); | ||
const errorReasons = { | ||
@@ -55,3 +55,3 @@ 'aborted': 'Cancellation', | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page); | ||
}); | ||
@@ -62,3 +62,3 @@ } | ||
const base64Encoded = !!response.body && !helper_1.helper.isString(response.body); | ||
const responseBody = response.body ? (base64Encoded ? response.body.toString('base64') : response.body) : undefined; | ||
const responseBody = response.body ? (base64Encoded ? response.body.toString('base64') : response.body) : ''; | ||
const responseHeaders = {}; | ||
@@ -72,3 +72,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.interceptWithResponse', { | ||
@@ -85,3 +85,3 @@ requestId: this._requestId, | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page); | ||
}); | ||
@@ -99,3 +99,3 @@ } | ||
// or the page was closed. We should tolerate these errors. | ||
helper_1.debugError(error); | ||
logger_1.logError(this.request._page); | ||
}); | ||
@@ -106,3 +106,3 @@ } | ||
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'); | ||
}; | ||
@@ -109,0 +109,0 @@ return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); |
@@ -30,9 +30,13 @@ "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 errors_1 = require("../errors"); | ||
const logger_1 = require("../logger"); | ||
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; | ||
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; | ||
class WKPage { | ||
constructor(browserContext, pageProxySession, opener, hasInitialAboutBlank) { | ||
constructor(browserContext, pageProxySession, opener) { | ||
this._provisionalPage = null; | ||
@@ -43,7 +47,7 @@ this._pagePromiseCallback = () => { }; | ||
this._evaluateOnNewDocumentSources = []; | ||
this._initialized = false; | ||
this._firstNonInitialNavigationCommittedCallback = () => { }; | ||
this._initializedPage = null; | ||
this._firstNonInitialNavigationCommittedFulfill = () => { }; | ||
this._firstNonInitialNavigationCommittedReject = (e) => { }; | ||
this._pageProxySession = pageProxySession; | ||
this._opener = opener; | ||
this._hasInitialAboutBlank = hasInitialAboutBlank; | ||
this.rawKeyboard = new wkInput_1.RawKeyboardImpl(pageProxySession); | ||
@@ -56,3 +60,3 @@ this.rawMouse = new wkInput_1.RawMouseImpl(pageProxySession); | ||
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 = [ | ||
@@ -65,7 +69,7 @@ helper_1.helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), | ||
this._pagePromise = new Promise(f => this._pagePromiseCallback = f); | ||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); | ||
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => { | ||
this._firstNonInitialNavigationCommittedFulfill = f; | ||
this._firstNonInitialNavigationCommittedReject = r; | ||
}); | ||
} | ||
_initializedPage() { | ||
return this._initialized ? this._page : null; | ||
} | ||
async _initializePageProxySession() { | ||
@@ -140,2 +144,3 @@ const promises = [ | ||
} | ||
promises.push(this.updateEmulateMedia()); | ||
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() })); | ||
@@ -173,4 +178,6 @@ if (contextOptions.offline) | ||
helper_1.helper.removeEventListeners(this._sessionListeners); | ||
if (crashed) | ||
if (crashed) { | ||
this._session.markAsCrashed(); | ||
this._page._didCrash(); | ||
} | ||
} | ||
@@ -193,2 +200,3 @@ } | ||
this._page._didDisconnect(); | ||
this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); | ||
} | ||
@@ -199,4 +207,8 @@ dispatchMessageToSession(message) { | ||
handleProvisionalLoadFailed(event) { | ||
if (!this._initialized || !this._provisionalPage) | ||
if (!this._initializedPage) { | ||
this._firstNonInitialNavigationCommittedReject(new Error('Initial load failed')); | ||
return; | ||
} | ||
if (!this._provisionalPage) | ||
return; | ||
let errorText = event.error; | ||
@@ -220,3 +232,3 @@ if (errorText.includes('cancelled')) | ||
helper_1.assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); | ||
if (!this._initialized) { | ||
if (!this._initializedPage) { | ||
helper_1.assert(!targetInfo.isProvisional); | ||
@@ -236,6 +248,19 @@ let pageOrError; | ||
if (targetInfo.isPaused) | ||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(helper_1.debugError); | ||
if (this._hasInitialAboutBlank) | ||
await this._firstNonInitialNavigationCommittedPromise; | ||
this._initialized = true; | ||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logger_1.logError(this._page)); | ||
if ((pageOrError instanceof page_1.Page) && this._page.mainFrame().url() === '') { | ||
try { | ||
// Initial empty page has an empty url. We should wait until the first real url has been loaded, | ||
// even if that url is about:blank. This is especially important for popups, where we need the | ||
// actual url before interacting with it. | ||
await this._firstNonInitialNavigationCommittedPromise; | ||
} | ||
catch (e) { | ||
pageOrError = e; | ||
} | ||
} | ||
else { | ||
// Avoid rejection on disconnect. | ||
this._firstNonInitialNavigationCommittedPromise.catch(() => { }); | ||
} | ||
this._initializedPage = pageOrError instanceof page_1.Page ? pageOrError : null; | ||
this._pagePromiseCallback(pageOrError); | ||
@@ -249,3 +274,3 @@ } | ||
this._provisionalPage.initializationPromise.then(() => { | ||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(helper_1.debugError); | ||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logger_1.logError(this._page)); | ||
}); | ||
@@ -274,2 +299,4 @@ } | ||
helper_1.helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._onLifecycleEvent(event.frameId, 'domcontentloaded')), | ||
helper_1.helper.addEventListener(this._session, 'Page.willRequestOpenWindow', event => this._onWillRequestOpenWindow()), | ||
helper_1.helper.addEventListener(this._session, 'Page.didRequestOpenWindow', event => this._onDidRequestOpenWindow(event)), | ||
helper_1.helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), | ||
@@ -297,6 +324,6 @@ helper_1.helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), | ||
sessions.push(this._provisionalPage._session); | ||
await Promise.all(sessions.map(session => callback(session).catch(helper_1.debugError))); | ||
await Promise.all(sessions.map(session => callback(session).catch(logger_1.logError(this._page)))); | ||
} | ||
_onFrameScheduledNavigation(frameId) { | ||
this._page._frameManager.frameRequestedNavigation(frameId); | ||
this._page._frameManager.frameRequestedNavigation(frameId, ''); | ||
} | ||
@@ -309,2 +336,12 @@ _onFrameStoppedLoading(frameId) { | ||
} | ||
_onWillRequestOpenWindow() { | ||
for (const barrier of this._page._frameManager._signalBarriers) | ||
barrier.expectPopup(); | ||
} | ||
_onDidRequestOpenWindow(event) { | ||
if (!event.opened) { | ||
for (const barrier of this._page._frameManager._signalBarriers) | ||
barrier.unexpectPopup(); | ||
} | ||
} | ||
_handleFrameTree(frameTree) { | ||
@@ -331,3 +368,3 @@ this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null); | ||
if (!initial) | ||
this._firstNonInitialNavigationCommittedCallback(); | ||
this._firstNonInitialNavigationCommittedFulfill(); | ||
} | ||
@@ -384,4 +421,12 @@ _onFrameNavigatedWithinDocument(frameId, url) { | ||
if (level === 'error' && source === 'javascript') { | ||
const error = new Error(text); | ||
error.stack = 'Error: ' + error.message; // Nullify stack. Stack is supposed to contain error message as the first line. | ||
const message = text.startsWith('Error: ') ? text.substring(7) : text; | ||
const error = new Error(message); | ||
if (event.message.stackTrace) { | ||
error.stack = event.message.stackTrace.map(callFrame => { | ||
return `${callFrame.functionName}@${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber}`; | ||
}).join('\n'); | ||
} | ||
else { | ||
error.stack = ''; | ||
} | ||
this._page.emit(events_1.Events.Page.PageError, error); | ||
@@ -448,4 +493,5 @@ return; | ||
} | ||
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)); | ||
} | ||
@@ -474,2 +520,6 @@ async setViewportSize(viewportSize) { | ||
]; | ||
if (options.isMobile) { | ||
const angle = viewport.width > viewport.height ? 90 : 0; | ||
promises.push(this._session.send('Page.setOrientationOverride', { angle })); | ||
} | ||
await Promise.all(promises); | ||
@@ -522,3 +572,3 @@ } | ||
const script = this._bindingToScript(binding); | ||
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(helper_1.debugError))); | ||
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(logger_1.logError(this._page)))); | ||
} | ||
@@ -549,3 +599,3 @@ async evaluateOnNewDocument(script) { | ||
runBeforeUnload | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
} | ||
@@ -556,14 +606,11 @@ canScreenshotOutsideViewport() { | ||
async setBackgroundColor(color) { | ||
// TODO: line below crashes, sort it out. | ||
await this._session.send('Page.setDefaultBackgroundColorOverride', { color }); | ||
} | ||
async takeScreenshot(format, documentRect, viewportRect, quality) { | ||
// TODO: documentRect does not include pageScale, while backend considers it does. | ||
// This brakes mobile screenshots of elements or full page. | ||
const rect = (documentRect || viewportRect); | ||
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' }); | ||
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; | ||
@@ -617,2 +664,4 @@ } | ||
}).catch(e => { | ||
if (e instanceof Error && e.message.includes('Node is detached from document')) | ||
throw new errors_1.NotConnectedError(); | ||
if (e instanceof Error && e.message.includes('Node does not have a layout object')) | ||
@@ -626,3 +675,3 @@ e.message = 'Node is either not visible or not an HTMLElement'; | ||
objectId: toRemoteObject(handle).objectId | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result) | ||
@@ -642,3 +691,3 @@ return null; | ||
const objectId = toRemoteObject(handle).objectId; | ||
await this._session.send('DOM.setInputFiles', { objectId, files }); | ||
await this._session.send('DOM.setInputFiles', { objectId, files: dom.toFileTransferPayload(files) }); | ||
} | ||
@@ -649,3 +698,3 @@ async adoptElementHandle(handle, to) { | ||
executionContextId: to._delegate._contextId | ||
}).catch(helper_1.debugError); | ||
}).catch(logger_1.logError(this._page)); | ||
if (!result || result.object.subtype === 'null') | ||
@@ -664,4 +713,3 @@ throw new Error('Unable to adopt element handle from a different document'); | ||
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) => { | ||
@@ -693,2 +741,4 @@ const frame = await handle.contentFrame().catch(e => null); | ||
const documentId = isNavigationRequest ? event.loaderId : undefined; | ||
if (isNavigationRequest) | ||
this._page._frameManager.frameUpdatedDocumentIdForNavigation(event.frameId, documentId); | ||
const request = new wkInterceptableRequest_1.WKInterceptableRequest(session, this._page._needsRequestInterception(), frame, event, redirectedFrom, documentId); | ||
@@ -695,0 +745,0 @@ this._requestIdToRequest.set(event.requestId, request); |
@@ -33,3 +33,3 @@ "use strict"; | ||
helper_1.helper.addEventListener(session, 'Worker.workerCreated', (event) => { | ||
const worker = new page_1.Worker(event.url); | ||
const worker = new page_1.Worker(this._page, event.url); | ||
const workerSession = new wkConnection_1.WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message) => { | ||
@@ -36,0 +36,0 @@ session.send('Worker.sendMessageToWorker', { |
{ | ||
"name": "playwright-core", | ||
"version": "0.12.1", | ||
"version": "0.13.0-next.1587452068410", | ||
"description": "A high-level API to automate web browsers", | ||
@@ -11,19 +11,19 @@ "repository": "github:Microsoft/playwright", | ||
"playwright": { | ||
"chromium_revision": "751710", | ||
"firefox_revision": "1051", | ||
"webkit_revision": "1182" | ||
"chromium_revision": "759546", | ||
"firefox_revision": "1084", | ||
"webkit_revision": "1201" | ||
}, | ||
"scripts": { | ||
"ctest": "cross-env BROWSER=chromium node test/test.js", | ||
"ftest": "cross-env BROWSER=firefox node test/test.js", | ||
"wtest": "cross-env BROWSER=webkit node test/test.js", | ||
"test": "cross-env BROWSER=all node test/test.js", | ||
"cunit": "cross-env BROWSER=chromium node test/test.js", | ||
"funit": "cross-env BROWSER=firefox node test/test.js", | ||
"wunit": "cross-env BROWSER=webkit node test/test.js", | ||
"unit": "cross-env BROWSER=all node test/test.js", | ||
"ccoverage": "cross-env COVERAGE=true BROWSER=chromium node test/test.js", | ||
"fcoverage": "cross-env COVERAGE=true BROWSER=firefox node test/test.js", | ||
"wcoverage": "cross-env COVERAGE=true BROWSER=webkit node test/test.js", | ||
"coverage": "cross-env COVERAGE=true BROWSER=all node test/test.js", | ||
"ctest": "cross-env BROWSER=chromium node --unhandled-rejections=strict test/test.js", | ||
"ftest": "cross-env BROWSER=firefox node --unhandled-rejections=strict test/test.js", | ||
"wtest": "cross-env BROWSER=webkit node --unhandled-rejections=strict test/test.js", | ||
"test": "cross-env BROWSER=all node --unhandled-rejections=strict test/test.js", | ||
"cunit": "cross-env BROWSER=chromium node --unhandled-rejections=strict test/test.js", | ||
"funit": "cross-env BROWSER=firefox node --unhandled-rejections=strict test/test.js", | ||
"wunit": "cross-env BROWSER=webkit node --unhandled-rejections=strict test/test.js", | ||
"unit": "cross-env BROWSER=all node --unhandled-rejections=strict test/test.js", | ||
"ccoverage": "cross-env COVERAGE=true BROWSER=chromium node --unhandled-rejections=strict test/test.js", | ||
"fcoverage": "cross-env COVERAGE=true BROWSER=firefox node --unhandled-rejections=strict test/test.js", | ||
"wcoverage": "cross-env COVERAGE=true BROWSER=webkit node --unhandled-rejections=strict test/test.js", | ||
"coverage": "cross-env COVERAGE=true BROWSER=all node --unhandled-rejections=strict test/test.js", | ||
"eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src", | ||
@@ -39,3 +39,2 @@ "tsc": "tsc -p .", | ||
"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", | ||
@@ -49,7 +48,8 @@ "generate-types": "node utils/generate_types/" | ||
"dependencies": { | ||
"debug": "^4.1.0", | ||
"extract-zip": "^1.6.6", | ||
"debug": "^4.1.1", | ||
"extract-zip": "^2.0.0", | ||
"https-proxy-agent": "^3.0.0", | ||
"jpeg-js": "^0.3.6", | ||
"pngjs": "^3.4.0", | ||
"jpeg-js": "^0.3.7", | ||
"mime": "^2.4.4", | ||
"pngjs": "^5.0.0", | ||
"progress": "^2.0.3", | ||
@@ -63,3 +63,4 @@ "proxy-from-env": "^1.1.0", | ||
"@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", | ||
@@ -77,3 +78,3 @@ "@types/proxy-from-env": "^1.0.0", | ||
"formidable": "^1.2.1", | ||
"minimist": "^1.2.0", | ||
"minimist": "^1.2.5", | ||
"ncp": "^2.0.0", | ||
@@ -84,3 +85,3 @@ "node-stream-zip": "^1.8.2", | ||
"ts-loader": "^6.1.2", | ||
"typescript": "^3.7.5", | ||
"typescript": "^3.8.3", | ||
"webpack": "^4.41.0", | ||
@@ -87,0 +88,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 3 instances in 1 package
9
1408452
10
25
34797
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)
+ Addedpngjs@5.0.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)
- Removedpngjs@3.4.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)
Updateddebug@^4.1.1
Updatedextract-zip@^2.0.0
Updatedjpeg-js@^0.3.7
Updatedpngjs@^5.0.0