Target.ts (8185B)
1 /** 2 * @license 3 * Copyright 2019 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 9 import type {Browser} from '../api/Browser.js'; 10 import type {BrowserContext} from '../api/BrowserContext.js'; 11 import {PageEvent, type Page} from '../api/Page.js'; 12 import {Target, TargetType} from '../api/Target.js'; 13 import {debugError} from '../common/util.js'; 14 import type {Viewport} from '../common/Viewport.js'; 15 import {Deferred} from '../util/Deferred.js'; 16 17 import type {CdpCDPSession} from './CdpSession.js'; 18 import {CdpPage} from './Page.js'; 19 import type {TargetManager} from './TargetManager.js'; 20 import {CdpWebWorker} from './WebWorker.js'; 21 22 /** 23 * @internal 24 */ 25 export enum InitializationStatus { 26 SUCCESS = 'success', 27 ABORTED = 'aborted', 28 } 29 30 /** 31 * @internal 32 */ 33 export class CdpTarget extends Target { 34 #browserContext?: BrowserContext; 35 #session?: CdpCDPSession; 36 #targetInfo: Protocol.Target.TargetInfo; 37 #targetManager?: TargetManager; 38 #sessionFactory: 39 | ((isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>) 40 | undefined; 41 #childTargets = new Set<CdpTarget>(); 42 _initializedDeferred = Deferred.create<InitializationStatus>(); 43 _isClosedDeferred = Deferred.create<void>(); 44 _targetId: string; 45 46 /** 47 * To initialize the target for use, call initialize. 48 * 49 * @internal 50 */ 51 constructor( 52 targetInfo: Protocol.Target.TargetInfo, 53 session: CdpCDPSession | undefined, 54 browserContext: BrowserContext | undefined, 55 targetManager: TargetManager | undefined, 56 sessionFactory: 57 | ((isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>) 58 | undefined, 59 ) { 60 super(); 61 this.#session = session; 62 this.#targetManager = targetManager; 63 this.#targetInfo = targetInfo; 64 this.#browserContext = browserContext; 65 this._targetId = targetInfo.targetId; 66 this.#sessionFactory = sessionFactory; 67 if (this.#session) { 68 this.#session.setTarget(this); 69 } 70 } 71 72 override async asPage(): Promise<Page> { 73 const session = this._session(); 74 if (!session) { 75 return await this.createCDPSession().then(client => { 76 return CdpPage._create(client, this, null); 77 }); 78 } 79 return await CdpPage._create(session, this, null); 80 } 81 82 _subtype(): string | undefined { 83 return this.#targetInfo.subtype; 84 } 85 86 _session(): CdpCDPSession | undefined { 87 return this.#session; 88 } 89 90 _addChildTarget(target: CdpTarget): void { 91 this.#childTargets.add(target); 92 } 93 94 _removeChildTarget(target: CdpTarget): void { 95 this.#childTargets.delete(target); 96 } 97 98 _childTargets(): ReadonlySet<CdpTarget> { 99 return this.#childTargets; 100 } 101 102 protected _sessionFactory(): ( 103 isAutoAttachEmulated: boolean, 104 ) => Promise<CdpCDPSession> { 105 if (!this.#sessionFactory) { 106 throw new Error('sessionFactory is not initialized'); 107 } 108 return this.#sessionFactory; 109 } 110 111 override createCDPSession(): Promise<CdpCDPSession> { 112 if (!this.#sessionFactory) { 113 throw new Error('sessionFactory is not initialized'); 114 } 115 return this.#sessionFactory(false).then(session => { 116 session.setTarget(this); 117 return session; 118 }); 119 } 120 121 override url(): string { 122 return this.#targetInfo.url; 123 } 124 125 override type(): TargetType { 126 const type = this.#targetInfo.type; 127 switch (type) { 128 case 'page': 129 return TargetType.PAGE; 130 case 'background_page': 131 return TargetType.BACKGROUND_PAGE; 132 case 'service_worker': 133 return TargetType.SERVICE_WORKER; 134 case 'shared_worker': 135 return TargetType.SHARED_WORKER; 136 case 'browser': 137 return TargetType.BROWSER; 138 case 'webview': 139 return TargetType.WEBVIEW; 140 case 'tab': 141 return TargetType.TAB; 142 default: 143 return TargetType.OTHER; 144 } 145 } 146 147 _targetManager(): TargetManager { 148 if (!this.#targetManager) { 149 throw new Error('targetManager is not initialized'); 150 } 151 return this.#targetManager; 152 } 153 154 _getTargetInfo(): Protocol.Target.TargetInfo { 155 return this.#targetInfo; 156 } 157 158 override browser(): Browser { 159 if (!this.#browserContext) { 160 throw new Error('browserContext is not initialized'); 161 } 162 return this.#browserContext.browser(); 163 } 164 165 override browserContext(): BrowserContext { 166 if (!this.#browserContext) { 167 throw new Error('browserContext is not initialized'); 168 } 169 return this.#browserContext; 170 } 171 172 override opener(): Target | undefined { 173 const {openerId} = this.#targetInfo; 174 if (!openerId) { 175 return; 176 } 177 return this.browser() 178 .targets() 179 .find(target => { 180 return (target as CdpTarget)._targetId === openerId; 181 }); 182 } 183 184 _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void { 185 this.#targetInfo = targetInfo; 186 this._checkIfInitialized(); 187 } 188 189 _initialize(): void { 190 this._initializedDeferred.resolve(InitializationStatus.SUCCESS); 191 } 192 193 _isTargetExposed(): boolean { 194 return this.type() !== TargetType.TAB && !this._subtype(); 195 } 196 197 protected _checkIfInitialized(): void { 198 if (!this._initializedDeferred.resolved()) { 199 this._initializedDeferred.resolve(InitializationStatus.SUCCESS); 200 } 201 } 202 } 203 204 /** 205 * @internal 206 */ 207 export class PageTarget extends CdpTarget { 208 #defaultViewport?: Viewport; 209 protected pagePromise?: Promise<Page>; 210 211 constructor( 212 targetInfo: Protocol.Target.TargetInfo, 213 session: CdpCDPSession | undefined, 214 browserContext: BrowserContext, 215 targetManager: TargetManager, 216 sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>, 217 defaultViewport: Viewport | null, 218 ) { 219 super(targetInfo, session, browserContext, targetManager, sessionFactory); 220 this.#defaultViewport = defaultViewport ?? undefined; 221 } 222 223 override _initialize(): void { 224 this._initializedDeferred 225 .valueOrThrow() 226 .then(async result => { 227 if (result === InitializationStatus.ABORTED) { 228 return; 229 } 230 const opener = this.opener(); 231 if (!(opener instanceof PageTarget)) { 232 return; 233 } 234 if (!opener || !opener.pagePromise || this.type() !== 'page') { 235 return true; 236 } 237 const openerPage = await opener.pagePromise; 238 if (!openerPage.listenerCount(PageEvent.Popup)) { 239 return true; 240 } 241 const popupPage = await this.page(); 242 openerPage.emit(PageEvent.Popup, popupPage); 243 return true; 244 }) 245 .catch(debugError); 246 this._checkIfInitialized(); 247 } 248 249 override async page(): Promise<Page | null> { 250 if (!this.pagePromise) { 251 const session = this._session(); 252 this.pagePromise = ( 253 session 254 ? Promise.resolve(session) 255 : this._sessionFactory()(/* isAutoAttachEmulated=*/ false) 256 ).then(client => { 257 return CdpPage._create(client, this, this.#defaultViewport ?? null); 258 }); 259 } 260 return (await this.pagePromise) ?? null; 261 } 262 263 override _checkIfInitialized(): void { 264 if (this._initializedDeferred.resolved()) { 265 return; 266 } 267 if (this._getTargetInfo().url !== '') { 268 this._initializedDeferred.resolve(InitializationStatus.SUCCESS); 269 } 270 } 271 } 272 273 /** 274 * @internal 275 */ 276 export class DevToolsTarget extends PageTarget {} 277 278 /** 279 * @internal 280 */ 281 export class WorkerTarget extends CdpTarget { 282 #workerPromise?: Promise<CdpWebWorker>; 283 284 override async worker(): Promise<CdpWebWorker | null> { 285 if (!this.#workerPromise) { 286 const session = this._session(); 287 // TODO(einbinder): Make workers send their console logs. 288 this.#workerPromise = ( 289 session 290 ? Promise.resolve(session) 291 : this._sessionFactory()(/* isAutoAttachEmulated=*/ false) 292 ).then(client => { 293 return new CdpWebWorker( 294 client, 295 this._getTargetInfo().url, 296 this._targetId, 297 this.type(), 298 () => {} /* consoleAPICalled */, 299 () => {} /* exceptionThrown */, 300 undefined /* networkManager */, 301 ); 302 }); 303 } 304 return await this.#workerPromise; 305 } 306 } 307 308 /** 309 * @internal 310 */ 311 export class OtherTarget extends CdpTarget {}