lib/Target.js
const util = require('util')
const Events = require('./Events')
const Page = require('./page/Page')
const { helper } = require('./helper')
/** @ignore */
const {
closeTarget,
exposeCDPOnTarget,
getWindowForTarget,
setWindowBounds
} = require('./__shared')
/**
* @typedef {Object} TargetInit
* @property {Object} targetInfo
* @property {?BrowserContext} [browserContext]
* @property {?function():Promise<CDPSession>} [sessionFactory]
* @property {?PageInitOptions} [pageOpts]
* @property {Chrome|CDPSession|CRIConnection} [client]
*/
/**
* @see https://chromedevtools.github.io/devtools-protocol/tot/Target
*/
class Target {
/**
* @param {!TargetInit} targetInit
*/
constructor ({
targetInfo,
browserContext,
sessionFactory,
pageOpts,
client
}) {
/**
* @type {!Object}
* @private
*/
this._targetInfo = targetInfo
/**
* @type {?BrowserContext}
* @private
*/
this._browserContext = browserContext
/**
* @type {Chrome|CDPSession|CRIConnection|undefined}
*/
this._client = client
/**
* @type {string}
* @private
*/
this._targetId = targetInfo.targetId
/**
* @type {function(): Promise<CDPSession>}
* @private
*/
this._sessionFactory = sessionFactory || this.__sessionFactory.bind(this)
/**
* @ignore
* @type {?Array<{emitter: !EventEmitter, eventName: (string|symbol), handler: function(*)}>}
* @private
*/
this.__eventListeners = null
if (!this._browserContext) {
this.__eventListeners = [
helper.addEventListener(
this._client,
'Target.targetInfoChanged',
this.__onTargetInfoChanged
),
helper.addEventListener(
this._client,
'Target.targetDestroyed',
this.__onTargetDestroyed
)
]
}
/**
* @type {PageInitOptions}
* @private
*/
this._pageOpts = pageOpts || {}
/** @type {?Promise<Page>} */
this._pagePromise = null
/** @type {Promise<boolean>} */
this._initializedPromise = new Promise(
resolve => (this._initializedCallback = resolve)
).then(async success => {
if (!success) return false
const opener = this.opener()
if (!opener || !opener._pagePromise || this.type() !== 'page') return true
const openerPage = await opener._pagePromise
if (!openerPage.listenerCount(Events.Page.Popup)) return true
const popupPage = await this.page()
openerPage.emit(Events.Page.Popup, popupPage)
return true
})
/** @type {Promise<*>} */
this._isClosedPromise = new Promise(
resolve => (this._closedCallback = resolve)
)
/** @type {boolean} */
this._isInitialized =
this._targetInfo.type !== 'page' || this._targetInfo.url !== ''
if (this._isInitialized) this._initializedCallback(true)
}
/**
* @return {string}
*/
id () {
return this._targetInfo.targetId
}
/**
* @return {string}
*/
url () {
return this._targetInfo.url
}
/**
* @return {"page"|"background_page"|"service_worker"|"other"|"browser"}
*/
type () {
const type = this._targetInfo.type
if (
type === 'page' ||
type === 'background_page' ||
type === 'service_worker' ||
type === 'browser'
) {
return type
}
return 'other'
}
/**
* @return {?string}
*/
openerId () {
return this._targetInfo.openerId
}
/**
* @return {Promise<CDPSession>}
*/
createCDPSession () {
return this._sessionFactory()
}
/**
* Returns T/F indicating if the target is for a page
* @return {boolean}
*/
isPageTarget () {
if (this._targetInfo) return this._targetInfo.type === 'page'
return false
}
/**
* Returns T/F indicating if the target is for a background page
* @return {boolean}
*/
isBackgroundPageTarget () {
if (this._targetInfo) return this._targetInfo.type === 'background_page'
return false
}
/**
* Returns T/F indicating if the target is for a browser
* @return {boolean}
*/
isBrowserTarget () {
if (this._targetInfo) return this._targetInfo.type === 'browser'
return false
}
/**
* Returns T/F indicating if the target is for a service worker
* @return {boolean}
*/
isServiceWorkerTarget () {
if (this._targetInfo) return this._targetInfo.type === 'service_worker'
return false
}
/**
* @return {?Browser}
*/
browser () {
if (!this._browserContext) return null
return this._browserContext.browser()
}
/**
* @return {?BrowserContext}
*/
browserContext () {
if (!this._browserContext) return null
return this._browserContext
}
/**
* @return {?Target}
*/
opener () {
const { openerId } = this._targetInfo
if (!openerId) return null
const browser = this.browser()
if (browser) return browser.getTargetById(openerId)
return null
}
/**
* @return {Promise<Page|undefined>}
*/
page () {
if (
(this._targetInfo.type === 'page' ||
this._targetInfo.type === 'background_page') &&
!this._pagePromise
) {
this._pagePromise = this._sessionFactory().then(client =>
Page.create(client, Object.assign({ target: this }, this._pageOpts))
)
}
return this._pagePromise
}
/**
* Closes the target. If the target is a page that gets closed too.
* @return {Promise<boolean>}
*/
close () {
if (this._browserContext) {
return this._browserContext.closeTarget(this._targetInfo.targetId)
}
return closeTarget(this._client, this._targetInfo.targetId)
}
/**
* Get the browser window id and bounds for this target
* @return {Promise<{bounds: WindowBounds, windowId: number}>}
*/
getWindowBounds () {
if (this._browserContext) {
return this._browserContext.getWindowForTarget(this._targetInfo.targetId)
}
return getWindowForTarget(this._client, this._targetInfo.targetId)
}
/**
* Set position and/or size of the browser window. EXPERIMENTAL
* @param {number} windowId - An browser window id
* @param {WindowBounds} bounds - New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged.
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Browser#method-setWindowBounds
*/
async setWindowBounds (windowId, bounds) {
if (this._browserContext) {
await this._browserContext.setWindowBounds(windowId, bounds)
}
await setWindowBounds(this._client, windowId, bounds)
}
/**
* Inject object to the target's main frame that provides a communication channel with browser target.
*
* Injected object will be available as window[bindingName].
*
* The object has the following API:
* * binding.send(json) - a method to send messages over the remote debugging protocol
* * binding.onmessage = json => handleMessage(json) - a callback that will be called for the protocol notifications and command responses.
*
* EXPERIMENTAL
* @param {string} [bindingName] - Binding name, 'cdp' if not specified
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Target#method-exposeDevToolsProtocol
*/
async exposeDevToolsProtocol (bindingName) {
if (this._browserContext) {
await this._browserContext.exposeCDPOnTarget(
this._targetInfo.targetId,
bindingName || undefined
)
return
}
await exposeCDPOnTarget(
this._client,
this._targetInfo.targetId,
bindingName
)
}
/**
* @return {Object}
*/
toJSON () {
return this._targetInfo
}
/**
* @param {!Object} targetInfo
*/
_targetInfoChanged (targetInfo) {
this._targetInfo = targetInfo
if (
!this._isInitialized &&
(this._targetInfo.type !== 'page' || this._targetInfo.url !== '')
) {
this._isInitialized = true
this._initializedCallback(true)
}
}
/**
* @ignore
* @param {!Object} event
*/
__onTargetInfoChanged (event) {
if (event.targetInfo.targetId === this._targetInfo.targetId) {
this._targetInfoChanged(event.targetInfo)
}
}
/**
* @ignore
* @param {!Object} event
*/
__onTargetDestroyed (event) {
if (event.targetInfo.targetId === this._targetInfo.targetId) {
this._initializedCallback(false)
this._closedCallback()
helper.removeEventListeners(this.__eventListeners)
}
}
/**
* @ignore
* @return {Promise<CDPSession>|MSMediaKeySession|MediaKeySession}
* @private
*/
__sessionFactory () {
return this._client.createSession(this._targetInfo)
}
/** @ignore */
// eslint-disable-next-line space-before-function-paren
[util.inspect.custom](depth, options) {
if (depth < 0) {
return options.stylize('[Target]', 'special')
}
const newOptions = Object.assign({}, options, {
depth: options.depth == null ? null : options.depth - 1
})
const inner = util.inspect(this._targetInfo, newOptions)
return `${options.stylize('Target', 'special')} ${inner}`
}
}
/**
* @type {Target}
*/
module.exports = Target