| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // |
| // CDPClient |
| // |
| class CDPClient { |
| constructor() { |
| this._requestId = 0; |
| this._sessions = new Map(); |
| } |
| |
| nextRequestId() { |
| return ++this._requestId; |
| } |
| |
| addSession(session) { |
| this._sessions.set(session.sessionId(), session); |
| } |
| |
| getSession(sessionId) { |
| this._sessions.get(sessionId); |
| } |
| |
| async dispatchMessage(message) { |
| const messageObject = JSON.parse(message); |
| const session = this._sessions.get(messageObject.sessionId || ''); |
| if (session) { |
| session.dispatchMessage(messageObject); |
| } |
| } |
| |
| reportError(message, error) { |
| if (error) { |
| console.error(`${message}: ${error}\n${error.stack}`); |
| } else { |
| console.error(message); |
| } |
| } |
| } |
| |
| const cdpClient = new CDPClient(); |
| |
| // |
| // CDPSession |
| // |
| class CDPSession { |
| constructor(sessionId) { |
| this._sessionId = sessionId || ''; |
| this._parentSessionId = null; |
| this._dispatchTable = new Map(); |
| this._eventHandlers = new Map(); |
| this._protocol = this._getProtocol(); |
| cdpClient.addSession(this); |
| } |
| |
| sessionId() { |
| return this._sessionId; |
| } |
| |
| protocol() { |
| return this._protocol; |
| } |
| |
| createSession(sessionId) { |
| const session = new CDPSession(sessionId); |
| session._parentSessionId = this._sessionId; |
| return session; |
| } |
| |
| async sendCommand(method, params) { |
| const requestId = cdpClient.nextRequestId(); |
| const messageObject = {'id': requestId, 'method': method, 'params': params}; |
| if (this._sessionId) { |
| messageObject.sessionId = this._sessionId; |
| } |
| sendDevToolsMessage(JSON.stringify(messageObject)); |
| return new Promise(f => this._dispatchTable.set(requestId, f)); |
| } |
| |
| async dispatchMessage(message) { |
| try { |
| const messageId = message.id; |
| if (typeof messageId === 'number') { |
| const handler = this._dispatchTable.get(messageId); |
| if (handler) { |
| this._dispatchTable.delete(messageId); |
| handler(message); |
| } else { |
| cdpClient.reportError(`Unexpected result id ${messageId}`); |
| } |
| } else { |
| const eventName = message.method; |
| for (const handler of (this._eventHandlers.get(eventName) || [])) { |
| handler(message); |
| } |
| } |
| } catch (e) { |
| cdpClient.reportError( |
| `Exception when dispatching message\n' + |
| '${JSON.stringify(message)}`, |
| e); |
| } |
| } |
| |
| _getProtocol() { |
| return new Proxy({}, { |
| get: (target, domainName, receiver) => new Proxy({}, { |
| get: (target, methodName, receiver) => { |
| const eventPattern = /^(on(ce)?|off)([A-Z][A-Za-z0-9]*)/; |
| const match = eventPattern.exec(methodName); |
| if (!match) { |
| return args => this.sendCommand( |
| `${domainName}.${methodName}`, args || {}); |
| } |
| let eventName = match[3]; |
| eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1); |
| if (match[1] === 'once') { |
| return eventMatcher => this._waitForEvent( |
| `${domainName}.${eventName}`, eventMatcher); |
| } |
| if (match[1] === 'off') { |
| return listener => this._removeEventHandler( |
| `${domainName}.${eventName}`, listener); |
| } |
| return listener => this._addEventHandler( |
| `${domainName}.${eventName}`, listener); |
| }, |
| }), |
| }); |
| } |
| |
| _waitForEvent(eventName, eventMatcher) { |
| return new Promise(callback => { |
| const handler = result => { |
| if (eventMatcher && !eventMatcher(result)) { |
| return; |
| } |
| this._removeEventHandler(eventName, handler); |
| callback(result); |
| }; |
| this._addEventHandler(eventName, handler); |
| }); |
| } |
| |
| _addEventHandler(eventName, handler) { |
| const handlers = this._eventHandlers.get(eventName) || []; |
| handlers.push(handler); |
| this._eventHandlers.set(eventName, handlers); |
| } |
| |
| _removeEventHandler(eventName, handler) { |
| const handlers = this._eventHandlers.get(eventName) || []; |
| const index = handlers.indexOf(handler); |
| if (index === -1) { |
| return; |
| } |
| handlers.splice(index, 1); |
| this._eventHandlers.set(eventName, handlers); |
| } |
| } |
| |
| // |
| // TargetPage |
| // |
| class TargetPage { |
| constructor(browserSession) { |
| this._browserSession = browserSession; |
| this._targetId = ''; |
| this._session; |
| } |
| |
| static async create(browserSession) { |
| const targetPage = new TargetPage(browserSession); |
| |
| const dp = browserSession.protocol(); |
| const params = {url: 'about:blank'}; |
| targetPage._targetId = |
| (await dp.Target.createTarget(params)).result.targetId; |
| |
| const sessionId = (await dp.Target.attachToTarget({ |
| targetId: targetPage._targetId, |
| flatten: true, |
| })).result.sessionId; |
| targetPage._session = browserSession.createSession(sessionId); |
| |
| return targetPage; |
| } |
| |
| targetId() { |
| return this._targetId; |
| } |
| |
| session() { |
| return this._session; |
| } |
| |
| async load(url) { |
| const dp = this._session.protocol(); |
| await dp.Page.enable(); |
| await dp.Page.setLifecycleEventsEnabled({enabled: true}); |
| const frameId = (await dp.Page.navigate({url})).result.frameId; |
| await dp.Page.onceLifecycleEvent( |
| event => |
| event.params.name === 'load' && event.params.frameId === frameId); |
| } |
| |
| close() { |
| const dp = this._browserSession.protocol(); |
| return dp.Target.closeTarget({targetId: this._targetId}); |
| } |
| } |
| |
| // |
| // Command handlers |
| // |
| async function dumpDOM(dp) { |
| const script = '(document.doctype ? new ' + |
| 'XMLSerializer().serializeToString(document.doctype) + \'\\n\' : \'\')' + |
| ' + document.documentElement.outerHTML'; |
| |
| const response = await dp.Runtime.evaluate({expression: script}); |
| return response.result.result.value; |
| } |
| |
| async function printToPDF(dp, params) { |
| const displayHeaderFooter = !params.noHeaderFooter; |
| const generateTaggedPDF = !params.disablePDFTagging; |
| const generateDocumentOutline = params.generateDocumentOutline; |
| |
| const printToPDFParams = { |
| displayHeaderFooter, |
| generateTaggedPDF, |
| generateDocumentOutline, |
| printBackground: true, |
| preferCSSPageSize: true, |
| }; |
| |
| const response = await dp.Page.printToPDF(printToPDFParams); |
| return response.result.data; |
| } |
| |
| async function screenshot(dp, params) { |
| const format = params.format || 'png'; |
| const screenshotParams = { |
| format, |
| }; |
| |
| if (params.width > 0 && params.height > 0) { |
| screenshotParams.clip = { |
| x: 0, |
| y: 0, |
| width: params.width, |
| height: params.height, |
| scale: 1.0, |
| }; |
| } |
| |
| const response = await dp.Page.captureScreenshot(screenshotParams); |
| return response.result.data; |
| } |
| |
| async function handleCommands(dp, commands) { |
| const result = {}; |
| if ('dumpDom' in commands) { |
| result.dumpDomResult = await dumpDOM(dp); |
| } |
| |
| if ('printToPDF' in commands) { |
| result.printToPdfResult = await printToPDF(dp, commands.printToPDF); |
| } |
| |
| if ('screenshot' in commands) { |
| result.screenshotResult = await screenshot(dp, commands.screenshot); |
| } |
| |
| return result; |
| } |
| |
| // |
| // Target.exposeDevToolsProtocol() communication functions. |
| // |
| function sendDevToolsMessage(json) { |
| // console.log('[send] ' + json); |
| window.cdp.send(json); |
| } |
| |
| // |
| // This is called from the host. |
| // |
| async function executeCommands(commands) { |
| window.cdp.onmessage = json => { |
| // console.log('[recv] ' + json); |
| cdpClient.dispatchMessage(json); |
| }; |
| |
| const browserSession = new CDPSession(); |
| const targetPage = await TargetPage.create(browserSession); |
| const dp = targetPage.session().protocol(); |
| |
| let domContentEventFired = false; |
| dp.Page.onceDomContentEventFired(() => { |
| domContentEventFired = true; |
| }); |
| |
| const promises = []; |
| let pageLoadTimedOut; |
| if ('timeout' in commands) { |
| const timeoutPromise = new Promise(resolve => { |
| setTimeout(() => { |
| if (pageLoadTimedOut === undefined) { |
| pageLoadTimedOut = true; |
| dp.Page.stopLoading(); |
| } |
| resolve(); |
| }, commands.timeout); |
| }); |
| promises.push(timeoutPromise); |
| } |
| |
| promises.push(targetPage.load(commands.targetUrl)); |
| await Promise.race(promises); |
| |
| if (pageLoadTimedOut === undefined) { |
| pageLoadTimedOut = false; |
| } |
| |
| if ('defaultBackgroundColor' in commands) { |
| await dp.Emulation.setDefaultBackgroundColorOverride( |
| {color: commands.defaultBackgroundColor}); |
| } |
| |
| if ('virtualTimeBudget' in commands && !pageLoadTimedOut) { |
| await dp.Emulation.setVirtualTimePolicy({ |
| budget: commands.virtualTimeBudget, |
| maxVirtualTimeTaskStarvationCount: 9999, |
| policy: 'pauseIfNetworkFetchesPending', |
| }); |
| await dp.Emulation.onceVirtualTimeBudgetExpired(); |
| } |
| |
| const result = await handleCommands(dp, commands); |
| |
| // Report timeouts only if we received no content at all. |
| if (pageLoadTimedOut && !domContentEventFired) { |
| result.pageLoadTimedOut = true; |
| } |
| |
| await targetPage.close(); |
| |
| return result; |
| } |