diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/cdp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/cdp')
188 files changed, 39524 insertions, 0 deletions
diff --git a/remote/cdp/CDP.sys.mjs b/remote/cdp/CDP.sys.mjs new file mode 100644 index 0000000000..0307c71149 --- /dev/null +++ b/remote/cdp/CDP.sys.mjs @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + JSONHandler: "chrome://remote/content/cdp/JSONHandler.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + RecommendedPreferences: + "chrome://remote/content/shared/RecommendedPreferences.sys.mjs", + TargetList: "chrome://remote/content/cdp/targets/TargetList.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); +ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder()); + +// Map of CDP-specific preferences that should be set via +// RecommendedPreferences. +const RECOMMENDED_PREFS = new Map([ + // Prevent various error message on the console + // jest-puppeteer asserts that no error message is emitted by the console + [ + "browser.contentblocking.features.standard", + "-tp,tpPrivate,cookieBehavior0,-cm,-fp", + ], + // Accept all cookies (see behavior definitions in nsICookieService.idl) + ["network.cookie.cookieBehavior", 0], +]); + +/** + * Entry class for the Chrome DevTools Protocol support. + * + * It holds the list of available targets (tabs, main browser), and also + * sets up the necessary handlers for the HTTP server. + * + * @see https://chromedevtools.github.io/devtools-protocol + */ +export class CDP { + /** + * Creates a new instance of the CDP class. + * + * @param {RemoteAgent} agent + * Reference to the Remote Agent instance. + */ + constructor(agent) { + this.agent = agent; + this.targetList = null; + + this._running = false; + this._activePortPath; + } + + get address() { + const mainTarget = this.targetList.getMainProcessTarget(); + return mainTarget.wsDebuggerURL; + } + + get mainTargetPath() { + const mainTarget = this.targetList.getMainProcessTarget(); + return mainTarget.path; + } + + /** + * Starts the CDP support. + */ + async start() { + if (this._running) { + return; + } + + // Note: Ideally this would only be set at the end of the method. However + // since start() is async, we prefer to set the flag early in order to + // avoid potential race conditions. + this._running = true; + + lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS); + + // Starting CDP too early can cause issues with clients in not being able + // to find any available target. Also when closing the application while + // it's still starting up can cause shutdown hangs. As such CDP will be + // started when the initial application window has finished initializing. + lazy.logger.debug(`Waiting for initial application window`); + await this.agent.browserStartupFinished; + + this.agent.server.registerPrefixHandler("/", new lazy.JSONHandler(this)); + + this.targetList = new lazy.TargetList(); + this.targetList.on("target-created", (eventName, target) => { + this.agent.server.registerPathHandler(target.path, target); + }); + this.targetList.on("target-destroyed", (eventName, target) => { + this.agent.server.registerPathHandler(target.path, null); + }); + + await this.targetList.watchForTargets(); + + Cu.printStderr(`DevTools listening on ${this.address}\n`); + + // Write connection details to DevToolsActivePort file within the profile. + this._activePortPath = PathUtils.join( + PathUtils.profileDir, + "DevToolsActivePort" + ); + + const data = `${this.agent.port}\n${this.mainTargetPath}`; + try { + await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data)); + } catch (e) { + lazy.logger.warn( + `Failed to create ${this._activePortPath} (${e.message})` + ); + } + } + + /** + * Stops the CDP support. + */ + async stop() { + if (!this._running) { + return; + } + + try { + await IOUtils.remove(this._activePortPath); + } catch (e) { + lazy.logger.warn( + `Failed to remove ${this._activePortPath} (${e.message})` + ); + } + + try { + this.targetList?.destructor(); + this.targetList = null; + + lazy.RecommendedPreferences.restorePreferences(RECOMMENDED_PREFS); + } catch (e) { + lazy.logger.error("Failed to stop protocol", e); + } finally { + this._running = false; + } + } +} diff --git a/remote/cdp/CDPConnection.sys.mjs b/remote/cdp/CDPConnection.sys.mjs new file mode 100644 index 0000000000..1d2eb3e77c --- /dev/null +++ b/remote/cdp/CDPConnection.sys.mjs @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", + UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); + +export class CDPConnection extends WebSocketConnection { + /** + * @param {WebSocket} webSocket + * The WebSocket server connection to wrap. + * @param {Connection} httpdConnection + * Reference to the httpd.js's connection needed for clean-up. + */ + constructor(webSocket, httpdConnection) { + super(webSocket, httpdConnection); + + this.sessions = new Map(); + this.defaultSession = null; + } + + /** + * Register a new Session to forward the messages to. + * + * A session without any `id` attribute will be considered to be the + * default one, to which messages without `sessionId` attribute are + * forwarded to. Only one such session can be registered. + * + * @param {Session} session + * The session to register. + */ + registerSession(session) { + // CDP is not compatible with Fission by default, check the appropriate + // preferences are set to ensure compatibility. + if ( + Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 || + Services.prefs.getBoolPref("fission.bfcacheInParent") + ) { + lazy.logger.warn( + `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` + + `to 0 and "fission.bfcacheInParent" to false before Firefox starts.` + ); + } + + if (!session.id) { + if (this.defaultSession) { + throw new Error( + "Default session is already set on Connection, " + + "can't register another one." + ); + } + this.defaultSession = session; + } + + this.sessions.set(session.id, session); + } + + /** + * Send an error back to the CDP client. + * + * @param {number} id + * Id of the packet which lead to an error. + * @param {Error} err + * Error object with `message` and `stack` attributes. + * @param {string=} sessionId + * Id of the session used to send this packet. Falls back to the + * default session if not specified. + */ + sendError(id, err, sessionId) { + const error = { + message: err.message, + data: err.stack, + }; + + this.send({ id, error, sessionId }); + } + + /** + * Send an event coming from a Domain to the CDP client. + * + * @param {string} method + * The event name. This is composed by a domain name, a dot character + * followed by the event name, e.g. `Target.targetCreated`. + * @param {object} params + * A JSON-serializable object, which is the payload of this event. + * @param {string=} sessionId + * The sessionId from which this packet is emitted. Falls back to the + * default session if not specified. + */ + sendEvent(method, params, sessionId) { + this.send({ method, params, sessionId }); + + if (Services.profiler?.IsActive()) { + ChromeUtils.addProfilerMarker( + "CDP: Event", + { category: "Remote-Protocol" }, + method + ); + } + + // When a client attaches to a secondary target via + // `Target.attachToTarget`, we should emit an event back with the + // result including the `sessionId` attribute of this secondary target's + // session. `Target.attachToTarget` creates the secondary session and + // returns the session ID. + if (sessionId) { + // receivedMessageFromTarget is expected to send a raw CDP packet + // in the `message` property and it to be already serialized to a + // string + this.send({ + method: "Target.receivedMessageFromTarget", + params: { sessionId, message: JSON.stringify({ method, params }) }, + }); + } + } + + /** + * Interpret a given CDP packet for a given Session. + * + * @param {string} sessionId + * ID of the session for which we should execute a command. + * @param {string} message + * The stringified JSON payload of the CDP packet, which is about + * executing a Domain's function. + */ + sendMessageToTarget(sessionId, message) { + const session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`Session '${sessionId}' doesn't exist.`); + } + // `message` is received from `Target.sendMessageToTarget` where the + // message attribute is a stringified JSON payload which represent a CDP + // packet. + const packet = JSON.parse(message); + + // The CDP packet sent by the client shouldn't have a sessionId attribute + // as it is passed as another argument of `Target.sendMessageToTarget`. + // Set it here in order to reuse the codepath of flatten session, where + // the client sends CDP packets with a `sessionId` attribute instead + // of going through the old and probably deprecated + // `Target.sendMessageToTarget` API. + packet.sessionId = sessionId; + this.onPacket(packet); + } + + /** + * Send the result of a call to a Domain's function back to the CDP client. + * + * @param {number} id + * The request id being sent by the client to call the domain's method. + * @param {object} result + * A JSON-serializable object, which is the actual result. + * @param {string=} sessionId + * The sessionId from which this packet is emitted. Falls back to the + * default session if not specified. + */ + sendResult(id, result, sessionId) { + result = typeof result != "undefined" ? result : {}; + this.send({ id, result, sessionId }); + + // When a client attaches to a secondary target via + // `Target.attachToTarget`, and it executes a command via + // `Target.sendMessageToTarget`, we should emit an event back with the + // result including the `sessionId` attribute of this secondary target's + // session. `Target.attachToTarget` creates the secondary session and + // returns the session ID. + if (sessionId) { + // receivedMessageFromTarget is expected to send a raw CDP packet + // in the `message` property and it to be already serialized to a + // string + this.send({ + method: "Target.receivedMessageFromTarget", + params: { sessionId, message: JSON.stringify({ id, result }) }, + }); + } + } + + // Transport hooks + + /** + * Called by the `transport` when the connection is closed. + */ + onConnectionClose() { + // Cleanup all the registered sessions. + for (const session of this.sessions.values()) { + session.destructor(); + } + this.sessions.clear(); + + super.onConnectionClose(); + } + + /** + * Receive a packet from the WebSocket layer. + * + * This packet is sent by a CDP client and is meant to execute + * a particular function on a given Domain. + * + * @param {object} packet + * JSON-serializable object sent by the client. + */ + async onPacket(packet) { + super.onPacket(packet); + + const { id, method, params, sessionId } = packet; + const startTime = Cu.now(); + + try { + // First check for mandatory field in the packets + if (typeof id == "undefined") { + throw new TypeError("Message missing 'id' field"); + } + if (typeof method == "undefined") { + throw new TypeError("Message missing 'method' field"); + } + + // Extract the domain name and the method name out of `method` attribute + const { domain, command } = splitMethod(method); + + // If a `sessionId` field is passed, retrieve the session to which we + // should forward this packet. Otherwise send it to the default session. + let session; + if (!sessionId) { + if (!this.defaultSession) { + throw new Error("Connection is missing a default Session."); + } + session = this.defaultSession; + } else { + session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`Session '${sessionId}' doesn't exists.`); + } + } + + // Bug 1600317 - Workaround to deny internal methods to be called + if (command.startsWith("_")) { + throw new lazy.UnknownMethodError(command); + } + + // Finally, instruct the targeted session to execute the command + const result = await session.execute(id, domain, command, params); + this.sendResult(id, result, sessionId); + } catch (e) { + this.sendError(id, e, packet.sessionId); + } + + if (Services.profiler?.IsActive()) { + ChromeUtils.addProfilerMarker( + "CDP: Command", + { startTime, category: "Remote-Protocol" }, + `${method} (${id})` + ); + } + } +} + +/** + * Splits a CDP method into domain and command components. + * + * @param {string} method + * Name of the method to split, e.g. "Browser.getVersion". + * + * @returns {Object<string, string>} + * Object with the domain ("Browser") and command ("getVersion") + * as properties. + */ +export function splitMethod(method) { + const parts = method.split("."); + + if (parts.length != 2 || !parts[0].length || !parts[1].length) { + throw new TypeError(`Invalid method format: '${method}'`); + } + + return { + domain: parts[0], + command: parts[1], + }; +} diff --git a/remote/cdp/Error.sys.mjs b/remote/cdp/Error.sys.mjs new file mode 100644 index 0000000000..b047285649 --- /dev/null +++ b/remote/cdp/Error.sys.mjs @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); + +export class RemoteAgentError extends Error { + constructor(message = "", cause = undefined) { + cause = cause || message; + super(cause); + + this.name = this.constructor.name; + this.message = message; + this.cause = cause; + + this.notify(); + } + + notify() { + console.error(this); + lazy.logger.error(this.toString({ stack: true })); + } + + toString({ stack = false } = {}) { + return RemoteAgentError.format(this, { stack }); + } + + static format(e, { stack = false } = {}) { + return formatError(e, { stack }); + } + + /** + * Takes a serialised CDP error and reconstructs it + * as a RemoteAgentError. + * + * The error must be of this form: + * + * {"message": "TypeError: foo is not a function\n + * execute@chrome://remote/content/cdp/sessions/Session.jsm:73:39\n + * onMessage@chrome://remote/content/cdp/sessions/TabSession.jsm:65:20"} + * + * This approach has the notable deficiency that it cannot deal + * with causes to errors because of the unstructured nature of CDP + * errors. A possible future improvement would be to extend the + * error serialisation to include discrete fields for each data + * property. + * + * @param {object} json + * CDP error encoded as a JSON object, which must have a + * "message" field, where the first line will make out the error + * message and the subsequent lines the stacktrace. + * + * @returns {RemoteAgentError} + */ + static fromJSON(json) { + const [message, ...stack] = json.message.split("\n"); + const err = new RemoteAgentError(); + err.message = message.slice(0, -1); + err.stack = stack.map(s => s.trim()).join("\n"); + err.cause = null; + return err; + } +} + +/** + * A fatal error that it is not possible to recover from + * or send back to the client. + * + * Constructing this error will force the application to quit. + */ +export class FatalError extends RemoteAgentError { + constructor(...args) { + super(...args); + this.quit(); + } + + notify() { + lazy.logger.fatal(this.toString({ stack: true })); + } + + quit(mode = Ci.nsIAppStartup.eForceQuit) { + Services.startup.quit(mode); + } +} + +/** When an operation is not yet implemented. */ +export class UnsupportedError extends RemoteAgentError {} + +/** The requested remote method does not exist. */ +export class UnknownMethodError extends RemoteAgentError { + constructor(domain, command = null) { + if (command) { + super(`${domain}.${command}`); + } else { + super(domain); + } + } +} + +function formatError(error, { stack = false } = {}) { + const els = []; + + els.push(error.name); + if (error.message) { + els.push(": "); + els.push(error.message); + } + + if (stack && error.stack) { + els.push(":\n"); + + const stack = error.stack.trim().split("\n"); + els.push(stack.map(line => `\t${line}`).join("\n")); + + if (error.cause) { + els.push("\n"); + els.push("caused by: " + formatError(error.cause, { stack })); + } + } + + return els.join(""); +} diff --git a/remote/cdp/JSONHandler.sys.mjs b/remote/cdp/JSONHandler.sys.mjs new file mode 100644 index 0000000000..5cb81d6a9a --- /dev/null +++ b/remote/cdp/JSONHandler.sys.mjs @@ -0,0 +1,266 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", + HTTP_404: "chrome://remote/content/server/httpd.sys.mjs", + HTTP_405: "chrome://remote/content/server/httpd.sys.mjs", + HTTP_500: "chrome://remote/content/server/httpd.sys.mjs", + Protocol: "chrome://remote/content/cdp/Protocol.sys.mjs", + RemoteAgentError: "chrome://remote/content/cdp/Error.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", +}); + +export class JSONHandler { + constructor(cdp) { + this.cdp = cdp; + this.routes = { + "/json/version": { + handler: this.getVersion.bind(this), + }, + + "/json/protocol": { + handler: this.getProtocol.bind(this), + }, + + "/json/list": { + handler: this.getTargetList.bind(this), + }, + + "/json": { + handler: this.getTargetList.bind(this), + }, + + // PUT only - /json/new?{url} + "/json/new": { + handler: this.newTarget.bind(this), + method: "PUT", + }, + + // /json/activate/{targetId} + "/json/activate": { + handler: this.activateTarget.bind(this), + parameter: true, + }, + + // /json/close/{targetId} + "/json/close": { + handler: this.closeTarget.bind(this), + parameter: true, + }, + }; + } + + getVersion() { + const mainProcessTarget = this.cdp.targetList.getMainProcessTarget(); + + const { userAgent } = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + + return { + body: { + Browser: `${Services.appinfo.name}/${Services.appinfo.version}`, + "Protocol-Version": "1.3", + "User-Agent": userAgent, + "V8-Version": "1.0", + "WebKit-Version": "1.0", + webSocketDebuggerUrl: mainProcessTarget.toJSON().webSocketDebuggerUrl, + }, + }; + } + + getProtocol() { + return { body: lazy.Protocol.Description }; + } + + getTargetList() { + return { body: [...this.cdp.targetList].filter(x => x.type !== "browser") }; + } + + /** HTTP copy of Target.createTarget() */ + async newTarget(url) { + const onTarget = this.cdp.targetList.once("target-created"); + + // Open new tab + const tab = await lazy.TabManager.addTab({ + focus: true, + }); + + // Get the newly created target + const target = await onTarget; + if (tab.linkedBrowser != target.browser) { + throw new Error( + "Unexpected tab opened: " + tab.linkedBrowser.currentURI.spec + ); + } + + const returnJson = target.toJSON(); + + // Load URL if given, otherwise stay on about:blank + if (url) { + let validURL; + try { + validURL = Services.io.newURI(url); + } catch { + // If we failed to parse given URL, return now since we already loaded about:blank + return { body: returnJson }; + } + + target.browsingContext.loadURI(validURL, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + + // Force the URL in the returned target JSON to match given + // even if loading/will fail (matches Chromium behavior) + returnJson.url = url; + } + + return { body: returnJson }; + } + + /** HTTP copy of Target.activateTarget() */ + async activateTarget(targetId) { + // Try to get the target from given id + const target = this.cdp.targetList.getById(targetId); + + if (!target) { + return { + status: lazy.HTTP_404, + body: `No such target id: ${targetId}`, + json: false, + }; + } + + // Select the tab (this endpoint does not show the window) + await lazy.TabManager.selectTab(target.tab); + + return { body: "Target activated", json: false }; + } + + /** HTTP copy of Target.closeTarget() */ + async closeTarget(targetId) { + // Try to get the target from given id + const target = this.cdp.targetList.getById(targetId); + + if (!target) { + return { + status: lazy.HTTP_404, + body: `No such target id: ${targetId}`, + json: false, + }; + } + + // Remove the tab + await lazy.TabManager.removeTab(target.tab); + + return { body: "Target is closing", json: false }; + } + + // nsIHttpRequestHandler + + async handle(request, response) { + // Mark request as async so we can execute async routes and return values + response.processAsync(); + + // Run a provided route (function) with an argument + const runRoute = async (route, data) => { + try { + // Run the route to get data to return + const { + status = { code: 200, description: "OK" }, + json = true, + body, + } = await route(data); + + // Stringify into returnable JSON if wanted + const payload = json + ? JSON.stringify(body, null, lazy.Log.verbose ? "\t" : null) + : body; + + // Handle HTTP response + response.setStatusLine( + request.httpVersion, + status.code, + status.description + ); + response.setHeader("Content-Type", "application/json"); + response.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); + response.write(payload); + } catch (e) { + new lazy.RemoteAgentError(e).notify(); + + // Mark as 500 as an error has occured internally + response.setStatusLine( + request.httpVersion, + lazy.HTTP_500.code, + lazy.HTTP_500.description + ); + } + }; + + // Trim trailing slashes to conform with expected routes + const path = request.path.replace(/\/+$/, ""); + + let route; + for (const _route in this.routes) { + // Prefixed/parameter route (/path/{parameter}) + if (path.startsWith(_route + "/") && this.routes[_route].parameter) { + route = _route; + break; + } + + // Regular route (/path/example) + if (path === _route) { + route = _route; + break; + } + } + + if (!route) { + // Route does not exist + response.setStatusLine( + request.httpVersion, + lazy.HTTP_404.code, + lazy.HTTP_404.description + ); + response.write("Unknown command: " + path.replace("/json/", "")); + + return response.finish(); + } + + const { handler, method, parameter } = this.routes[route]; + + // If only one valid method for route, check method matches + if (method && request.method !== method) { + response.setStatusLine( + request.httpVersion, + lazy.HTTP_405.code, + lazy.HTTP_405.description + ); + response.write( + `Using unsafe HTTP verb ${request.method} to invoke ${route}. This action supports only PUT verb.` + ); + return response.finish(); + } + + if (parameter) { + await runRoute(handler, path.split("/").pop()); + } else { + await runRoute(handler, request.queryString); + } + + // Send response + return response.finish(); + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIHttpRequestHandler"]); + } +} diff --git a/remote/cdp/Protocol.sys.mjs b/remote/cdp/Protocol.sys.mjs new file mode 100644 index 0000000000..f239c82b42 --- /dev/null +++ b/remote/cdp/Protocol.sys.mjs @@ -0,0 +1,17358 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The `Description` below is imported from Chromium Code. + +// TODO(ato): We send back a description of the protocol +// when the user makes the initial HTTP request, +// but the following is pure fiction. +const Description = { + "domains": [ + { + "domain": "Accessibility", + "experimental": true, + "dependencies": [ + "DOM" + ], + "types": [ + { + "id": "AXNodeId", + "description": "Unique accessibility node identifier.", + "type": "string" + }, + { + "id": "AXValueType", + "description": "Enum of possible property types.", + "type": "string", + "enum": [ + "boolean", + "tristate", + "booleanOrUndefined", + "idref", + "idrefList", + "integer", + "node", + "nodeList", + "number", + "string", + "computedString", + "token", + "tokenList", + "domRelation", + "role", + "internalRole", + "valueUndefined" + ] + }, + { + "id": "AXValueSourceType", + "description": "Enum of possible property sources.", + "type": "string", + "enum": [ + "attribute", + "implicit", + "style", + "contents", + "placeholder", + "relatedElement" + ] + }, + { + "id": "AXValueNativeSourceType", + "description": "Enum of possible native property sources (as a subtype of a particular AXValueSourceType).", + "type": "string", + "enum": [ + "figcaption", + "label", + "labelfor", + "labelwrapped", + "legend", + "tablecaption", + "title", + "other" + ] + }, + { + "id": "AXValueSource", + "description": "A single source for a computed AX property.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "What type of source this is.", + "$ref": "AXValueSourceType" + }, + { + "name": "value", + "description": "The value of this property source.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "attribute", + "description": "The name of the relevant attribute, if any.", + "optional": true, + "type": "string" + }, + { + "name": "attributeValue", + "description": "The value of the relevant attribute, if any.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "superseded", + "description": "Whether this source is superseded by a higher priority source.", + "optional": true, + "type": "boolean" + }, + { + "name": "nativeSource", + "description": "The native markup source for this value, e.g. a <label> element.", + "optional": true, + "$ref": "AXValueNativeSourceType" + }, + { + "name": "nativeSourceValue", + "description": "The value, such as a node or node list, of the native source.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "invalid", + "description": "Whether the value for this property is invalid.", + "optional": true, + "type": "boolean" + }, + { + "name": "invalidReason", + "description": "Reason for the value being invalid, if it is.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "AXRelatedNode", + "type": "object", + "properties": [ + { + "name": "backendDOMNodeId", + "description": "The BackendNodeId of the related DOM node.", + "$ref": "DOM.BackendNodeId" + }, + { + "name": "idref", + "description": "The IDRef value provided, if any.", + "optional": true, + "type": "string" + }, + { + "name": "text", + "description": "The text alternative of this node in the current context.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "AXProperty", + "type": "object", + "properties": [ + { + "name": "name", + "description": "The name of this property.", + "$ref": "AXPropertyName" + }, + { + "name": "value", + "description": "The value of this property.", + "$ref": "AXValue" + } + ] + }, + { + "id": "AXValue", + "description": "A single computed AX property.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "The type of this value.", + "$ref": "AXValueType" + }, + { + "name": "value", + "description": "The computed value of this property.", + "optional": true, + "type": "any" + }, + { + "name": "relatedNodes", + "description": "One or more related nodes, if applicable.", + "optional": true, + "type": "array", + "items": { + "$ref": "AXRelatedNode" + } + }, + { + "name": "sources", + "description": "The sources which contributed to the computation of this property.", + "optional": true, + "type": "array", + "items": { + "$ref": "AXValueSource" + } + } + ] + }, + { + "id": "AXPropertyName", + "description": "Values of AXProperty name: from 'busy' to 'roledescription' - states which apply to every AX\nnode, from 'live' to 'root' - attributes which apply to nodes in live regions, from\n'autocomplete' to 'valuetext' - attributes which apply to widgets, from 'checked' to 'selected'\n- states which apply to widgets, from 'activedescendant' to 'owns' - relationships between\nelements other than parent/child/sibling.", + "type": "string", + "enum": [ + "busy", + "disabled", + "editable", + "focusable", + "focused", + "hidden", + "hiddenRoot", + "invalid", + "keyshortcuts", + "settable", + "roledescription", + "live", + "atomic", + "relevant", + "root", + "autocomplete", + "hasPopup", + "level", + "multiselectable", + "orientation", + "multiline", + "readonly", + "required", + "valuemin", + "valuemax", + "valuetext", + "checked", + "expanded", + "modal", + "pressed", + "selected", + "activedescendant", + "controls", + "describedby", + "details", + "errormessage", + "flowto", + "labelledby", + "owns" + ] + }, + { + "id": "AXNode", + "description": "A node in the accessibility tree.", + "type": "object", + "properties": [ + { + "name": "nodeId", + "description": "Unique identifier for this node.", + "$ref": "AXNodeId" + }, + { + "name": "ignored", + "description": "Whether this node is ignored for accessibility", + "type": "boolean" + }, + { + "name": "ignoredReasons", + "description": "Collection of reasons why this node is hidden.", + "optional": true, + "type": "array", + "items": { + "$ref": "AXProperty" + } + }, + { + "name": "role", + "description": "This `Node`'s role, whether explicit or implicit.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "name", + "description": "The accessible name for this `Node`.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "description", + "description": "The accessible description for this `Node`.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "value", + "description": "The value for this `Node`.", + "optional": true, + "$ref": "AXValue" + }, + { + "name": "properties", + "description": "All other properties", + "optional": true, + "type": "array", + "items": { + "$ref": "AXProperty" + } + }, + { + "name": "childIds", + "description": "IDs for each of this node's child nodes.", + "optional": true, + "type": "array", + "items": { + "$ref": "AXNodeId" + } + }, + { + "name": "backendDOMNodeId", + "description": "The backend ID for the associated DOM node, if any.", + "optional": true, + "$ref": "DOM.BackendNodeId" + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables the accessibility domain." + }, + { + "name": "enable", + "description": "Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled." + }, + { + "name": "getPartialAXTree", + "description": "Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node to get the partial accessibility tree for.", + "optional": true, + "$ref": "DOM.NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node to get the partial accessibility tree for.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper to get the partial accessibility tree for.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + }, + { + "name": "fetchRelatives", + "description": "Whether to fetch this nodes ancestors, siblings and children. Defaults to true.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "nodes", + "description": "The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\nchildren, if requested.", + "type": "array", + "items": { + "$ref": "AXNode" + } + } + ] + }, + { + "name": "getFullAXTree", + "description": "Fetches the entire accessibility tree", + "experimental": true, + "returns": [ + { + "name": "nodes", + "type": "array", + "items": { + "$ref": "AXNode" + } + } + ] + } + ] + }, + { + "domain": "Animation", + "experimental": true, + "dependencies": [ + "Runtime", + "DOM" + ], + "types": [ + { + "id": "Animation", + "description": "Animation instance.", + "type": "object", + "properties": [ + { + "name": "id", + "description": "`Animation`'s id.", + "type": "string" + }, + { + "name": "name", + "description": "`Animation`'s name.", + "type": "string" + }, + { + "name": "pausedState", + "description": "`Animation`'s internal paused state.", + "type": "boolean" + }, + { + "name": "playState", + "description": "`Animation`'s play state.", + "type": "string" + }, + { + "name": "playbackRate", + "description": "`Animation`'s playback rate.", + "type": "number" + }, + { + "name": "startTime", + "description": "`Animation`'s start time.", + "type": "number" + }, + { + "name": "currentTime", + "description": "`Animation`'s current time.", + "type": "number" + }, + { + "name": "type", + "description": "Animation type of `Animation`.", + "type": "string", + "enum": [ + "CSSTransition", + "CSSAnimation", + "WebAnimation" + ] + }, + { + "name": "source", + "description": "`Animation`'s source animation node.", + "optional": true, + "$ref": "AnimationEffect" + }, + { + "name": "cssId", + "description": "A unique ID for `Animation` representing the sources that triggered this CSS\nanimation/transition.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "AnimationEffect", + "description": "AnimationEffect instance", + "type": "object", + "properties": [ + { + "name": "delay", + "description": "`AnimationEffect`'s delay.", + "type": "number" + }, + { + "name": "endDelay", + "description": "`AnimationEffect`'s end delay.", + "type": "number" + }, + { + "name": "iterationStart", + "description": "`AnimationEffect`'s iteration start.", + "type": "number" + }, + { + "name": "iterations", + "description": "`AnimationEffect`'s iterations.", + "type": "number" + }, + { + "name": "duration", + "description": "`AnimationEffect`'s iteration duration.", + "type": "number" + }, + { + "name": "direction", + "description": "`AnimationEffect`'s playback direction.", + "type": "string" + }, + { + "name": "fill", + "description": "`AnimationEffect`'s fill mode.", + "type": "string" + }, + { + "name": "backendNodeId", + "description": "`AnimationEffect`'s target node.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "keyframesRule", + "description": "`AnimationEffect`'s keyframes.", + "optional": true, + "$ref": "KeyframesRule" + }, + { + "name": "easing", + "description": "`AnimationEffect`'s timing function.", + "type": "string" + } + ] + }, + { + "id": "KeyframesRule", + "description": "Keyframes Rule", + "type": "object", + "properties": [ + { + "name": "name", + "description": "CSS keyframed animation's name.", + "optional": true, + "type": "string" + }, + { + "name": "keyframes", + "description": "List of animation keyframes.", + "type": "array", + "items": { + "$ref": "KeyframeStyle" + } + } + ] + }, + { + "id": "KeyframeStyle", + "description": "Keyframe Style", + "type": "object", + "properties": [ + { + "name": "offset", + "description": "Keyframe's time offset.", + "type": "string" + }, + { + "name": "easing", + "description": "`AnimationEffect`'s timing function.", + "type": "string" + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables animation domain notifications." + }, + { + "name": "enable", + "description": "Enables animation domain notifications." + }, + { + "name": "getCurrentTime", + "description": "Returns the current time of the an animation.", + "parameters": [ + { + "name": "id", + "description": "Id of animation.", + "type": "string" + } + ], + "returns": [ + { + "name": "currentTime", + "description": "Current time of the page.", + "type": "number" + } + ] + }, + { + "name": "getPlaybackRate", + "description": "Gets the playback rate of the document timeline.", + "returns": [ + { + "name": "playbackRate", + "description": "Playback rate for animations on page.", + "type": "number" + } + ] + }, + { + "name": "releaseAnimations", + "description": "Releases a set of animations to no longer be manipulated.", + "parameters": [ + { + "name": "animations", + "description": "List of animation ids to seek.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "resolveAnimation", + "description": "Gets the remote object of the Animation.", + "parameters": [ + { + "name": "animationId", + "description": "Animation id.", + "type": "string" + } + ], + "returns": [ + { + "name": "remoteObject", + "description": "Corresponding remote object.", + "$ref": "Runtime.RemoteObject" + } + ] + }, + { + "name": "seekAnimations", + "description": "Seek a set of animations to a particular time within each animation.", + "parameters": [ + { + "name": "animations", + "description": "List of animation ids to seek.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "currentTime", + "description": "Set the current time of each animation.", + "type": "number" + } + ] + }, + { + "name": "setPaused", + "description": "Sets the paused state of a set of animations.", + "parameters": [ + { + "name": "animations", + "description": "Animations to set the pause state of.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "paused", + "description": "Paused state to set to.", + "type": "boolean" + } + ] + }, + { + "name": "setPlaybackRate", + "description": "Sets the playback rate of the document timeline.", + "parameters": [ + { + "name": "playbackRate", + "description": "Playback rate for animations on page", + "type": "number" + } + ] + }, + { + "name": "setTiming", + "description": "Sets the timing of an animation node.", + "parameters": [ + { + "name": "animationId", + "description": "Animation id.", + "type": "string" + }, + { + "name": "duration", + "description": "Duration of the animation.", + "type": "number" + }, + { + "name": "delay", + "description": "Delay of the animation.", + "type": "number" + } + ] + } + ], + "events": [ + { + "name": "animationCanceled", + "description": "Event for when an animation has been cancelled.", + "parameters": [ + { + "name": "id", + "description": "Id of the animation that was cancelled.", + "type": "string" + } + ] + }, + { + "name": "animationCreated", + "description": "Event for each animation that has been created.", + "parameters": [ + { + "name": "id", + "description": "Id of the animation that was created.", + "type": "string" + } + ] + }, + { + "name": "animationStarted", + "description": "Event for animation that has been started.", + "parameters": [ + { + "name": "animation", + "description": "Animation that was started.", + "$ref": "Animation" + } + ] + } + ] + }, + { + "domain": "ApplicationCache", + "experimental": true, + "types": [ + { + "id": "ApplicationCacheResource", + "description": "Detailed application cache resource information.", + "type": "object", + "properties": [ + { + "name": "url", + "description": "Resource url.", + "type": "string" + }, + { + "name": "size", + "description": "Resource size.", + "type": "integer" + }, + { + "name": "type", + "description": "Resource type.", + "type": "string" + } + ] + }, + { + "id": "ApplicationCache", + "description": "Detailed application cache information.", + "type": "object", + "properties": [ + { + "name": "manifestURL", + "description": "Manifest URL.", + "type": "string" + }, + { + "name": "size", + "description": "Application cache size.", + "type": "number" + }, + { + "name": "creationTime", + "description": "Application cache creation time.", + "type": "number" + }, + { + "name": "updateTime", + "description": "Application cache update time.", + "type": "number" + }, + { + "name": "resources", + "description": "Application cache resources.", + "type": "array", + "items": { + "$ref": "ApplicationCacheResource" + } + } + ] + }, + { + "id": "FrameWithManifest", + "description": "Frame identifier - manifest URL pair.", + "type": "object", + "properties": [ + { + "name": "frameId", + "description": "Frame identifier.", + "$ref": "Page.FrameId" + }, + { + "name": "manifestURL", + "description": "Manifest URL.", + "type": "string" + }, + { + "name": "status", + "description": "Application cache status.", + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "enable", + "description": "Enables application cache domain notifications." + }, + { + "name": "getApplicationCacheForFrame", + "description": "Returns relevant application cache data for the document in given frame.", + "parameters": [ + { + "name": "frameId", + "description": "Identifier of the frame containing document whose application cache is retrieved.", + "$ref": "Page.FrameId" + } + ], + "returns": [ + { + "name": "applicationCache", + "description": "Relevant application cache data for the document in given frame.", + "$ref": "ApplicationCache" + } + ] + }, + { + "name": "getFramesWithManifests", + "description": "Returns array of frame identifiers with manifest urls for each frame containing a document\nassociated with some application cache.", + "returns": [ + { + "name": "frameIds", + "description": "Array of frame identifiers with manifest urls for each frame containing a document\nassociated with some application cache.", + "type": "array", + "items": { + "$ref": "FrameWithManifest" + } + } + ] + }, + { + "name": "getManifestForFrame", + "description": "Returns manifest URL for document in the given frame.", + "parameters": [ + { + "name": "frameId", + "description": "Identifier of the frame containing document whose manifest is retrieved.", + "$ref": "Page.FrameId" + } + ], + "returns": [ + { + "name": "manifestURL", + "description": "Manifest URL for document in the given frame.", + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "applicationCacheStatusUpdated", + "parameters": [ + { + "name": "frameId", + "description": "Identifier of the frame containing document whose application cache updated status.", + "$ref": "Page.FrameId" + }, + { + "name": "manifestURL", + "description": "Manifest URL.", + "type": "string" + }, + { + "name": "status", + "description": "Updated application cache status.", + "type": "integer" + } + ] + }, + { + "name": "networkStateUpdated", + "parameters": [ + { + "name": "isNowOnline", + "type": "boolean" + } + ] + } + ] + }, + { + "domain": "Audits", + "description": "Audits domain allows investigation of page violations and possible improvements.", + "experimental": true, + "dependencies": [ + "Network" + ], + "commands": [ + { + "name": "getEncodedResponse", + "description": "Returns the response body and size if it were re-encoded with the specified settings. Only\napplies to images.", + "parameters": [ + { + "name": "requestId", + "description": "Identifier of the network request to get content for.", + "$ref": "Network.RequestId" + }, + { + "name": "encoding", + "description": "The encoding to use.", + "type": "string", + "enum": [ + "webp", + "jpeg", + "png" + ] + }, + { + "name": "quality", + "description": "The quality of the encoding (0-1). (defaults to 1)", + "optional": true, + "type": "number" + }, + { + "name": "sizeOnly", + "description": "Whether to only return the size information (defaults to false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "body", + "description": "The encoded body as a base64 string. Omitted if sizeOnly is true.", + "optional": true, + "type": "binary" + }, + { + "name": "originalSize", + "description": "Size before re-encoding.", + "type": "integer" + }, + { + "name": "encodedSize", + "description": "Size after re-encoding.", + "type": "integer" + } + ] + } + ] + }, + { + "domain": "Browser", + "description": "The Browser domain defines methods and events for browser managing.", + "types": [ + { + "id": "WindowID", + "experimental": true, + "type": "integer" + }, + { + "id": "WindowState", + "description": "The state of the browser window.", + "experimental": true, + "type": "string", + "enum": [ + "normal", + "minimized", + "maximized", + "fullscreen" + ] + }, + { + "id": "Bounds", + "description": "Browser window bounds information", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "left", + "description": "The offset from the left edge of the screen to the window in pixels.", + "optional": true, + "type": "integer" + }, + { + "name": "top", + "description": "The offset from the top edge of the screen to the window in pixels.", + "optional": true, + "type": "integer" + }, + { + "name": "width", + "description": "The window width in pixels.", + "optional": true, + "type": "integer" + }, + { + "name": "height", + "description": "The window height in pixels.", + "optional": true, + "type": "integer" + }, + { + "name": "windowState", + "description": "The window state. Default to normal.", + "optional": true, + "$ref": "WindowState" + } + ] + }, + { + "id": "PermissionType", + "experimental": true, + "type": "string", + "enum": [ + "accessibilityEvents", + "audioCapture", + "backgroundSync", + "backgroundFetch", + "clipboardRead", + "clipboardWrite", + "durableStorage", + "flash", + "geolocation", + "midi", + "midiSysex", + "notifications", + "paymentHandler", + "protectedMediaIdentifier", + "sensors", + "videoCapture" + ] + }, + { + "id": "Bucket", + "description": "Chrome histogram bucket.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "low", + "description": "Minimum value (inclusive).", + "type": "integer" + }, + { + "name": "high", + "description": "Maximum value (exclusive).", + "type": "integer" + }, + { + "name": "count", + "description": "Number of samples.", + "type": "integer" + } + ] + }, + { + "id": "Histogram", + "description": "Chrome histogram.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "name", + "description": "Name.", + "type": "string" + }, + { + "name": "sum", + "description": "Sum of sample values.", + "type": "integer" + }, + { + "name": "count", + "description": "Total number of samples.", + "type": "integer" + }, + { + "name": "buckets", + "description": "Buckets.", + "type": "array", + "items": { + "$ref": "Bucket" + } + } + ] + } + ], + "commands": [ + { + "name": "grantPermissions", + "description": "Grant specific permissions to the given origin and reject all others.", + "experimental": true, + "parameters": [ + { + "name": "origin", + "type": "string" + }, + { + "name": "permissions", + "type": "array", + "items": { + "$ref": "PermissionType" + } + }, + { + "name": "browserContextId", + "description": "BrowserContext to override permissions. When omitted, default browser context is used.", + "optional": true, + "$ref": "Target.BrowserContextID" + } + ] + }, + { + "name": "resetPermissions", + "description": "Reset all permission management for all origins.", + "experimental": true, + "parameters": [ + { + "name": "browserContextId", + "description": "BrowserContext to reset permissions. When omitted, default browser context is used.", + "optional": true, + "$ref": "Target.BrowserContextID" + } + ] + }, + { + "name": "close", + "description": "Close browser gracefully." + }, + { + "name": "crash", + "description": "Crashes browser on the main thread.", + "experimental": true + }, + { + "name": "getVersion", + "description": "Returns version information.", + "returns": [ + { + "name": "protocolVersion", + "description": "Protocol version.", + "type": "string" + }, + { + "name": "product", + "description": "Product name.", + "type": "string" + }, + { + "name": "revision", + "description": "Product revision.", + "type": "string" + }, + { + "name": "userAgent", + "description": "User-Agent.", + "type": "string" + }, + { + "name": "jsVersion", + "description": "V8 version.", + "type": "string" + } + ] + }, + { + "name": "getBrowserCommandLine", + "description": "Returns the command line switches for the browser process if, and only if\n--enable-automation is on the commandline.", + "experimental": true, + "returns": [ + { + "name": "arguments", + "description": "Commandline parameters", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "getHistograms", + "description": "Get Chrome histograms.", + "experimental": true, + "parameters": [ + { + "name": "query", + "description": "Requested substring in name. Only histograms which have query as a\nsubstring in their name are extracted. An empty or absent query returns\nall histograms.", + "optional": true, + "type": "string" + }, + { + "name": "delta", + "description": "If true, retrieve delta since last call.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "histograms", + "description": "Histograms.", + "type": "array", + "items": { + "$ref": "Histogram" + } + } + ] + }, + { + "name": "getHistogram", + "description": "Get a Chrome histogram by name.", + "experimental": true, + "parameters": [ + { + "name": "name", + "description": "Requested histogram name.", + "type": "string" + }, + { + "name": "delta", + "description": "If true, retrieve delta since last call.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "histogram", + "description": "Histogram.", + "$ref": "Histogram" + } + ] + }, + { + "name": "getWindowBounds", + "description": "Get position and size of the browser window.", + "experimental": true, + "parameters": [ + { + "name": "windowId", + "description": "Browser window id.", + "$ref": "WindowID" + } + ], + "returns": [ + { + "name": "bounds", + "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.", + "$ref": "Bounds" + } + ] + }, + { + "name": "getWindowForTarget", + "description": "Get the browser window that contains the devtools target.", + "experimental": true, + "parameters": [ + { + "name": "targetId", + "description": "Devtools agent host id. If called as a part of the session, associated targetId is used.", + "optional": true, + "$ref": "Target.TargetID" + } + ], + "returns": [ + { + "name": "windowId", + "description": "Browser window id.", + "$ref": "WindowID" + }, + { + "name": "bounds", + "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.", + "$ref": "Bounds" + } + ] + }, + { + "name": "setWindowBounds", + "description": "Set position and/or size of the browser window.", + "experimental": true, + "parameters": [ + { + "name": "windowId", + "description": "Browser window id.", + "$ref": "WindowID" + }, + { + "name": "bounds", + "description": "New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined\nwith 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged.", + "$ref": "Bounds" + } + ] + }, + { + "name": "setDockTile", + "description": "Set dock tile details, platform-specific.", + "experimental": true, + "parameters": [ + { + "name": "badgeLabel", + "optional": true, + "type": "string" + }, + { + "name": "image", + "description": "Png encoded image.", + "optional": true, + "type": "binary" + } + ] + } + ] + }, + { + "domain": "CSS", + "description": "This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\nhave an associated `id` used in subsequent operations on the related object. Each object type has\na specific `id` structure, and those are not interchangeable between objects of different kinds.\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.", + "experimental": true, + "dependencies": [ + "DOM" + ], + "types": [ + { + "id": "StyleSheetId", + "type": "string" + }, + { + "id": "StyleSheetOrigin", + "description": "Stylesheet type: \"injected\" for stylesheets injected via extension, \"user-agent\" for user-agent\nstylesheets, \"inspector\" for stylesheets created by the inspector (i.e. those holding the \"via\ninspector\" rules), \"regular\" for regular stylesheets.", + "type": "string", + "enum": [ + "injected", + "user-agent", + "inspector", + "regular" + ] + }, + { + "id": "PseudoElementMatches", + "description": "CSS rule collection for a single pseudo style.", + "type": "object", + "properties": [ + { + "name": "pseudoType", + "description": "Pseudo element type.", + "$ref": "DOM.PseudoType" + }, + { + "name": "matches", + "description": "Matches of CSS rules applicable to the pseudo style.", + "type": "array", + "items": { + "$ref": "RuleMatch" + } + } + ] + }, + { + "id": "InheritedStyleEntry", + "description": "Inherited CSS rule collection from ancestor node.", + "type": "object", + "properties": [ + { + "name": "inlineStyle", + "description": "The ancestor node's inline style, if any, in the style inheritance chain.", + "optional": true, + "$ref": "CSSStyle" + }, + { + "name": "matchedCSSRules", + "description": "Matches of CSS rules matching the ancestor node in the style inheritance chain.", + "type": "array", + "items": { + "$ref": "RuleMatch" + } + } + ] + }, + { + "id": "RuleMatch", + "description": "Match data for a CSS rule.", + "type": "object", + "properties": [ + { + "name": "rule", + "description": "CSS rule in the match.", + "$ref": "CSSRule" + }, + { + "name": "matchingSelectors", + "description": "Matching selector indices in the rule's selectorList selectors (0-based).", + "type": "array", + "items": { + "type": "integer" + } + } + ] + }, + { + "id": "Value", + "description": "Data for a simple selector (these are delimited by commas in a selector list).", + "type": "object", + "properties": [ + { + "name": "text", + "description": "Value text.", + "type": "string" + }, + { + "name": "range", + "description": "Value range in the underlying resource (if available).", + "optional": true, + "$ref": "SourceRange" + } + ] + }, + { + "id": "SelectorList", + "description": "Selector list data.", + "type": "object", + "properties": [ + { + "name": "selectors", + "description": "Selectors in the list.", + "type": "array", + "items": { + "$ref": "Value" + } + }, + { + "name": "text", + "description": "Rule selector text.", + "type": "string" + } + ] + }, + { + "id": "CSSStyleSheetHeader", + "description": "CSS stylesheet metainformation.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The stylesheet identifier.", + "$ref": "StyleSheetId" + }, + { + "name": "frameId", + "description": "Owner frame identifier.", + "$ref": "Page.FrameId" + }, + { + "name": "sourceURL", + "description": "Stylesheet resource URL.", + "type": "string" + }, + { + "name": "sourceMapURL", + "description": "URL of source map associated with the stylesheet (if any).", + "optional": true, + "type": "string" + }, + { + "name": "origin", + "description": "Stylesheet origin.", + "$ref": "StyleSheetOrigin" + }, + { + "name": "title", + "description": "Stylesheet title.", + "type": "string" + }, + { + "name": "ownerNode", + "description": "The backend id for the owner node of the stylesheet.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "disabled", + "description": "Denotes whether the stylesheet is disabled.", + "type": "boolean" + }, + { + "name": "hasSourceURL", + "description": "Whether the sourceURL field value comes from the sourceURL comment.", + "optional": true, + "type": "boolean" + }, + { + "name": "isInline", + "description": "Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\ndocument.written STYLE tags.", + "type": "boolean" + }, + { + "name": "startLine", + "description": "Line offset of the stylesheet within the resource (zero based).", + "type": "number" + }, + { + "name": "startColumn", + "description": "Column offset of the stylesheet within the resource (zero based).", + "type": "number" + }, + { + "name": "length", + "description": "Size of the content (in characters).", + "type": "number" + } + ] + }, + { + "id": "CSSRule", + "description": "CSS rule representation.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.", + "optional": true, + "$ref": "StyleSheetId" + }, + { + "name": "selectorList", + "description": "Rule selector data.", + "$ref": "SelectorList" + }, + { + "name": "origin", + "description": "Parent stylesheet's origin.", + "$ref": "StyleSheetOrigin" + }, + { + "name": "style", + "description": "Associated style declaration.", + "$ref": "CSSStyle" + }, + { + "name": "media", + "description": "Media list array (for rules involving media queries). The array enumerates media queries\nstarting with the innermost one, going outwards.", + "optional": true, + "type": "array", + "items": { + "$ref": "CSSMedia" + } + } + ] + }, + { + "id": "RuleUsage", + "description": "CSS coverage information.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.", + "$ref": "StyleSheetId" + }, + { + "name": "startOffset", + "description": "Offset of the start of the rule (including selector) from the beginning of the stylesheet.", + "type": "number" + }, + { + "name": "endOffset", + "description": "Offset of the end of the rule body from the beginning of the stylesheet.", + "type": "number" + }, + { + "name": "used", + "description": "Indicates whether the rule was actually used by some element in the page.", + "type": "boolean" + } + ] + }, + { + "id": "SourceRange", + "description": "Text range within a resource. All numbers are zero-based.", + "type": "object", + "properties": [ + { + "name": "startLine", + "description": "Start line of range.", + "type": "integer" + }, + { + "name": "startColumn", + "description": "Start column of range (inclusive).", + "type": "integer" + }, + { + "name": "endLine", + "description": "End line of range", + "type": "integer" + }, + { + "name": "endColumn", + "description": "End column of range (exclusive).", + "type": "integer" + } + ] + }, + { + "id": "ShorthandEntry", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Shorthand name.", + "type": "string" + }, + { + "name": "value", + "description": "Shorthand value.", + "type": "string" + }, + { + "name": "important", + "description": "Whether the property has \"!important\" annotation (implies `false` if absent).", + "optional": true, + "type": "boolean" + } + ] + }, + { + "id": "CSSComputedStyleProperty", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Computed style property name.", + "type": "string" + }, + { + "name": "value", + "description": "Computed style property value.", + "type": "string" + } + ] + }, + { + "id": "CSSStyle", + "description": "CSS style representation.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.", + "optional": true, + "$ref": "StyleSheetId" + }, + { + "name": "cssProperties", + "description": "CSS properties in the style.", + "type": "array", + "items": { + "$ref": "CSSProperty" + } + }, + { + "name": "shorthandEntries", + "description": "Computed values for all shorthands found in the style.", + "type": "array", + "items": { + "$ref": "ShorthandEntry" + } + }, + { + "name": "cssText", + "description": "Style declaration text (if available).", + "optional": true, + "type": "string" + }, + { + "name": "range", + "description": "Style declaration range in the enclosing stylesheet (if available).", + "optional": true, + "$ref": "SourceRange" + } + ] + }, + { + "id": "CSSProperty", + "description": "CSS property declaration data.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "The property name.", + "type": "string" + }, + { + "name": "value", + "description": "The property value.", + "type": "string" + }, + { + "name": "important", + "description": "Whether the property has \"!important\" annotation (implies `false` if absent).", + "optional": true, + "type": "boolean" + }, + { + "name": "implicit", + "description": "Whether the property is implicit (implies `false` if absent).", + "optional": true, + "type": "boolean" + }, + { + "name": "text", + "description": "The full property text as specified in the style.", + "optional": true, + "type": "string" + }, + { + "name": "parsedOk", + "description": "Whether the property is understood by the browser (implies `true` if absent).", + "optional": true, + "type": "boolean" + }, + { + "name": "disabled", + "description": "Whether the property is disabled by the user (present for source-based properties only).", + "optional": true, + "type": "boolean" + }, + { + "name": "range", + "description": "The entire property range in the enclosing style declaration (if available).", + "optional": true, + "$ref": "SourceRange" + } + ] + }, + { + "id": "CSSMedia", + "description": "CSS media rule descriptor.", + "type": "object", + "properties": [ + { + "name": "text", + "description": "Media query text.", + "type": "string" + }, + { + "name": "source", + "description": "Source of the media query: \"mediaRule\" if specified by a @media rule, \"importRule\" if\nspecified by an @import rule, \"linkedSheet\" if specified by a \"media\" attribute in a linked\nstylesheet's LINK tag, \"inlineSheet\" if specified by a \"media\" attribute in an inline\nstylesheet's STYLE tag.", + "type": "string", + "enum": [ + "mediaRule", + "importRule", + "linkedSheet", + "inlineSheet" + ] + }, + { + "name": "sourceURL", + "description": "URL of the document containing the media query description.", + "optional": true, + "type": "string" + }, + { + "name": "range", + "description": "The associated rule (@media or @import) header range in the enclosing stylesheet (if\navailable).", + "optional": true, + "$ref": "SourceRange" + }, + { + "name": "styleSheetId", + "description": "Identifier of the stylesheet containing this object (if exists).", + "optional": true, + "$ref": "StyleSheetId" + }, + { + "name": "mediaList", + "description": "Array of media queries.", + "optional": true, + "type": "array", + "items": { + "$ref": "MediaQuery" + } + } + ] + }, + { + "id": "MediaQuery", + "description": "Media query descriptor.", + "type": "object", + "properties": [ + { + "name": "expressions", + "description": "Array of media query expressions.", + "type": "array", + "items": { + "$ref": "MediaQueryExpression" + } + }, + { + "name": "active", + "description": "Whether the media query condition is satisfied.", + "type": "boolean" + } + ] + }, + { + "id": "MediaQueryExpression", + "description": "Media query expression descriptor.", + "type": "object", + "properties": [ + { + "name": "value", + "description": "Media query expression value.", + "type": "number" + }, + { + "name": "unit", + "description": "Media query expression units.", + "type": "string" + }, + { + "name": "feature", + "description": "Media query expression feature.", + "type": "string" + }, + { + "name": "valueRange", + "description": "The associated range of the value text in the enclosing stylesheet (if available).", + "optional": true, + "$ref": "SourceRange" + }, + { + "name": "computedLength", + "description": "Computed length of media query expression (if applicable).", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "PlatformFontUsage", + "description": "Information about amount of glyphs that were rendered with given font.", + "type": "object", + "properties": [ + { + "name": "familyName", + "description": "Font's family name reported by platform.", + "type": "string" + }, + { + "name": "isCustomFont", + "description": "Indicates if the font was downloaded or resolved locally.", + "type": "boolean" + }, + { + "name": "glyphCount", + "description": "Amount of glyphs that were rendered with this font.", + "type": "number" + } + ] + }, + { + "id": "FontFace", + "description": "Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions", + "type": "object", + "properties": [ + { + "name": "fontFamily", + "description": "The font-family.", + "type": "string" + }, + { + "name": "fontStyle", + "description": "The font-style.", + "type": "string" + }, + { + "name": "fontVariant", + "description": "The font-variant.", + "type": "string" + }, + { + "name": "fontWeight", + "description": "The font-weight.", + "type": "string" + }, + { + "name": "fontStretch", + "description": "The font-stretch.", + "type": "string" + }, + { + "name": "unicodeRange", + "description": "The unicode-range.", + "type": "string" + }, + { + "name": "src", + "description": "The src.", + "type": "string" + }, + { + "name": "platformFontFamily", + "description": "The resolved platform font family", + "type": "string" + } + ] + }, + { + "id": "CSSKeyframesRule", + "description": "CSS keyframes rule representation.", + "type": "object", + "properties": [ + { + "name": "animationName", + "description": "Animation name.", + "$ref": "Value" + }, + { + "name": "keyframes", + "description": "List of keyframes.", + "type": "array", + "items": { + "$ref": "CSSKeyframeRule" + } + } + ] + }, + { + "id": "CSSKeyframeRule", + "description": "CSS keyframe rule representation.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.", + "optional": true, + "$ref": "StyleSheetId" + }, + { + "name": "origin", + "description": "Parent stylesheet's origin.", + "$ref": "StyleSheetOrigin" + }, + { + "name": "keyText", + "description": "Associated key text.", + "$ref": "Value" + }, + { + "name": "style", + "description": "Associated style declaration.", + "$ref": "CSSStyle" + } + ] + }, + { + "id": "StyleDeclarationEdit", + "description": "A descriptor of operation to mutate style declaration text.", + "type": "object", + "properties": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier.", + "$ref": "StyleSheetId" + }, + { + "name": "range", + "description": "The range of the style text in the enclosing stylesheet.", + "$ref": "SourceRange" + }, + { + "name": "text", + "description": "New style text.", + "type": "string" + } + ] + } + ], + "commands": [ + { + "name": "addRule", + "description": "Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\nposition specified by `location`.", + "parameters": [ + { + "name": "styleSheetId", + "description": "The css style sheet identifier where a new rule should be inserted.", + "$ref": "StyleSheetId" + }, + { + "name": "ruleText", + "description": "The text of a new rule.", + "type": "string" + }, + { + "name": "location", + "description": "Text position of a new rule in the target style sheet.", + "$ref": "SourceRange" + } + ], + "returns": [ + { + "name": "rule", + "description": "The newly created rule.", + "$ref": "CSSRule" + } + ] + }, + { + "name": "collectClassNames", + "description": "Returns all class names from specified stylesheet.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + } + ], + "returns": [ + { + "name": "classNames", + "description": "Class name list.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "createStyleSheet", + "description": "Creates a new special \"via-inspector\" stylesheet in the frame with given `frameId`.", + "parameters": [ + { + "name": "frameId", + "description": "Identifier of the frame where \"via-inspector\" stylesheet should be created.", + "$ref": "Page.FrameId" + } + ], + "returns": [ + { + "name": "styleSheetId", + "description": "Identifier of the created \"via-inspector\" stylesheet.", + "$ref": "StyleSheetId" + } + ] + }, + { + "name": "disable", + "description": "Disables the CSS agent for the given page." + }, + { + "name": "enable", + "description": "Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\nenabled until the result of this command is received." + }, + { + "name": "forcePseudoState", + "description": "Ensures that the given node will have specified pseudo-classes whenever its style is computed by\nthe browser.", + "parameters": [ + { + "name": "nodeId", + "description": "The element id for which to force the pseudo state.", + "$ref": "DOM.NodeId" + }, + { + "name": "forcedPseudoClasses", + "description": "Element pseudo classes to force when computing the element's style.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "getBackgroundColors", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to get background colors for.", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "backgroundColors", + "description": "The range of background colors behind this element, if it contains any visible text. If no\nvisible text is present, this will be undefined. In the case of a flat background color,\nthis will consist of simply that color. In the case of a gradient, this will consist of each\nof the color stops. For anything more complicated, this will be an empty array. Images will\nbe ignored (as if the image had failed to load).", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "computedFontSize", + "description": "The computed font size for this node, as a CSS computed value string (e.g. '12px').", + "optional": true, + "type": "string" + }, + { + "name": "computedFontWeight", + "description": "The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or\n'100').", + "optional": true, + "type": "string" + }, + { + "name": "computedBodyFontSize", + "description": "The computed font size for the document body, as a computed CSS value string (e.g. '16px').", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "getComputedStyleForNode", + "description": "Returns the computed style for a DOM node identified by `nodeId`.", + "parameters": [ + { + "name": "nodeId", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "computedStyle", + "description": "Computed style for the specified DOM node.", + "type": "array", + "items": { + "$ref": "CSSComputedStyleProperty" + } + } + ] + }, + { + "name": "getInlineStylesForNode", + "description": "Returns the styles defined inline (explicitly in the \"style\" attribute and implicitly, using DOM\nattributes) for a DOM node identified by `nodeId`.", + "parameters": [ + { + "name": "nodeId", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "inlineStyle", + "description": "Inline style for the specified DOM node.", + "optional": true, + "$ref": "CSSStyle" + }, + { + "name": "attributesStyle", + "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").", + "optional": true, + "$ref": "CSSStyle" + } + ] + }, + { + "name": "getMatchedStylesForNode", + "description": "Returns requested styles for a DOM node identified by `nodeId`.", + "parameters": [ + { + "name": "nodeId", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "inlineStyle", + "description": "Inline style for the specified DOM node.", + "optional": true, + "$ref": "CSSStyle" + }, + { + "name": "attributesStyle", + "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").", + "optional": true, + "$ref": "CSSStyle" + }, + { + "name": "matchedCSSRules", + "description": "CSS rules matching this node, from all applicable stylesheets.", + "optional": true, + "type": "array", + "items": { + "$ref": "RuleMatch" + } + }, + { + "name": "pseudoElements", + "description": "Pseudo style matches for this node.", + "optional": true, + "type": "array", + "items": { + "$ref": "PseudoElementMatches" + } + }, + { + "name": "inherited", + "description": "A chain of inherited styles (from the immediate node parent up to the DOM tree root).", + "optional": true, + "type": "array", + "items": { + "$ref": "InheritedStyleEntry" + } + }, + { + "name": "cssKeyframesRules", + "description": "A list of CSS keyframed animations matching this node.", + "optional": true, + "type": "array", + "items": { + "$ref": "CSSKeyframesRule" + } + } + ] + }, + { + "name": "getMediaQueries", + "description": "Returns all media queries parsed by the rendering engine.", + "returns": [ + { + "name": "medias", + "type": "array", + "items": { + "$ref": "CSSMedia" + } + } + ] + }, + { + "name": "getPlatformFontsForNode", + "description": "Requests information about platform fonts which we used to render child TextNodes in the given\nnode.", + "parameters": [ + { + "name": "nodeId", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "fonts", + "description": "Usage statistics for every employed platform font.", + "type": "array", + "items": { + "$ref": "PlatformFontUsage" + } + } + ] + }, + { + "name": "getStyleSheetText", + "description": "Returns the current textual content for a stylesheet.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + } + ], + "returns": [ + { + "name": "text", + "description": "The stylesheet text.", + "type": "string" + } + ] + }, + { + "name": "setEffectivePropertyValueForNode", + "description": "Find a rule with the given active property for the given node and set the new value for this\nproperty", + "parameters": [ + { + "name": "nodeId", + "description": "The element id for which to set property.", + "$ref": "DOM.NodeId" + }, + { + "name": "propertyName", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + }, + { + "name": "setKeyframeKey", + "description": "Modifies the keyframe rule key text.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + }, + { + "name": "range", + "$ref": "SourceRange" + }, + { + "name": "keyText", + "type": "string" + } + ], + "returns": [ + { + "name": "keyText", + "description": "The resulting key text after modification.", + "$ref": "Value" + } + ] + }, + { + "name": "setMediaText", + "description": "Modifies the rule selector.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + }, + { + "name": "range", + "$ref": "SourceRange" + }, + { + "name": "text", + "type": "string" + } + ], + "returns": [ + { + "name": "media", + "description": "The resulting CSS media rule after modification.", + "$ref": "CSSMedia" + } + ] + }, + { + "name": "setRuleSelector", + "description": "Modifies the rule selector.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + }, + { + "name": "range", + "$ref": "SourceRange" + }, + { + "name": "selector", + "type": "string" + } + ], + "returns": [ + { + "name": "selectorList", + "description": "The resulting selector list after modification.", + "$ref": "SelectorList" + } + ] + }, + { + "name": "setStyleSheetText", + "description": "Sets the new stylesheet text.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + }, + { + "name": "text", + "type": "string" + } + ], + "returns": [ + { + "name": "sourceMapURL", + "description": "URL of source map associated with script (if any).", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "setStyleTexts", + "description": "Applies specified style edits one after another in the given order.", + "parameters": [ + { + "name": "edits", + "type": "array", + "items": { + "$ref": "StyleDeclarationEdit" + } + } + ], + "returns": [ + { + "name": "styles", + "description": "The resulting styles after modification.", + "type": "array", + "items": { + "$ref": "CSSStyle" + } + } + ] + }, + { + "name": "startRuleUsageTracking", + "description": "Enables the selector recording." + }, + { + "name": "stopRuleUsageTracking", + "description": "Stop tracking rule usage and return the list of rules that were used since last call to\n`takeCoverageDelta` (or since start of coverage instrumentation)", + "returns": [ + { + "name": "ruleUsage", + "type": "array", + "items": { + "$ref": "RuleUsage" + } + } + ] + }, + { + "name": "takeCoverageDelta", + "description": "Obtain list of rules that became used since last call to this method (or since start of coverage\ninstrumentation)", + "returns": [ + { + "name": "coverage", + "type": "array", + "items": { + "$ref": "RuleUsage" + } + } + ] + } + ], + "events": [ + { + "name": "fontsUpdated", + "description": "Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\nweb font", + "parameters": [ + { + "name": "font", + "description": "The web font that has loaded.", + "optional": true, + "$ref": "FontFace" + } + ] + }, + { + "name": "mediaQueryResultChanged", + "description": "Fires whenever a MediaQuery result changes (for example, after a browser window has been\nresized.) The current implementation considers only viewport-dependent media features." + }, + { + "name": "styleSheetAdded", + "description": "Fired whenever an active document stylesheet is added.", + "parameters": [ + { + "name": "header", + "description": "Added stylesheet metainfo.", + "$ref": "CSSStyleSheetHeader" + } + ] + }, + { + "name": "styleSheetChanged", + "description": "Fired whenever a stylesheet is changed as a result of the client operation.", + "parameters": [ + { + "name": "styleSheetId", + "$ref": "StyleSheetId" + } + ] + }, + { + "name": "styleSheetRemoved", + "description": "Fired whenever an active document stylesheet is removed.", + "parameters": [ + { + "name": "styleSheetId", + "description": "Identifier of the removed stylesheet.", + "$ref": "StyleSheetId" + } + ] + } + ] + }, + { + "domain": "CacheStorage", + "experimental": true, + "types": [ + { + "id": "CacheId", + "description": "Unique identifier of the Cache object.", + "type": "string" + }, + { + "id": "CachedResponseType", + "description": "type of HTTP response cached", + "type": "string", + "enum": [ + "basic", + "cors", + "default", + "error", + "opaqueResponse", + "opaqueRedirect" + ] + }, + { + "id": "DataEntry", + "description": "Data entry.", + "type": "object", + "properties": [ + { + "name": "requestURL", + "description": "Request URL.", + "type": "string" + }, + { + "name": "requestMethod", + "description": "Request method.", + "type": "string" + }, + { + "name": "requestHeaders", + "description": "Request headers", + "type": "array", + "items": { + "$ref": "Header" + } + }, + { + "name": "responseTime", + "description": "Number of seconds since epoch.", + "type": "number" + }, + { + "name": "responseStatus", + "description": "HTTP response status code.", + "type": "integer" + }, + { + "name": "responseStatusText", + "description": "HTTP response status text.", + "type": "string" + }, + { + "name": "responseType", + "description": "HTTP response type", + "$ref": "CachedResponseType" + }, + { + "name": "responseHeaders", + "description": "Response headers", + "type": "array", + "items": { + "$ref": "Header" + } + } + ] + }, + { + "id": "Cache", + "description": "Cache identifier.", + "type": "object", + "properties": [ + { + "name": "cacheId", + "description": "An opaque unique id of the cache.", + "$ref": "CacheId" + }, + { + "name": "securityOrigin", + "description": "Security origin of the cache.", + "type": "string" + }, + { + "name": "cacheName", + "description": "The name of the cache.", + "type": "string" + } + ] + }, + { + "id": "Header", + "type": "object", + "properties": [ + { + "name": "name", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + }, + { + "id": "CachedResponse", + "description": "Cached response", + "type": "object", + "properties": [ + { + "name": "body", + "description": "Entry content, base64-encoded.", + "type": "binary" + } + ] + } + ], + "commands": [ + { + "name": "deleteCache", + "description": "Deletes a cache.", + "parameters": [ + { + "name": "cacheId", + "description": "Id of cache for deletion.", + "$ref": "CacheId" + } + ] + }, + { + "name": "deleteEntry", + "description": "Deletes a cache entry.", + "parameters": [ + { + "name": "cacheId", + "description": "Id of cache where the entry will be deleted.", + "$ref": "CacheId" + }, + { + "name": "request", + "description": "URL spec of the request.", + "type": "string" + } + ] + }, + { + "name": "requestCacheNames", + "description": "Requests cache names.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + } + ], + "returns": [ + { + "name": "caches", + "description": "Caches for the security origin.", + "type": "array", + "items": { + "$ref": "Cache" + } + } + ] + }, + { + "name": "requestCachedResponse", + "description": "Fetches cache entry.", + "parameters": [ + { + "name": "cacheId", + "description": "Id of cache that contains the enty.", + "$ref": "CacheId" + }, + { + "name": "requestURL", + "description": "URL spec of the request.", + "type": "string" + } + ], + "returns": [ + { + "name": "response", + "description": "Response read from the cache.", + "$ref": "CachedResponse" + } + ] + }, + { + "name": "requestEntries", + "description": "Requests data from cache.", + "parameters": [ + { + "name": "cacheId", + "description": "ID of cache to get entries from.", + "$ref": "CacheId" + }, + { + "name": "skipCount", + "description": "Number of records to skip.", + "type": "integer" + }, + { + "name": "pageSize", + "description": "Number of records to fetch.", + "type": "integer" + } + ], + "returns": [ + { + "name": "cacheDataEntries", + "description": "Array of object store data entries.", + "type": "array", + "items": { + "$ref": "DataEntry" + } + }, + { + "name": "hasMore", + "description": "If true, there are more entries to fetch in the given range.", + "type": "boolean" + } + ] + } + ] + }, + { + "domain": "DOM", + "description": "This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\nand never sends the same node twice. It is client's responsibility to collect information about\nthe nodes that were sent to the client.<p>Note that `iframe` owner elements will return\ncorresponding document elements as their child nodes.</p>", + "dependencies": [ + "Runtime" + ], + "types": [ + { + "id": "NodeId", + "description": "Unique DOM node identifier.", + "type": "integer" + }, + { + "id": "BackendNodeId", + "description": "Unique DOM node identifier used to reference a node that may not have been pushed to the\nfront-end.", + "type": "integer" + }, + { + "id": "BackendNode", + "description": "Backend node with a friendly name.", + "type": "object", + "properties": [ + { + "name": "nodeType", + "description": "`Node`'s nodeType.", + "type": "integer" + }, + { + "name": "nodeName", + "description": "`Node`'s nodeName.", + "type": "string" + }, + { + "name": "backendNodeId", + "$ref": "BackendNodeId" + } + ] + }, + { + "id": "PseudoType", + "description": "Pseudo element type.", + "type": "string", + "enum": [ + "first-line", + "first-letter", + "before", + "after", + "backdrop", + "selection", + "first-line-inherited", + "scrollbar", + "scrollbar-thumb", + "scrollbar-button", + "scrollbar-track", + "scrollbar-track-piece", + "scrollbar-corner", + "resizer", + "input-list-button" + ] + }, + { + "id": "ShadowRootType", + "description": "Shadow root type.", + "type": "string", + "enum": [ + "user-agent", + "open", + "closed" + ] + }, + { + "id": "Node", + "description": "DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\nDOMNode is a base node mirror type.", + "type": "object", + "properties": [ + { + "name": "nodeId", + "description": "Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\nwill only push node with given `id` once. It is aware of all requested nodes and will only\nfire DOM events for nodes known to the client.", + "$ref": "NodeId" + }, + { + "name": "parentId", + "description": "The id of the parent node if any.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "The BackendNodeId for this node.", + "$ref": "BackendNodeId" + }, + { + "name": "nodeType", + "description": "`Node`'s nodeType.", + "type": "integer" + }, + { + "name": "nodeName", + "description": "`Node`'s nodeName.", + "type": "string" + }, + { + "name": "localName", + "description": "`Node`'s localName.", + "type": "string" + }, + { + "name": "nodeValue", + "description": "`Node`'s nodeValue.", + "type": "string" + }, + { + "name": "childNodeCount", + "description": "Child count for `Container` nodes.", + "optional": true, + "type": "integer" + }, + { + "name": "children", + "description": "Child nodes of this node when requested with children.", + "optional": true, + "type": "array", + "items": { + "$ref": "Node" + } + }, + { + "name": "attributes", + "description": "Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "documentURL", + "description": "Document URL that `Document` or `FrameOwner` node points to.", + "optional": true, + "type": "string" + }, + { + "name": "baseURL", + "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.", + "optional": true, + "type": "string" + }, + { + "name": "publicId", + "description": "`DocumentType`'s publicId.", + "optional": true, + "type": "string" + }, + { + "name": "systemId", + "description": "`DocumentType`'s systemId.", + "optional": true, + "type": "string" + }, + { + "name": "internalSubset", + "description": "`DocumentType`'s internalSubset.", + "optional": true, + "type": "string" + }, + { + "name": "xmlVersion", + "description": "`Document`'s XML version in case of XML documents.", + "optional": true, + "type": "string" + }, + { + "name": "name", + "description": "`Attr`'s name.", + "optional": true, + "type": "string" + }, + { + "name": "value", + "description": "`Attr`'s value.", + "optional": true, + "type": "string" + }, + { + "name": "pseudoType", + "description": "Pseudo element type for this node.", + "optional": true, + "$ref": "PseudoType" + }, + { + "name": "shadowRootType", + "description": "Shadow root type.", + "optional": true, + "$ref": "ShadowRootType" + }, + { + "name": "frameId", + "description": "Frame ID for frame owner elements.", + "optional": true, + "$ref": "Page.FrameId" + }, + { + "name": "contentDocument", + "description": "Content document for frame owner elements.", + "optional": true, + "$ref": "Node" + }, + { + "name": "shadowRoots", + "description": "Shadow root list for given element host.", + "optional": true, + "type": "array", + "items": { + "$ref": "Node" + } + }, + { + "name": "templateContent", + "description": "Content document fragment for template elements.", + "optional": true, + "$ref": "Node" + }, + { + "name": "pseudoElements", + "description": "Pseudo elements associated with this node.", + "optional": true, + "type": "array", + "items": { + "$ref": "Node" + } + }, + { + "name": "importedDocument", + "description": "Import document for the HTMLImport links.", + "optional": true, + "$ref": "Node" + }, + { + "name": "distributedNodes", + "description": "Distributed nodes for given insertion point.", + "optional": true, + "type": "array", + "items": { + "$ref": "BackendNode" + } + }, + { + "name": "isSVG", + "description": "Whether the node is SVG.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "id": "RGBA", + "description": "A structure holding an RGBA color.", + "type": "object", + "properties": [ + { + "name": "r", + "description": "The red component, in the [0-255] range.", + "type": "integer" + }, + { + "name": "g", + "description": "The green component, in the [0-255] range.", + "type": "integer" + }, + { + "name": "b", + "description": "The blue component, in the [0-255] range.", + "type": "integer" + }, + { + "name": "a", + "description": "The alpha component, in the [0-1] range (default: 1).", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "Quad", + "description": "An array of quad vertices, x immediately followed by y for each point, points clock-wise.", + "type": "array", + "items": { + "type": "number" + } + }, + { + "id": "BoxModel", + "description": "Box model.", + "type": "object", + "properties": [ + { + "name": "content", + "description": "Content box", + "$ref": "Quad" + }, + { + "name": "padding", + "description": "Padding box", + "$ref": "Quad" + }, + { + "name": "border", + "description": "Border box", + "$ref": "Quad" + }, + { + "name": "margin", + "description": "Margin box", + "$ref": "Quad" + }, + { + "name": "width", + "description": "Node width", + "type": "integer" + }, + { + "name": "height", + "description": "Node height", + "type": "integer" + }, + { + "name": "shapeOutside", + "description": "Shape outside coordinates", + "optional": true, + "$ref": "ShapeOutsideInfo" + } + ] + }, + { + "id": "ShapeOutsideInfo", + "description": "CSS Shape Outside details.", + "type": "object", + "properties": [ + { + "name": "bounds", + "description": "Shape bounds", + "$ref": "Quad" + }, + { + "name": "shape", + "description": "Shape coordinate details", + "type": "array", + "items": { + "type": "any" + } + }, + { + "name": "marginShape", + "description": "Margin shape bounds", + "type": "array", + "items": { + "type": "any" + } + } + ] + }, + { + "id": "Rect", + "description": "Rectangle.", + "type": "object", + "properties": [ + { + "name": "x", + "description": "X coordinate", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate", + "type": "number" + }, + { + "name": "width", + "description": "Rectangle width", + "type": "number" + }, + { + "name": "height", + "description": "Rectangle height", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "collectClassNamesFromSubtree", + "description": "Collects class names for the node with given id and all of it's child nodes.", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to collect class names.", + "$ref": "NodeId" + } + ], + "returns": [ + { + "name": "classNames", + "description": "Class name list.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "copyTo", + "description": "Creates a deep copy of the specified node and places it into the target container before the\ngiven anchor.", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to copy.", + "$ref": "NodeId" + }, + { + "name": "targetNodeId", + "description": "Id of the element to drop the copy into.", + "$ref": "NodeId" + }, + { + "name": "insertBeforeNodeId", + "description": "Drop the copy before this node (if absent, the copy becomes the last child of\n`targetNodeId`).", + "optional": true, + "$ref": "NodeId" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "Id of the node clone.", + "$ref": "NodeId" + } + ] + }, + { + "name": "describeNode", + "description": "Describes node given its id, does not require domain to be enabled. Does not start tracking any\nobjects, can be used for automation.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + }, + { + "name": "depth", + "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.", + "optional": true, + "type": "integer" + }, + { + "name": "pierce", + "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "node", + "description": "Node description.", + "$ref": "Node" + } + ] + }, + { + "name": "disable", + "description": "Disables DOM agent for the given page." + }, + { + "name": "discardSearchResults", + "description": "Discards search results from the session with the given id. `getSearchResults` should no longer\nbe called for that search.", + "experimental": true, + "parameters": [ + { + "name": "searchId", + "description": "Unique search session identifier.", + "type": "string" + } + ] + }, + { + "name": "enable", + "description": "Enables DOM agent for the given page." + }, + { + "name": "focus", + "description": "Focuses the given element.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ] + }, + { + "name": "getAttributes", + "description": "Returns attributes for the specified node.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to retrieve attibutes for.", + "$ref": "NodeId" + } + ], + "returns": [ + { + "name": "attributes", + "description": "An interleaved array of node attribute names and values.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "getBoxModel", + "description": "Returns boxes for the given node.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ], + "returns": [ + { + "name": "model", + "description": "Box model for the node.", + "$ref": "BoxModel" + } + ] + }, + { + "name": "getContentQuads", + "description": "Returns quads that describe node position on the page. This method\nmight return multiple quads for inline nodes.", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ], + "returns": [ + { + "name": "quads", + "description": "Quads that describe node layout relative to viewport.", + "type": "array", + "items": { + "$ref": "Quad" + } + } + ] + }, + { + "name": "getDocument", + "description": "Returns the root DOM node (and optionally the subtree) to the caller.", + "parameters": [ + { + "name": "depth", + "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.", + "optional": true, + "type": "integer" + }, + { + "name": "pierce", + "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "root", + "description": "Resulting node.", + "$ref": "Node" + } + ] + }, + { + "name": "getFlattenedDocument", + "description": "Returns the root DOM node (and optionally the subtree) to the caller.", + "parameters": [ + { + "name": "depth", + "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.", + "optional": true, + "type": "integer" + }, + { + "name": "pierce", + "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "nodes", + "description": "Resulting node.", + "type": "array", + "items": { + "$ref": "Node" + } + } + ] + }, + { + "name": "getNodeForLocation", + "description": "Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\neither returned or not.", + "experimental": true, + "parameters": [ + { + "name": "x", + "description": "X coordinate.", + "type": "integer" + }, + { + "name": "y", + "description": "Y coordinate.", + "type": "integer" + }, + { + "name": "includeUserAgentShadowDOM", + "description": "False to skip to the nearest non-UA shadow root ancestor (default: false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "backendNodeId", + "description": "Resulting node.", + "$ref": "BackendNodeId" + }, + { + "name": "nodeId", + "description": "Id of the node at given coordinates, only when enabled.", + "optional": true, + "$ref": "NodeId" + } + ] + }, + { + "name": "getOuterHTML", + "description": "Returns node's HTML markup.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ], + "returns": [ + { + "name": "outerHTML", + "description": "Outer HTML markup.", + "type": "string" + } + ] + }, + { + "name": "getRelayoutBoundary", + "description": "Returns the id of the nearest ancestor that is a relayout boundary.", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node.", + "$ref": "NodeId" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "Relayout boundary node id for the given node.", + "$ref": "NodeId" + } + ] + }, + { + "name": "getSearchResults", + "description": "Returns search results from given `fromIndex` to given `toIndex` from the search with the given\nidentifier.", + "experimental": true, + "parameters": [ + { + "name": "searchId", + "description": "Unique search session identifier.", + "type": "string" + }, + { + "name": "fromIndex", + "description": "Start index of the search result to be returned.", + "type": "integer" + }, + { + "name": "toIndex", + "description": "End index of the search result to be returned.", + "type": "integer" + } + ], + "returns": [ + { + "name": "nodeIds", + "description": "Ids of the search result nodes.", + "type": "array", + "items": { + "$ref": "NodeId" + } + } + ] + }, + { + "name": "hideHighlight", + "description": "Hides any highlight.", + "redirect": "Overlay" + }, + { + "name": "highlightNode", + "description": "Highlights DOM node.", + "redirect": "Overlay" + }, + { + "name": "highlightRect", + "description": "Highlights given rectangle.", + "redirect": "Overlay" + }, + { + "name": "markUndoableState", + "description": "Marks last undoable state.", + "experimental": true + }, + { + "name": "moveTo", + "description": "Moves node into the new container, places it before the given anchor.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to move.", + "$ref": "NodeId" + }, + { + "name": "targetNodeId", + "description": "Id of the element to drop the moved node into.", + "$ref": "NodeId" + }, + { + "name": "insertBeforeNodeId", + "description": "Drop node before this one (if absent, the moved node becomes the last child of\n`targetNodeId`).", + "optional": true, + "$ref": "NodeId" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "New id of the moved node.", + "$ref": "NodeId" + } + ] + }, + { + "name": "performSearch", + "description": "Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\n`cancelSearch` to end this search session.", + "experimental": true, + "parameters": [ + { + "name": "query", + "description": "Plain text or query selector or XPath search query.", + "type": "string" + }, + { + "name": "includeUserAgentShadowDOM", + "description": "True to search in user agent shadow DOM.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "searchId", + "description": "Unique search session identifier.", + "type": "string" + }, + { + "name": "resultCount", + "description": "Number of search results.", + "type": "integer" + } + ] + }, + { + "name": "pushNodeByPathToFrontend", + "description": "Requests that the node is sent to the caller given its path. // FIXME, use XPath", + "experimental": true, + "parameters": [ + { + "name": "path", + "description": "Path to node in the proprietary format.", + "type": "string" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "Id of the node for given path.", + "$ref": "NodeId" + } + ] + }, + { + "name": "pushNodesByBackendIdsToFrontend", + "description": "Requests that a batch of nodes is sent to the caller given their backend node ids.", + "experimental": true, + "parameters": [ + { + "name": "backendNodeIds", + "description": "The array of backend node ids.", + "type": "array", + "items": { + "$ref": "BackendNodeId" + } + } + ], + "returns": [ + { + "name": "nodeIds", + "description": "The array of ids of pushed nodes that correspond to the backend ids specified in\nbackendNodeIds.", + "type": "array", + "items": { + "$ref": "NodeId" + } + } + ] + }, + { + "name": "querySelector", + "description": "Executes `querySelector` on a given node.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to query upon.", + "$ref": "NodeId" + }, + { + "name": "selector", + "description": "Selector string.", + "type": "string" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "Query selector result.", + "$ref": "NodeId" + } + ] + }, + { + "name": "querySelectorAll", + "description": "Executes `querySelectorAll` on a given node.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to query upon.", + "$ref": "NodeId" + }, + { + "name": "selector", + "description": "Selector string.", + "type": "string" + } + ], + "returns": [ + { + "name": "nodeIds", + "description": "Query selector result.", + "type": "array", + "items": { + "$ref": "NodeId" + } + } + ] + }, + { + "name": "redo", + "description": "Re-does the last undone action.", + "experimental": true + }, + { + "name": "removeAttribute", + "description": "Removes attribute with given name from an element with given id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the element to remove attribute from.", + "$ref": "NodeId" + }, + { + "name": "name", + "description": "Name of the attribute to remove.", + "type": "string" + } + ] + }, + { + "name": "removeNode", + "description": "Removes node with given id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to remove.", + "$ref": "NodeId" + } + ] + }, + { + "name": "requestChildNodes", + "description": "Requests that children of the node with given id are returned to the caller in form of\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\nthe specified depth.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to get children for.", + "$ref": "NodeId" + }, + { + "name": "depth", + "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.", + "optional": true, + "type": "integer" + }, + { + "name": "pierce", + "description": "Whether or not iframes and shadow roots should be traversed when returning the sub-tree\n(default is false).", + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "requestNode", + "description": "Requests that the node is sent to the caller given the JavaScript node object reference. All\nnodes that form the path from the node to the root are also sent to the client as a series of\n`setChildNodes` notifications.", + "parameters": [ + { + "name": "objectId", + "description": "JavaScript object id to convert into node.", + "$ref": "Runtime.RemoteObjectId" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "Node id for given object.", + "$ref": "NodeId" + } + ] + }, + { + "name": "resolveNode", + "description": "Resolves the JavaScript node object for a given NodeId or BackendNodeId.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to resolve.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Backend identifier of the node to resolve.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "objectGroup", + "description": "Symbolic group name that can be used to release multiple objects.", + "optional": true, + "type": "string" + } + ], + "returns": [ + { + "name": "object", + "description": "JavaScript object wrapper for given node.", + "$ref": "Runtime.RemoteObject" + } + ] + }, + { + "name": "setAttributeValue", + "description": "Sets attribute for an element with given id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the element to set attribute for.", + "$ref": "NodeId" + }, + { + "name": "name", + "description": "Attribute name.", + "type": "string" + }, + { + "name": "value", + "description": "Attribute value.", + "type": "string" + } + ] + }, + { + "name": "setAttributesAsText", + "description": "Sets attributes on element with given id. This method is useful when user edits some existing\nattribute value and types in several attribute name/value pairs.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the element to set attributes for.", + "$ref": "NodeId" + }, + { + "name": "text", + "description": "Text with a number of attributes. Will parse this text using HTML parser.", + "type": "string" + }, + { + "name": "name", + "description": "Attribute name to replace with new attributes derived from text in case text parsed\nsuccessfully.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "setFileInputFiles", + "description": "Sets files for the given file input element.", + "parameters": [ + { + "name": "files", + "description": "Array of file paths to set.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "nodeId", + "description": "Identifier of the node.", + "optional": true, + "$ref": "NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node.", + "optional": true, + "$ref": "BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node wrapper.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ] + }, + { + "name": "setInspectedNode", + "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions).", + "experimental": true, + "parameters": [ + { + "name": "nodeId", + "description": "DOM node id to be accessible by means of $x command line API.", + "$ref": "NodeId" + } + ] + }, + { + "name": "setNodeName", + "description": "Sets node name for a node with given id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to set name for.", + "$ref": "NodeId" + }, + { + "name": "name", + "description": "New node's name.", + "type": "string" + } + ], + "returns": [ + { + "name": "nodeId", + "description": "New node's id.", + "$ref": "NodeId" + } + ] + }, + { + "name": "setNodeValue", + "description": "Sets node value for a node with given id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to set value for.", + "$ref": "NodeId" + }, + { + "name": "value", + "description": "New node's value.", + "type": "string" + } + ] + }, + { + "name": "setOuterHTML", + "description": "Sets node HTML markup, returns new node id.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to set markup for.", + "$ref": "NodeId" + }, + { + "name": "outerHTML", + "description": "Outer HTML markup to set.", + "type": "string" + } + ] + }, + { + "name": "undo", + "description": "Undoes the last performed action.", + "experimental": true + }, + { + "name": "getFrameOwner", + "description": "Returns iframe node that owns iframe with the given domain.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "$ref": "Page.FrameId" + } + ], + "returns": [ + { + "name": "backendNodeId", + "description": "Resulting node.", + "$ref": "BackendNodeId" + }, + { + "name": "nodeId", + "description": "Id of the node at given coordinates, only when enabled.", + "optional": true, + "$ref": "NodeId" + } + ] + } + ], + "events": [ + { + "name": "attributeModified", + "description": "Fired when `Element`'s attribute is modified.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node that has changed.", + "$ref": "NodeId" + }, + { + "name": "name", + "description": "Attribute name.", + "type": "string" + }, + { + "name": "value", + "description": "Attribute value.", + "type": "string" + } + ] + }, + { + "name": "attributeRemoved", + "description": "Fired when `Element`'s attribute is removed.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node that has changed.", + "$ref": "NodeId" + }, + { + "name": "name", + "description": "A ttribute name.", + "type": "string" + } + ] + }, + { + "name": "characterDataModified", + "description": "Mirrors `DOMCharacterDataModified` event.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node that has changed.", + "$ref": "NodeId" + }, + { + "name": "characterData", + "description": "New text value.", + "type": "string" + } + ] + }, + { + "name": "childNodeCountUpdated", + "description": "Fired when `Container`'s child node count has changed.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node that has changed.", + "$ref": "NodeId" + }, + { + "name": "childNodeCount", + "description": "New node count.", + "type": "integer" + } + ] + }, + { + "name": "childNodeInserted", + "description": "Mirrors `DOMNodeInserted` event.", + "parameters": [ + { + "name": "parentNodeId", + "description": "Id of the node that has changed.", + "$ref": "NodeId" + }, + { + "name": "previousNodeId", + "description": "If of the previous siblint.", + "$ref": "NodeId" + }, + { + "name": "node", + "description": "Inserted node data.", + "$ref": "Node" + } + ] + }, + { + "name": "childNodeRemoved", + "description": "Mirrors `DOMNodeRemoved` event.", + "parameters": [ + { + "name": "parentNodeId", + "description": "Parent id.", + "$ref": "NodeId" + }, + { + "name": "nodeId", + "description": "Id of the node that has been removed.", + "$ref": "NodeId" + } + ] + }, + { + "name": "distributedNodesUpdated", + "description": "Called when distrubution is changed.", + "experimental": true, + "parameters": [ + { + "name": "insertionPointId", + "description": "Insertion point where distrubuted nodes were updated.", + "$ref": "NodeId" + }, + { + "name": "distributedNodes", + "description": "Distributed nodes for given insertion point.", + "type": "array", + "items": { + "$ref": "BackendNode" + } + } + ] + }, + { + "name": "documentUpdated", + "description": "Fired when `Document` has been totally updated. Node ids are no longer valid." + }, + { + "name": "inlineStyleInvalidated", + "description": "Fired when `Element`'s inline style is modified via a CSS property modification.", + "experimental": true, + "parameters": [ + { + "name": "nodeIds", + "description": "Ids of the nodes for which the inline styles have been invalidated.", + "type": "array", + "items": { + "$ref": "NodeId" + } + } + ] + }, + { + "name": "pseudoElementAdded", + "description": "Called when a pseudo element is added to an element.", + "experimental": true, + "parameters": [ + { + "name": "parentId", + "description": "Pseudo element's parent element id.", + "$ref": "NodeId" + }, + { + "name": "pseudoElement", + "description": "The added pseudo element.", + "$ref": "Node" + } + ] + }, + { + "name": "pseudoElementRemoved", + "description": "Called when a pseudo element is removed from an element.", + "experimental": true, + "parameters": [ + { + "name": "parentId", + "description": "Pseudo element's parent element id.", + "$ref": "NodeId" + }, + { + "name": "pseudoElementId", + "description": "The removed pseudo element id.", + "$ref": "NodeId" + } + ] + }, + { + "name": "setChildNodes", + "description": "Fired when backend wants to provide client with the missing DOM structure. This happens upon\nmost of the calls requesting node ids.", + "parameters": [ + { + "name": "parentId", + "description": "Parent node id to populate with children.", + "$ref": "NodeId" + }, + { + "name": "nodes", + "description": "Child nodes array.", + "type": "array", + "items": { + "$ref": "Node" + } + } + ] + }, + { + "name": "shadowRootPopped", + "description": "Called when shadow root is popped from the element.", + "experimental": true, + "parameters": [ + { + "name": "hostId", + "description": "Host element id.", + "$ref": "NodeId" + }, + { + "name": "rootId", + "description": "Shadow root id.", + "$ref": "NodeId" + } + ] + }, + { + "name": "shadowRootPushed", + "description": "Called when shadow root is pushed into the element.", + "experimental": true, + "parameters": [ + { + "name": "hostId", + "description": "Host element id.", + "$ref": "NodeId" + }, + { + "name": "root", + "description": "Shadow root.", + "$ref": "Node" + } + ] + } + ] + }, + { + "domain": "DOMDebugger", + "description": "DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\nexecution will stop on these operations as if there was a regular breakpoint set.", + "dependencies": [ + "DOM", + "Debugger", + "Runtime" + ], + "types": [ + { + "id": "DOMBreakpointType", + "description": "DOM breakpoint type.", + "type": "string", + "enum": [ + "subtree-modified", + "attribute-modified", + "node-removed" + ] + }, + { + "id": "EventListener", + "description": "Object event listener.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "`EventListener`'s type.", + "type": "string" + }, + { + "name": "useCapture", + "description": "`EventListener`'s useCapture.", + "type": "boolean" + }, + { + "name": "passive", + "description": "`EventListener`'s passive flag.", + "type": "boolean" + }, + { + "name": "once", + "description": "`EventListener`'s once flag.", + "type": "boolean" + }, + { + "name": "scriptId", + "description": "Script id of the handler code.", + "$ref": "Runtime.ScriptId" + }, + { + "name": "lineNumber", + "description": "Line number in the script (0-based).", + "type": "integer" + }, + { + "name": "columnNumber", + "description": "Column number in the script (0-based).", + "type": "integer" + }, + { + "name": "handler", + "description": "Event handler function value.", + "optional": true, + "$ref": "Runtime.RemoteObject" + }, + { + "name": "originalHandler", + "description": "Event original handler function value.", + "optional": true, + "$ref": "Runtime.RemoteObject" + }, + { + "name": "backendNodeId", + "description": "Node the listener is added to (if any).", + "optional": true, + "$ref": "DOM.BackendNodeId" + } + ] + } + ], + "commands": [ + { + "name": "getEventListeners", + "description": "Returns event listeners of the given object.", + "parameters": [ + { + "name": "objectId", + "description": "Identifier of the object to return listeners for.", + "$ref": "Runtime.RemoteObjectId" + }, + { + "name": "depth", + "description": "The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.", + "optional": true, + "type": "integer" + }, + { + "name": "pierce", + "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false). Reports listeners for all contexts if pierce is enabled.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "listeners", + "description": "Array of relevant listeners.", + "type": "array", + "items": { + "$ref": "EventListener" + } + } + ] + }, + { + "name": "removeDOMBreakpoint", + "description": "Removes DOM breakpoint that was set using `setDOMBreakpoint`.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node to remove breakpoint from.", + "$ref": "DOM.NodeId" + }, + { + "name": "type", + "description": "Type of the breakpoint to remove.", + "$ref": "DOMBreakpointType" + } + ] + }, + { + "name": "removeEventListenerBreakpoint", + "description": "Removes breakpoint on particular DOM event.", + "parameters": [ + { + "name": "eventName", + "description": "Event name.", + "type": "string" + }, + { + "name": "targetName", + "description": "EventTarget interface name.", + "experimental": true, + "optional": true, + "type": "string" + } + ] + }, + { + "name": "removeInstrumentationBreakpoint", + "description": "Removes breakpoint on particular native event.", + "experimental": true, + "parameters": [ + { + "name": "eventName", + "description": "Instrumentation name to stop on.", + "type": "string" + } + ] + }, + { + "name": "removeXHRBreakpoint", + "description": "Removes breakpoint from XMLHttpRequest.", + "parameters": [ + { + "name": "url", + "description": "Resource URL substring.", + "type": "string" + } + ] + }, + { + "name": "setDOMBreakpoint", + "description": "Sets breakpoint on particular operation with DOM.", + "parameters": [ + { + "name": "nodeId", + "description": "Identifier of the node to set breakpoint on.", + "$ref": "DOM.NodeId" + }, + { + "name": "type", + "description": "Type of the operation to stop upon.", + "$ref": "DOMBreakpointType" + } + ] + }, + { + "name": "setEventListenerBreakpoint", + "description": "Sets breakpoint on particular DOM event.", + "parameters": [ + { + "name": "eventName", + "description": "DOM Event name to stop on (any DOM event will do).", + "type": "string" + }, + { + "name": "targetName", + "description": "EventTarget interface name to stop on. If equal to `\"*\"` or not provided, will stop on any\nEventTarget.", + "experimental": true, + "optional": true, + "type": "string" + } + ] + }, + { + "name": "setInstrumentationBreakpoint", + "description": "Sets breakpoint on particular native event.", + "experimental": true, + "parameters": [ + { + "name": "eventName", + "description": "Instrumentation name to stop on.", + "type": "string" + } + ] + }, + { + "name": "setXHRBreakpoint", + "description": "Sets breakpoint on XMLHttpRequest.", + "parameters": [ + { + "name": "url", + "description": "Resource URL substring. All XHRs having this substring in the URL will get stopped upon.", + "type": "string" + } + ] + } + ] + }, + { + "domain": "DOMSnapshot", + "description": "This domain facilitates obtaining document snapshots with DOM, layout, and style information.", + "experimental": true, + "dependencies": [ + "CSS", + "DOM", + "DOMDebugger", + "Page" + ], + "types": [ + { + "id": "DOMNode", + "description": "A Node in the DOM tree.", + "type": "object", + "properties": [ + { + "name": "nodeType", + "description": "`Node`'s nodeType.", + "type": "integer" + }, + { + "name": "nodeName", + "description": "`Node`'s nodeName.", + "type": "string" + }, + { + "name": "nodeValue", + "description": "`Node`'s nodeValue.", + "type": "string" + }, + { + "name": "textValue", + "description": "Only set for textarea elements, contains the text value.", + "optional": true, + "type": "string" + }, + { + "name": "inputValue", + "description": "Only set for input elements, contains the input's associated text value.", + "optional": true, + "type": "string" + }, + { + "name": "inputChecked", + "description": "Only set for radio and checkbox input elements, indicates if the element has been checked", + "optional": true, + "type": "boolean" + }, + { + "name": "optionSelected", + "description": "Only set for option elements, indicates if the element has been selected", + "optional": true, + "type": "boolean" + }, + { + "name": "backendNodeId", + "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.", + "$ref": "DOM.BackendNodeId" + }, + { + "name": "childNodeIndexes", + "description": "The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if\nany.", + "optional": true, + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "attributes", + "description": "Attributes of an `Element` node.", + "optional": true, + "type": "array", + "items": { + "$ref": "NameValue" + } + }, + { + "name": "pseudoElementIndexes", + "description": "Indexes of pseudo elements associated with this node in the `domNodes` array returned by\n`getSnapshot`, if any.", + "optional": true, + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "layoutNodeIndex", + "description": "The index of the node's related layout tree node in the `layoutTreeNodes` array returned by\n`getSnapshot`, if any.", + "optional": true, + "type": "integer" + }, + { + "name": "documentURL", + "description": "Document URL that `Document` or `FrameOwner` node points to.", + "optional": true, + "type": "string" + }, + { + "name": "baseURL", + "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.", + "optional": true, + "type": "string" + }, + { + "name": "contentLanguage", + "description": "Only set for documents, contains the document's content language.", + "optional": true, + "type": "string" + }, + { + "name": "documentEncoding", + "description": "Only set for documents, contains the document's character set encoding.", + "optional": true, + "type": "string" + }, + { + "name": "publicId", + "description": "`DocumentType` node's publicId.", + "optional": true, + "type": "string" + }, + { + "name": "systemId", + "description": "`DocumentType` node's systemId.", + "optional": true, + "type": "string" + }, + { + "name": "frameId", + "description": "Frame ID for frame owner elements and also for the document node.", + "optional": true, + "$ref": "Page.FrameId" + }, + { + "name": "contentDocumentIndex", + "description": "The index of a frame owner element's content document in the `domNodes` array returned by\n`getSnapshot`, if any.", + "optional": true, + "type": "integer" + }, + { + "name": "pseudoType", + "description": "Type of a pseudo element node.", + "optional": true, + "$ref": "DOM.PseudoType" + }, + { + "name": "shadowRootType", + "description": "Shadow root type.", + "optional": true, + "$ref": "DOM.ShadowRootType" + }, + { + "name": "isClickable", + "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.", + "optional": true, + "type": "boolean" + }, + { + "name": "eventListeners", + "description": "Details of the node's event listeners, if any.", + "optional": true, + "type": "array", + "items": { + "$ref": "DOMDebugger.EventListener" + } + }, + { + "name": "currentSourceURL", + "description": "The selected url for nodes with a srcset attribute.", + "optional": true, + "type": "string" + }, + { + "name": "originURL", + "description": "The url of the script (if any) that generates this node.", + "optional": true, + "type": "string" + }, + { + "name": "scrollOffsetX", + "description": "Scroll offsets, set when this node is a Document.", + "optional": true, + "type": "number" + }, + { + "name": "scrollOffsetY", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "InlineTextBox", + "description": "Details of post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.", + "type": "object", + "properties": [ + { + "name": "boundingBox", + "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.", + "$ref": "DOM.Rect" + }, + { + "name": "startCharacterIndex", + "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.", + "type": "integer" + }, + { + "name": "numCharacters", + "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.", + "type": "integer" + } + ] + }, + { + "id": "LayoutTreeNode", + "description": "Details of an element in the DOM tree with a LayoutObject.", + "type": "object", + "properties": [ + { + "name": "domNodeIndex", + "description": "The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.", + "type": "integer" + }, + { + "name": "boundingBox", + "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.", + "$ref": "DOM.Rect" + }, + { + "name": "layoutText", + "description": "Contents of the LayoutText, if any.", + "optional": true, + "type": "string" + }, + { + "name": "inlineTextNodes", + "description": "The post-layout inline text nodes, if any.", + "optional": true, + "type": "array", + "items": { + "$ref": "InlineTextBox" + } + }, + { + "name": "styleIndex", + "description": "Index into the `computedStyles` array returned by `getSnapshot`.", + "optional": true, + "type": "integer" + }, + { + "name": "paintOrder", + "description": "Global paint order index, which is determined by the stacking order of the nodes. Nodes\nthat are painted together will have the same index. Only provided if includePaintOrder in\ngetSnapshot was true.", + "optional": true, + "type": "integer" + }, + { + "name": "isStackingContext", + "description": "Set to true to indicate the element begins a new stacking context.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "id": "ComputedStyle", + "description": "A subset of the full ComputedStyle as defined by the request whitelist.", + "type": "object", + "properties": [ + { + "name": "properties", + "description": "Name/value pairs of computed style properties.", + "type": "array", + "items": { + "$ref": "NameValue" + } + } + ] + }, + { + "id": "NameValue", + "description": "A name/value pair.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Attribute/property name.", + "type": "string" + }, + { + "name": "value", + "description": "Attribute/property value.", + "type": "string" + } + ] + }, + { + "id": "StringIndex", + "description": "Index of the string in the strings table.", + "type": "integer" + }, + { + "id": "ArrayOfStrings", + "description": "Index of the string in the strings table.", + "type": "array", + "items": { + "$ref": "StringIndex" + } + }, + { + "id": "RareStringData", + "description": "Data that is only present on rare nodes.", + "type": "object", + "properties": [ + { + "name": "index", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "value", + "type": "array", + "items": { + "$ref": "StringIndex" + } + } + ] + }, + { + "id": "RareBooleanData", + "type": "object", + "properties": [ + { + "name": "index", + "type": "array", + "items": { + "type": "integer" + } + } + ] + }, + { + "id": "RareIntegerData", + "type": "object", + "properties": [ + { + "name": "index", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "value", + "type": "array", + "items": { + "type": "integer" + } + } + ] + }, + { + "id": "Rectangle", + "type": "array", + "items": { + "type": "number" + } + }, + { + "id": "DocumentSnapshot", + "description": "Document snapshot.", + "type": "object", + "properties": [ + { + "name": "documentURL", + "description": "Document URL that `Document` or `FrameOwner` node points to.", + "$ref": "StringIndex" + }, + { + "name": "baseURL", + "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.", + "$ref": "StringIndex" + }, + { + "name": "contentLanguage", + "description": "Contains the document's content language.", + "$ref": "StringIndex" + }, + { + "name": "encodingName", + "description": "Contains the document's character set encoding.", + "$ref": "StringIndex" + }, + { + "name": "publicId", + "description": "`DocumentType` node's publicId.", + "$ref": "StringIndex" + }, + { + "name": "systemId", + "description": "`DocumentType` node's systemId.", + "$ref": "StringIndex" + }, + { + "name": "frameId", + "description": "Frame ID for frame owner elements and also for the document node.", + "$ref": "StringIndex" + }, + { + "name": "nodes", + "description": "A table with dom nodes.", + "$ref": "NodeTreeSnapshot" + }, + { + "name": "layout", + "description": "The nodes in the layout tree.", + "$ref": "LayoutTreeSnapshot" + }, + { + "name": "textBoxes", + "description": "The post-layout inline text nodes.", + "$ref": "TextBoxSnapshot" + }, + { + "name": "scrollOffsetX", + "description": "Scroll offsets.", + "optional": true, + "type": "number" + }, + { + "name": "scrollOffsetY", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "NodeTreeSnapshot", + "description": "Table containing nodes.", + "type": "object", + "properties": [ + { + "name": "parentIndex", + "description": "Parent node index.", + "optional": true, + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "nodeType", + "description": "`Node`'s nodeType.", + "optional": true, + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "nodeName", + "description": "`Node`'s nodeName.", + "optional": true, + "type": "array", + "items": { + "$ref": "StringIndex" + } + }, + { + "name": "nodeValue", + "description": "`Node`'s nodeValue.", + "optional": true, + "type": "array", + "items": { + "$ref": "StringIndex" + } + }, + { + "name": "backendNodeId", + "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.", + "optional": true, + "type": "array", + "items": { + "$ref": "DOM.BackendNodeId" + } + }, + { + "name": "attributes", + "description": "Attributes of an `Element` node. Flatten name, value pairs.", + "optional": true, + "type": "array", + "items": { + "$ref": "ArrayOfStrings" + } + }, + { + "name": "textValue", + "description": "Only set for textarea elements, contains the text value.", + "optional": true, + "$ref": "RareStringData" + }, + { + "name": "inputValue", + "description": "Only set for input elements, contains the input's associated text value.", + "optional": true, + "$ref": "RareStringData" + }, + { + "name": "inputChecked", + "description": "Only set for radio and checkbox input elements, indicates if the element has been checked", + "optional": true, + "$ref": "RareBooleanData" + }, + { + "name": "optionSelected", + "description": "Only set for option elements, indicates if the element has been selected", + "optional": true, + "$ref": "RareBooleanData" + }, + { + "name": "contentDocumentIndex", + "description": "The index of the document in the list of the snapshot documents.", + "optional": true, + "$ref": "RareIntegerData" + }, + { + "name": "pseudoType", + "description": "Type of a pseudo element node.", + "optional": true, + "$ref": "RareStringData" + }, + { + "name": "isClickable", + "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.", + "optional": true, + "$ref": "RareBooleanData" + }, + { + "name": "currentSourceURL", + "description": "The selected url for nodes with a srcset attribute.", + "optional": true, + "$ref": "RareStringData" + }, + { + "name": "originURL", + "description": "The url of the script (if any) that generates this node.", + "optional": true, + "$ref": "RareStringData" + } + ] + }, + { + "id": "LayoutTreeSnapshot", + "description": "Details of an element in the DOM tree with a LayoutObject.", + "type": "object", + "properties": [ + { + "name": "nodeIndex", + "description": "The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "styles", + "description": "Index into the `computedStyles` array returned by `captureSnapshot`.", + "type": "array", + "items": { + "$ref": "ArrayOfStrings" + } + }, + { + "name": "bounds", + "description": "The absolute position bounding box.", + "type": "array", + "items": { + "$ref": "Rectangle" + } + }, + { + "name": "text", + "description": "Contents of the LayoutText, if any.", + "type": "array", + "items": { + "$ref": "StringIndex" + } + }, + { + "name": "stackingContexts", + "description": "Stacking context information.", + "$ref": "RareBooleanData" + } + ] + }, + { + "id": "TextBoxSnapshot", + "description": "Details of post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.", + "type": "object", + "properties": [ + { + "name": "layoutIndex", + "description": "Intex of th elayout tree node that owns this box collection.", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "bounds", + "description": "The absolute position bounding box.", + "type": "array", + "items": { + "$ref": "Rectangle" + } + }, + { + "name": "start", + "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "length", + "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.", + "type": "array", + "items": { + "type": "integer" + } + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables DOM snapshot agent for the given page." + }, + { + "name": "enable", + "description": "Enables DOM snapshot agent for the given page." + }, + { + "name": "getSnapshot", + "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.", + "deprecated": true, + "parameters": [ + { + "name": "computedStyleWhitelist", + "description": "Whitelist of computed styles to return.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "includeEventListeners", + "description": "Whether or not to retrieve details of DOM listeners (default false).", + "optional": true, + "type": "boolean" + }, + { + "name": "includePaintOrder", + "description": "Whether to determine and include the paint order index of LayoutTreeNodes (default false).", + "optional": true, + "type": "boolean" + }, + { + "name": "includeUserAgentShadowTree", + "description": "Whether to include UA shadow tree in the snapshot (default false).", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "domNodes", + "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.", + "type": "array", + "items": { + "$ref": "DOMNode" + } + }, + { + "name": "layoutTreeNodes", + "description": "The nodes in the layout tree.", + "type": "array", + "items": { + "$ref": "LayoutTreeNode" + } + }, + { + "name": "computedStyles", + "description": "Whitelisted ComputedStyle properties for each node in the layout tree.", + "type": "array", + "items": { + "$ref": "ComputedStyle" + } + } + ] + }, + { + "name": "captureSnapshot", + "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.", + "parameters": [ + { + "name": "computedStyles", + "description": "Whitelist of computed styles to return.", + "type": "array", + "items": { + "type": "string" + } + } + ], + "returns": [ + { + "name": "documents", + "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.", + "type": "array", + "items": { + "$ref": "DocumentSnapshot" + } + }, + { + "name": "strings", + "description": "Shared string table that all string properties refer to with indexes.", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + }, + { + "domain": "DOMStorage", + "description": "Query and modify DOM storage.", + "experimental": true, + "types": [ + { + "id": "StorageId", + "description": "DOM Storage identifier.", + "type": "object", + "properties": [ + { + "name": "securityOrigin", + "description": "Security origin for the storage.", + "type": "string" + }, + { + "name": "isLocalStorage", + "description": "Whether the storage is local storage (not session storage).", + "type": "boolean" + } + ] + }, + { + "id": "Item", + "description": "DOM Storage item.", + "type": "array", + "items": { + "type": "string" + } + } + ], + "commands": [ + { + "name": "clear", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + } + ] + }, + { + "name": "disable", + "description": "Disables storage tracking, prevents storage events from being sent to the client." + }, + { + "name": "enable", + "description": "Enables storage tracking, storage events will now be delivered to the client." + }, + { + "name": "getDOMStorageItems", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + } + ], + "returns": [ + { + "name": "entries", + "type": "array", + "items": { + "$ref": "Item" + } + } + ] + }, + { + "name": "removeDOMStorageItem", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + }, + { + "name": "key", + "type": "string" + } + ] + }, + { + "name": "setDOMStorageItem", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + }, + { + "name": "key", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "domStorageItemAdded", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + }, + { + "name": "key", + "type": "string" + }, + { + "name": "newValue", + "type": "string" + } + ] + }, + { + "name": "domStorageItemRemoved", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + }, + { + "name": "key", + "type": "string" + } + ] + }, + { + "name": "domStorageItemUpdated", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + }, + { + "name": "key", + "type": "string" + }, + { + "name": "oldValue", + "type": "string" + }, + { + "name": "newValue", + "type": "string" + } + ] + }, + { + "name": "domStorageItemsCleared", + "parameters": [ + { + "name": "storageId", + "$ref": "StorageId" + } + ] + } + ] + }, + { + "domain": "Database", + "experimental": true, + "types": [ + { + "id": "DatabaseId", + "description": "Unique identifier of Database object.", + "type": "string" + }, + { + "id": "Database", + "description": "Database object.", + "type": "object", + "properties": [ + { + "name": "id", + "description": "Database ID.", + "$ref": "DatabaseId" + }, + { + "name": "domain", + "description": "Database domain.", + "type": "string" + }, + { + "name": "name", + "description": "Database name.", + "type": "string" + }, + { + "name": "version", + "description": "Database version.", + "type": "string" + } + ] + }, + { + "id": "Error", + "description": "Database error.", + "type": "object", + "properties": [ + { + "name": "message", + "description": "Error message.", + "type": "string" + }, + { + "name": "code", + "description": "Error code.", + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables database tracking, prevents database events from being sent to the client." + }, + { + "name": "enable", + "description": "Enables database tracking, database events will now be delivered to the client." + }, + { + "name": "executeSQL", + "parameters": [ + { + "name": "databaseId", + "$ref": "DatabaseId" + }, + { + "name": "query", + "type": "string" + } + ], + "returns": [ + { + "name": "columnNames", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "values", + "optional": true, + "type": "array", + "items": { + "type": "any" + } + }, + { + "name": "sqlError", + "optional": true, + "$ref": "Error" + } + ] + }, + { + "name": "getDatabaseTableNames", + "parameters": [ + { + "name": "databaseId", + "$ref": "DatabaseId" + } + ], + "returns": [ + { + "name": "tableNames", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ], + "events": [ + { + "name": "addDatabase", + "parameters": [ + { + "name": "database", + "$ref": "Database" + } + ] + } + ] + }, + { + "domain": "DeviceOrientation", + "experimental": true, + "commands": [ + { + "name": "clearDeviceOrientationOverride", + "description": "Clears the overridden Device Orientation." + }, + { + "name": "setDeviceOrientationOverride", + "description": "Overrides the Device Orientation.", + "parameters": [ + { + "name": "alpha", + "description": "Mock alpha", + "type": "number" + }, + { + "name": "beta", + "description": "Mock beta", + "type": "number" + }, + { + "name": "gamma", + "description": "Mock gamma", + "type": "number" + } + ] + } + ] + }, + { + "domain": "Emulation", + "description": "This domain emulates different environments for the page.", + "dependencies": [ + "DOM", + "Page", + "Runtime" + ], + "types": [ + { + "id": "ScreenOrientation", + "description": "Screen orientation.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "Orientation type.", + "type": "string", + "enum": [ + "portraitPrimary", + "portraitSecondary", + "landscapePrimary", + "landscapeSecondary" + ] + }, + { + "name": "angle", + "description": "Orientation angle.", + "type": "integer" + } + ] + }, + { + "id": "VirtualTimePolicy", + "description": "advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\nresource fetches.", + "experimental": true, + "type": "string", + "enum": [ + "advance", + "pause", + "pauseIfNetworkFetchesPending" + ] + } + ], + "commands": [ + { + "name": "canEmulate", + "description": "Tells whether emulation is supported.", + "returns": [ + { + "name": "result", + "description": "True if emulation is supported.", + "type": "boolean" + } + ] + }, + { + "name": "clearDeviceMetricsOverride", + "description": "Clears the overriden device metrics." + }, + { + "name": "clearGeolocationOverride", + "description": "Clears the overriden Geolocation Position and Error." + }, + { + "name": "resetPageScaleFactor", + "description": "Requests that page scale factor is reset to initial values.", + "experimental": true + }, + { + "name": "setFocusEmulationEnabled", + "description": "Enables or disables simulating a focused and active page.", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "description": "Whether to enable to disable focus emulation.", + "type": "boolean" + } + ] + }, + { + "name": "setCPUThrottlingRate", + "description": "Enables CPU throttling to emulate slow CPUs.", + "experimental": true, + "parameters": [ + { + "name": "rate", + "description": "Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).", + "type": "number" + } + ] + }, + { + "name": "setDefaultBackgroundColorOverride", + "description": "Sets or clears an override of the default background color of the frame. This override is used\nif the content does not specify one.", + "parameters": [ + { + "name": "color", + "description": "RGBA of the default background color. If not specified, any existing override will be\ncleared.", + "optional": true, + "$ref": "DOM.RGBA" + } + ] + }, + { + "name": "setDeviceMetricsOverride", + "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).", + "parameters": [ + { + "name": "width", + "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.", + "type": "integer" + }, + { + "name": "height", + "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.", + "type": "integer" + }, + { + "name": "deviceScaleFactor", + "description": "Overriding device scale factor value. 0 disables the override.", + "type": "number" + }, + { + "name": "mobile", + "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.", + "type": "boolean" + }, + { + "name": "scale", + "description": "Scale to apply to resulting view image.", + "experimental": true, + "optional": true, + "type": "number" + }, + { + "name": "screenWidth", + "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "screenHeight", + "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "positionX", + "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "positionY", + "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "dontSetVisibleSize", + "description": "Do not set visible view size, rely upon explicit setVisibleSize call.", + "experimental": true, + "optional": true, + "type": "boolean" + }, + { + "name": "screenOrientation", + "description": "Screen orientation override.", + "optional": true, + "$ref": "ScreenOrientation" + }, + { + "name": "viewport", + "description": "If set, the visible area of the page will be overridden to this viewport. This viewport\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.", + "experimental": true, + "optional": true, + "$ref": "Page.Viewport" + } + ] + }, + { + "name": "setScrollbarsHidden", + "experimental": true, + "parameters": [ + { + "name": "hidden", + "description": "Whether scrollbars should be always hidden.", + "type": "boolean" + } + ] + }, + { + "name": "setDocumentCookieDisabled", + "experimental": true, + "parameters": [ + { + "name": "disabled", + "description": "Whether document.coookie API should be disabled.", + "type": "boolean" + } + ] + }, + { + "name": "setEmitTouchEventsForMouse", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "description": "Whether touch emulation based on mouse input should be enabled.", + "type": "boolean" + }, + { + "name": "configuration", + "description": "Touch/gesture events configuration. Default: current platform.", + "optional": true, + "type": "string", + "enum": [ + "mobile", + "desktop" + ] + } + ] + }, + { + "name": "setEmulatedMedia", + "description": "Emulates the given media for CSS media queries.", + "parameters": [ + { + "name": "media", + "description": "Media type to emulate. Empty string disables the override.", + "type": "string" + } + ] + }, + { + "name": "setGeolocationOverride", + "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.", + "parameters": [ + { + "name": "latitude", + "description": "Mock latitude", + "optional": true, + "type": "number" + }, + { + "name": "longitude", + "description": "Mock longitude", + "optional": true, + "type": "number" + }, + { + "name": "accuracy", + "description": "Mock accuracy", + "optional": true, + "type": "number" + } + ] + }, + { + "name": "setNavigatorOverrides", + "description": "Overrides value returned by the javascript navigator object.", + "experimental": true, + "deprecated": true, + "parameters": [ + { + "name": "platform", + "description": "The platform navigator.platform should return.", + "type": "string" + } + ] + }, + { + "name": "setPageScaleFactor", + "description": "Sets a specified page scale factor.", + "experimental": true, + "parameters": [ + { + "name": "pageScaleFactor", + "description": "Page scale factor.", + "type": "number" + } + ] + }, + { + "name": "setScriptExecutionDisabled", + "description": "Switches script execution in the page.", + "parameters": [ + { + "name": "value", + "description": "Whether script execution should be disabled in the page.", + "type": "boolean" + } + ] + }, + { + "name": "setTouchEmulationEnabled", + "description": "Enables touch on platforms which do not support them.", + "parameters": [ + { + "name": "enabled", + "description": "Whether the touch event emulation should be enabled.", + "type": "boolean" + }, + { + "name": "maxTouchPoints", + "description": "Maximum touch points supported. Defaults to one.", + "optional": true, + "type": "integer" + } + ] + }, + { + "name": "setVirtualTimePolicy", + "description": "Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\nthe current virtual time policy. Note this supersedes any previous time budget.", + "experimental": true, + "parameters": [ + { + "name": "policy", + "$ref": "VirtualTimePolicy" + }, + { + "name": "budget", + "description": "If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\nvirtualTimeBudgetExpired event is sent.", + "optional": true, + "type": "number" + }, + { + "name": "maxVirtualTimeTaskStarvationCount", + "description": "If set this specifies the maximum number of tasks that can be run before virtual is forced\nforwards to prevent deadlock.", + "optional": true, + "type": "integer" + }, + { + "name": "waitForNavigation", + "description": "If set the virtual time policy change should be deferred until any frame starts navigating.\nNote any previous deferred policy change is superseded.", + "optional": true, + "type": "boolean" + }, + { + "name": "initialVirtualTime", + "description": "If set, base::Time::Now will be overriden to initially return this value.", + "optional": true, + "$ref": "Network.TimeSinceEpoch" + } + ], + "returns": [ + { + "name": "virtualTimeTicksBase", + "description": "Absolute timestamp at which virtual time was first enabled (up time in milliseconds).", + "type": "number" + } + ] + }, + { + "name": "setVisibleSize", + "description": "Resizes the frame/viewport of the page. Note that this does not affect the frame's container\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\non Android.", + "experimental": true, + "deprecated": true, + "parameters": [ + { + "name": "width", + "description": "Frame width (DIP).", + "type": "integer" + }, + { + "name": "height", + "description": "Frame height (DIP).", + "type": "integer" + } + ] + }, + { + "name": "setUserAgentOverride", + "description": "Allows overriding user agent with the given string.", + "parameters": [ + { + "name": "userAgent", + "description": "User agent to use.", + "type": "string" + }, + { + "name": "acceptLanguage", + "description": "Browser langugage to emulate.", + "optional": true, + "type": "string" + }, + { + "name": "platform", + "description": "The platform navigator.platform should return.", + "optional": true, + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "virtualTimeAdvanced", + "description": "Notification sent after the virtual time has advanced.", + "experimental": true, + "parameters": [ + { + "name": "virtualTimeElapsed", + "description": "The amount of virtual time that has elapsed in milliseconds since virtual time was first\nenabled.", + "type": "number" + } + ] + }, + { + "name": "virtualTimeBudgetExpired", + "description": "Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.", + "experimental": true + }, + { + "name": "virtualTimePaused", + "description": "Notification sent after the virtual time has paused.", + "experimental": true, + "parameters": [ + { + "name": "virtualTimeElapsed", + "description": "The amount of virtual time that has elapsed in milliseconds since virtual time was first\nenabled.", + "type": "number" + } + ] + } + ] + }, + { + "domain": "HeadlessExperimental", + "description": "This domain provides experimental commands only supported in headless mode.", + "experimental": true, + "dependencies": [ + "Page", + "Runtime" + ], + "types": [ + { + "id": "ScreenshotParams", + "description": "Encoding options for a screenshot.", + "type": "object", + "properties": [ + { + "name": "format", + "description": "Image compression format (defaults to png).", + "optional": true, + "type": "string", + "enum": [ + "jpeg", + "png" + ] + }, + { + "name": "quality", + "description": "Compression quality from range [0..100] (jpeg only).", + "optional": true, + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "beginFrame", + "description": "Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\nscreenshot from the resulting frame. Requires that the target was created with enabled\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\nhttps://goo.gl/3zHXhB for more background.", + "parameters": [ + { + "name": "frameTimeTicks", + "description": "Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\nthe current time will be used.", + "optional": true, + "type": "number" + }, + { + "name": "interval", + "description": "The interval between BeginFrames that is reported to the compositor, in milliseconds.\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.", + "optional": true, + "type": "number" + }, + { + "name": "noDisplayUpdates", + "description": "Whether updates should not be committed and drawn onto the display. False by default. If\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\nany visual updates may not be visible on the display or in screenshots.", + "optional": true, + "type": "boolean" + }, + { + "name": "screenshot", + "description": "If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\nduring renderer initialization. In such a case, no screenshot data will be returned.", + "optional": true, + "$ref": "ScreenshotParams" + } + ], + "returns": [ + { + "name": "hasDamage", + "description": "Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\ndisplay. Reported for diagnostic uses, may be removed in the future.", + "type": "boolean" + }, + { + "name": "screenshotData", + "description": "Base64-encoded image data of the screenshot, if one was requested and successfully taken.", + "optional": true, + "type": "binary" + } + ] + }, + { + "name": "disable", + "description": "Disables headless events for the target." + }, + { + "name": "enable", + "description": "Enables headless events for the target." + } + ], + "events": [ + { + "name": "needsBeginFramesChanged", + "description": "Issued when the target starts or stops needing BeginFrames.", + "parameters": [ + { + "name": "needsBeginFrames", + "description": "True if BeginFrames are needed, false otherwise.", + "type": "boolean" + } + ] + } + ] + }, + { + "domain": "IO", + "description": "Input/Output operations for streams produced by DevTools.", + "types": [ + { + "id": "StreamHandle", + "description": "This is either obtained from another method or specifed as `blob:<uuid>` where\n`<uuid>` is an UUID of a Blob.", + "type": "string" + } + ], + "commands": [ + { + "name": "close", + "description": "Close the stream, discard any temporary backing storage.", + "parameters": [ + { + "name": "handle", + "description": "Handle of the stream to close.", + "$ref": "StreamHandle" + } + ] + }, + { + "name": "read", + "description": "Read a chunk of the stream", + "parameters": [ + { + "name": "handle", + "description": "Handle of the stream to read.", + "$ref": "StreamHandle" + }, + { + "name": "offset", + "description": "Seek to the specified offset before reading (if not specificed, proceed with offset\nfollowing the last read). Some types of streams may only support sequential reads.", + "optional": true, + "type": "integer" + }, + { + "name": "size", + "description": "Maximum number of bytes to read (left upon the agent discretion if not specified).", + "optional": true, + "type": "integer" + } + ], + "returns": [ + { + "name": "base64Encoded", + "description": "Set if the data is base64-encoded", + "optional": true, + "type": "boolean" + }, + { + "name": "data", + "description": "Data that were read.", + "type": "string" + }, + { + "name": "eof", + "description": "Set if the end-of-file condition occured while reading.", + "type": "boolean" + } + ] + }, + { + "name": "resolveBlob", + "description": "Return UUID of Blob object specified by a remote object id.", + "parameters": [ + { + "name": "objectId", + "description": "Object id of a Blob object wrapper.", + "$ref": "Runtime.RemoteObjectId" + } + ], + "returns": [ + { + "name": "uuid", + "description": "UUID of the specified Blob.", + "type": "string" + } + ] + } + ] + }, + { + "domain": "IndexedDB", + "experimental": true, + "dependencies": [ + "Runtime" + ], + "types": [ + { + "id": "DatabaseWithObjectStores", + "description": "Database with an array of object stores.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Database name.", + "type": "string" + }, + { + "name": "version", + "description": "Database version.", + "type": "integer" + }, + { + "name": "objectStores", + "description": "Object stores in this database.", + "type": "array", + "items": { + "$ref": "ObjectStore" + } + } + ] + }, + { + "id": "ObjectStore", + "description": "Object store.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Object store name.", + "type": "string" + }, + { + "name": "keyPath", + "description": "Object store key path.", + "$ref": "KeyPath" + }, + { + "name": "autoIncrement", + "description": "If true, object store has auto increment flag set.", + "type": "boolean" + }, + { + "name": "indexes", + "description": "Indexes in this object store.", + "type": "array", + "items": { + "$ref": "ObjectStoreIndex" + } + } + ] + }, + { + "id": "ObjectStoreIndex", + "description": "Object store index.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Index name.", + "type": "string" + }, + { + "name": "keyPath", + "description": "Index key path.", + "$ref": "KeyPath" + }, + { + "name": "unique", + "description": "If true, index is unique.", + "type": "boolean" + }, + { + "name": "multiEntry", + "description": "If true, index allows multiple entries for a key.", + "type": "boolean" + } + ] + }, + { + "id": "Key", + "description": "Key.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "Key type.", + "type": "string", + "enum": [ + "number", + "string", + "date", + "array" + ] + }, + { + "name": "number", + "description": "Number value.", + "optional": true, + "type": "number" + }, + { + "name": "string", + "description": "String value.", + "optional": true, + "type": "string" + }, + { + "name": "date", + "description": "Date value.", + "optional": true, + "type": "number" + }, + { + "name": "array", + "description": "Array value.", + "optional": true, + "type": "array", + "items": { + "$ref": "Key" + } + } + ] + }, + { + "id": "KeyRange", + "description": "Key range.", + "type": "object", + "properties": [ + { + "name": "lower", + "description": "Lower bound.", + "optional": true, + "$ref": "Key" + }, + { + "name": "upper", + "description": "Upper bound.", + "optional": true, + "$ref": "Key" + }, + { + "name": "lowerOpen", + "description": "If true lower bound is open.", + "type": "boolean" + }, + { + "name": "upperOpen", + "description": "If true upper bound is open.", + "type": "boolean" + } + ] + }, + { + "id": "DataEntry", + "description": "Data entry.", + "type": "object", + "properties": [ + { + "name": "key", + "description": "Key object.", + "$ref": "Runtime.RemoteObject" + }, + { + "name": "primaryKey", + "description": "Primary key object.", + "$ref": "Runtime.RemoteObject" + }, + { + "name": "value", + "description": "Value object.", + "$ref": "Runtime.RemoteObject" + } + ] + }, + { + "id": "KeyPath", + "description": "Key path.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "Key path type.", + "type": "string", + "enum": [ + "null", + "string", + "array" + ] + }, + { + "name": "string", + "description": "String value.", + "optional": true, + "type": "string" + }, + { + "name": "array", + "description": "Array value.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ], + "commands": [ + { + "name": "clearObjectStore", + "description": "Clears all entries from an object store.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + }, + { + "name": "databaseName", + "description": "Database name.", + "type": "string" + }, + { + "name": "objectStoreName", + "description": "Object store name.", + "type": "string" + } + ] + }, + { + "name": "deleteDatabase", + "description": "Deletes a database.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + }, + { + "name": "databaseName", + "description": "Database name.", + "type": "string" + } + ] + }, + { + "name": "deleteObjectStoreEntries", + "description": "Delete a range of entries from an object store", + "parameters": [ + { + "name": "securityOrigin", + "type": "string" + }, + { + "name": "databaseName", + "type": "string" + }, + { + "name": "objectStoreName", + "type": "string" + }, + { + "name": "keyRange", + "description": "Range of entry keys to delete", + "$ref": "KeyRange" + } + ] + }, + { + "name": "disable", + "description": "Disables events from backend." + }, + { + "name": "enable", + "description": "Enables events from backend." + }, + { + "name": "requestData", + "description": "Requests data from object store or index.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + }, + { + "name": "databaseName", + "description": "Database name.", + "type": "string" + }, + { + "name": "objectStoreName", + "description": "Object store name.", + "type": "string" + }, + { + "name": "indexName", + "description": "Index name, empty string for object store data requests.", + "type": "string" + }, + { + "name": "skipCount", + "description": "Number of records to skip.", + "type": "integer" + }, + { + "name": "pageSize", + "description": "Number of records to fetch.", + "type": "integer" + }, + { + "name": "keyRange", + "description": "Key range.", + "optional": true, + "$ref": "KeyRange" + } + ], + "returns": [ + { + "name": "objectStoreDataEntries", + "description": "Array of object store data entries.", + "type": "array", + "items": { + "$ref": "DataEntry" + } + }, + { + "name": "hasMore", + "description": "If true, there are more entries to fetch in the given range.", + "type": "boolean" + } + ] + }, + { + "name": "requestDatabase", + "description": "Requests database with given name in given frame.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + }, + { + "name": "databaseName", + "description": "Database name.", + "type": "string" + } + ], + "returns": [ + { + "name": "databaseWithObjectStores", + "description": "Database with an array of object stores.", + "$ref": "DatabaseWithObjectStores" + } + ] + }, + { + "name": "requestDatabaseNames", + "description": "Requests database names for given security origin.", + "parameters": [ + { + "name": "securityOrigin", + "description": "Security origin.", + "type": "string" + } + ], + "returns": [ + { + "name": "databaseNames", + "description": "Database names for origin.", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + }, + { + "domain": "Input", + "types": [ + { + "id": "TouchPoint", + "type": "object", + "properties": [ + { + "name": "x", + "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.", + "type": "number" + }, + { + "name": "radiusX", + "description": "X radius of the touch area (default: 1.0).", + "optional": true, + "type": "number" + }, + { + "name": "radiusY", + "description": "Y radius of the touch area (default: 1.0).", + "optional": true, + "type": "number" + }, + { + "name": "rotationAngle", + "description": "Rotation angle (default: 0.0).", + "optional": true, + "type": "number" + }, + { + "name": "force", + "description": "Force (default: 1.0).", + "optional": true, + "type": "number" + }, + { + "name": "id", + "description": "Identifier used to track touch sources between events, must be unique within an event.", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "GestureSourceType", + "experimental": true, + "type": "string", + "enum": [ + "default", + "touch", + "mouse" + ] + }, + { + "id": "TimeSinceEpoch", + "description": "UTC time in seconds, counted from January 1, 1970.", + "type": "number" + } + ], + "commands": [ + { + "name": "dispatchKeyEvent", + "description": "Dispatches a key event to the page.", + "parameters": [ + { + "name": "type", + "description": "Type of the key event.", + "type": "string", + "enum": [ + "keyDown", + "keyUp", + "rawKeyDown", + "char" + ] + }, + { + "name": "modifiers", + "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "timestamp", + "description": "Time at which the event occurred.", + "optional": true, + "$ref": "TimeSinceEpoch" + }, + { + "name": "text", + "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for\nfor `keyUp` and `rawKeyDown` events (default: \"\")", + "optional": true, + "type": "string" + }, + { + "name": "unmodifiedText", + "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for\nshift). Useful for shortcut (accelerator) key handling (default: \"\").", + "optional": true, + "type": "string" + }, + { + "name": "keyIdentifier", + "description": "Unique key identifier (e.g., 'U+0041') (default: \"\").", + "optional": true, + "type": "string" + }, + { + "name": "code", + "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\").", + "optional": true, + "type": "string" + }, + { + "name": "key", + "description": "Unique DOM defined string value describing the meaning of the key in the context of active\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\").", + "optional": true, + "type": "string" + }, + { + "name": "windowsVirtualKeyCode", + "description": "Windows virtual key code (default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "nativeVirtualKeyCode", + "description": "Native virtual key code (default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "autoRepeat", + "description": "Whether the event was generated from auto repeat (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "isKeypad", + "description": "Whether the event was generated from the keypad (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "isSystemKey", + "description": "Whether the event was a system key event (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "location", + "description": "Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\n0).", + "optional": true, + "type": "integer" + } + ] + }, + { + "name": "insertText", + "description": "This method emulates inserting text that doesn't come from a key press,\nfor example an emoji keyboard or an IME.", + "experimental": true, + "parameters": [ + { + "name": "text", + "description": "The text to insert.", + "type": "string" + } + ] + }, + { + "name": "dispatchMouseEvent", + "description": "Dispatches a mouse event to the page.", + "parameters": [ + { + "name": "type", + "description": "Type of the mouse event.", + "type": "string", + "enum": [ + "mousePressed", + "mouseReleased", + "mouseMoved", + "mouseWheel" + ] + }, + { + "name": "x", + "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.", + "type": "number" + }, + { + "name": "modifiers", + "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "timestamp", + "description": "Time at which the event occurred.", + "optional": true, + "$ref": "TimeSinceEpoch" + }, + { + "name": "button", + "description": "Mouse button (default: \"none\").", + "optional": true, + "type": "string", + "enum": [ + "none", + "left", + "middle", + "right" + ] + }, + { + "name": "clickCount", + "description": "Number of times the mouse button was clicked (default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "deltaX", + "description": "X delta in CSS pixels for mouse wheel event (default: 0).", + "optional": true, + "type": "number" + }, + { + "name": "deltaY", + "description": "Y delta in CSS pixels for mouse wheel event (default: 0).", + "optional": true, + "type": "number" + } + ] + }, + { + "name": "dispatchTouchEvent", + "description": "Dispatches a touch event to the page.", + "parameters": [ + { + "name": "type", + "description": "Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\nTouchStart and TouchMove must contains at least one.", + "type": "string", + "enum": [ + "touchStart", + "touchEnd", + "touchMove", + "touchCancel" + ] + }, + { + "name": "touchPoints", + "description": "Active touch points on the touch device. One event per any changed point (compared to\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\none by one.", + "type": "array", + "items": { + "$ref": "TouchPoint" + } + }, + { + "name": "modifiers", + "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "timestamp", + "description": "Time at which the event occurred.", + "optional": true, + "$ref": "TimeSinceEpoch" + } + ] + }, + { + "name": "emulateTouchFromMouseEvent", + "description": "Emulates touch event from the mouse event parameters.", + "experimental": true, + "parameters": [ + { + "name": "type", + "description": "Type of the mouse event.", + "type": "string", + "enum": [ + "mousePressed", + "mouseReleased", + "mouseMoved", + "mouseWheel" + ] + }, + { + "name": "x", + "description": "X coordinate of the mouse pointer in DIP.", + "type": "integer" + }, + { + "name": "y", + "description": "Y coordinate of the mouse pointer in DIP.", + "type": "integer" + }, + { + "name": "button", + "description": "Mouse button.", + "type": "string", + "enum": [ + "none", + "left", + "middle", + "right" + ] + }, + { + "name": "timestamp", + "description": "Time at which the event occurred (default: current time).", + "optional": true, + "$ref": "TimeSinceEpoch" + }, + { + "name": "deltaX", + "description": "X delta in DIP for mouse wheel event (default: 0).", + "optional": true, + "type": "number" + }, + { + "name": "deltaY", + "description": "Y delta in DIP for mouse wheel event (default: 0).", + "optional": true, + "type": "number" + }, + { + "name": "modifiers", + "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "clickCount", + "description": "Number of times the mouse button was clicked (default: 0).", + "optional": true, + "type": "integer" + } + ] + }, + { + "name": "setIgnoreInputEvents", + "description": "Ignores input events (useful while auditing page).", + "parameters": [ + { + "name": "ignore", + "description": "Ignores input events processing when set to true.", + "type": "boolean" + } + ] + }, + { + "name": "synthesizePinchGesture", + "description": "Synthesizes a pinch gesture over a time period by issuing appropriate touch events.", + "experimental": true, + "parameters": [ + { + "name": "x", + "description": "X coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "scaleFactor", + "description": "Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).", + "type": "number" + }, + { + "name": "relativeSpeed", + "description": "Relative pointer speed in pixels per second (default: 800).", + "optional": true, + "type": "integer" + }, + { + "name": "gestureSourceType", + "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).", + "optional": true, + "$ref": "GestureSourceType" + } + ] + }, + { + "name": "synthesizeScrollGesture", + "description": "Synthesizes a scroll gesture over a time period by issuing appropriate touch events.", + "experimental": true, + "parameters": [ + { + "name": "x", + "description": "X coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "xDistance", + "description": "The distance to scroll along the X axis (positive to scroll left).", + "optional": true, + "type": "number" + }, + { + "name": "yDistance", + "description": "The distance to scroll along the Y axis (positive to scroll up).", + "optional": true, + "type": "number" + }, + { + "name": "xOverscroll", + "description": "The number of additional pixels to scroll back along the X axis, in addition to the given\ndistance.", + "optional": true, + "type": "number" + }, + { + "name": "yOverscroll", + "description": "The number of additional pixels to scroll back along the Y axis, in addition to the given\ndistance.", + "optional": true, + "type": "number" + }, + { + "name": "preventFling", + "description": "Prevent fling (default: true).", + "optional": true, + "type": "boolean" + }, + { + "name": "speed", + "description": "Swipe speed in pixels per second (default: 800).", + "optional": true, + "type": "integer" + }, + { + "name": "gestureSourceType", + "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).", + "optional": true, + "$ref": "GestureSourceType" + }, + { + "name": "repeatCount", + "description": "The number of times to repeat the gesture (default: 0).", + "optional": true, + "type": "integer" + }, + { + "name": "repeatDelayMs", + "description": "The number of milliseconds delay between each repeat. (default: 250).", + "optional": true, + "type": "integer" + }, + { + "name": "interactionMarkerName", + "description": "The name of the interaction markers to generate, if not empty (default: \"\").", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "synthesizeTapGesture", + "description": "Synthesizes a tap gesture over a time period by issuing appropriate touch events.", + "experimental": true, + "parameters": [ + { + "name": "x", + "description": "X coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y coordinate of the start of the gesture in CSS pixels.", + "type": "number" + }, + { + "name": "duration", + "description": "Duration between touchdown and touchup events in ms (default: 50).", + "optional": true, + "type": "integer" + }, + { + "name": "tapCount", + "description": "Number of times to perform the tap (e.g. 2 for double tap, default: 1).", + "optional": true, + "type": "integer" + }, + { + "name": "gestureSourceType", + "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).", + "optional": true, + "$ref": "GestureSourceType" + } + ] + } + ] + }, + { + "domain": "Inspector", + "experimental": true, + "commands": [ + { + "name": "disable", + "description": "Disables inspector domain notifications." + }, + { + "name": "enable", + "description": "Enables inspector domain notifications." + } + ], + "events": [ + { + "name": "detached", + "description": "Fired when remote debugging connection is about to be terminated. Contains detach reason.", + "parameters": [ + { + "name": "reason", + "description": "The reason why connection has been terminated.", + "type": "string" + } + ] + }, + { + "name": "targetCrashed", + "description": "Fired when debugging target has crashed" + }, + { + "name": "targetReloadedAfterCrash", + "description": "Fired when debugging target has reloaded after crash" + } + ] + }, + { + "domain": "LayerTree", + "experimental": true, + "dependencies": [ + "DOM" + ], + "types": [ + { + "id": "LayerId", + "description": "Unique Layer identifier.", + "type": "string" + }, + { + "id": "SnapshotId", + "description": "Unique snapshot identifier.", + "type": "string" + }, + { + "id": "ScrollRect", + "description": "Rectangle where scrolling happens on the main thread.", + "type": "object", + "properties": [ + { + "name": "rect", + "description": "Rectangle itself.", + "$ref": "DOM.Rect" + }, + { + "name": "type", + "description": "Reason for rectangle to force scrolling on the main thread", + "type": "string", + "enum": [ + "RepaintsOnScroll", + "TouchEventHandler", + "WheelEventHandler" + ] + } + ] + }, + { + "id": "StickyPositionConstraint", + "description": "Sticky position constraints.", + "type": "object", + "properties": [ + { + "name": "stickyBoxRect", + "description": "Layout rectangle of the sticky element before being shifted", + "$ref": "DOM.Rect" + }, + { + "name": "containingBlockRect", + "description": "Layout rectangle of the containing block of the sticky element", + "$ref": "DOM.Rect" + }, + { + "name": "nearestLayerShiftingStickyBox", + "description": "The nearest sticky layer that shifts the sticky box", + "optional": true, + "$ref": "LayerId" + }, + { + "name": "nearestLayerShiftingContainingBlock", + "description": "The nearest sticky layer that shifts the containing block", + "optional": true, + "$ref": "LayerId" + } + ] + }, + { + "id": "PictureTile", + "description": "Serialized fragment of layer picture along with its offset within the layer.", + "type": "object", + "properties": [ + { + "name": "x", + "description": "Offset from owning layer left boundary", + "type": "number" + }, + { + "name": "y", + "description": "Offset from owning layer top boundary", + "type": "number" + }, + { + "name": "picture", + "description": "Base64-encoded snapshot data.", + "type": "binary" + } + ] + }, + { + "id": "Layer", + "description": "Information about a compositing layer.", + "type": "object", + "properties": [ + { + "name": "layerId", + "description": "The unique id for this layer.", + "$ref": "LayerId" + }, + { + "name": "parentLayerId", + "description": "The id of parent (not present for root).", + "optional": true, + "$ref": "LayerId" + }, + { + "name": "backendNodeId", + "description": "The backend id for the node associated with this layer.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "offsetX", + "description": "Offset from parent layer, X coordinate.", + "type": "number" + }, + { + "name": "offsetY", + "description": "Offset from parent layer, Y coordinate.", + "type": "number" + }, + { + "name": "width", + "description": "Layer width.", + "type": "number" + }, + { + "name": "height", + "description": "Layer height.", + "type": "number" + }, + { + "name": "transform", + "description": "Transformation matrix for layer, default is identity matrix", + "optional": true, + "type": "array", + "items": { + "type": "number" + } + }, + { + "name": "anchorX", + "description": "Transform anchor point X, absent if no transform specified", + "optional": true, + "type": "number" + }, + { + "name": "anchorY", + "description": "Transform anchor point Y, absent if no transform specified", + "optional": true, + "type": "number" + }, + { + "name": "anchorZ", + "description": "Transform anchor point Z, absent if no transform specified", + "optional": true, + "type": "number" + }, + { + "name": "paintCount", + "description": "Indicates how many time this layer has painted.", + "type": "integer" + }, + { + "name": "drawsContent", + "description": "Indicates whether this layer hosts any content, rather than being used for\ntransform/scrolling purposes only.", + "type": "boolean" + }, + { + "name": "invisible", + "description": "Set if layer is not visible.", + "optional": true, + "type": "boolean" + }, + { + "name": "scrollRects", + "description": "Rectangles scrolling on main thread only.", + "optional": true, + "type": "array", + "items": { + "$ref": "ScrollRect" + } + }, + { + "name": "stickyPositionConstraint", + "description": "Sticky position constraint information", + "optional": true, + "$ref": "StickyPositionConstraint" + } + ] + }, + { + "id": "PaintProfile", + "description": "Array of timings, one per paint step.", + "type": "array", + "items": { + "type": "number" + } + } + ], + "commands": [ + { + "name": "compositingReasons", + "description": "Provides the reasons why the given layer was composited.", + "parameters": [ + { + "name": "layerId", + "description": "The id of the layer for which we want to get the reasons it was composited.", + "$ref": "LayerId" + } + ], + "returns": [ + { + "name": "compositingReasons", + "description": "A list of strings specifying reasons for the given layer to become composited.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "disable", + "description": "Disables compositing tree inspection." + }, + { + "name": "enable", + "description": "Enables compositing tree inspection." + }, + { + "name": "loadSnapshot", + "description": "Returns the snapshot identifier.", + "parameters": [ + { + "name": "tiles", + "description": "An array of tiles composing the snapshot.", + "type": "array", + "items": { + "$ref": "PictureTile" + } + } + ], + "returns": [ + { + "name": "snapshotId", + "description": "The id of the snapshot.", + "$ref": "SnapshotId" + } + ] + }, + { + "name": "makeSnapshot", + "description": "Returns the layer snapshot identifier.", + "parameters": [ + { + "name": "layerId", + "description": "The id of the layer.", + "$ref": "LayerId" + } + ], + "returns": [ + { + "name": "snapshotId", + "description": "The id of the layer snapshot.", + "$ref": "SnapshotId" + } + ] + }, + { + "name": "profileSnapshot", + "parameters": [ + { + "name": "snapshotId", + "description": "The id of the layer snapshot.", + "$ref": "SnapshotId" + }, + { + "name": "minRepeatCount", + "description": "The maximum number of times to replay the snapshot (1, if not specified).", + "optional": true, + "type": "integer" + }, + { + "name": "minDuration", + "description": "The minimum duration (in seconds) to replay the snapshot.", + "optional": true, + "type": "number" + }, + { + "name": "clipRect", + "description": "The clip rectangle to apply when replaying the snapshot.", + "optional": true, + "$ref": "DOM.Rect" + } + ], + "returns": [ + { + "name": "timings", + "description": "The array of paint profiles, one per run.", + "type": "array", + "items": { + "$ref": "PaintProfile" + } + } + ] + }, + { + "name": "releaseSnapshot", + "description": "Releases layer snapshot captured by the back-end.", + "parameters": [ + { + "name": "snapshotId", + "description": "The id of the layer snapshot.", + "$ref": "SnapshotId" + } + ] + }, + { + "name": "replaySnapshot", + "description": "Replays the layer snapshot and returns the resulting bitmap.", + "parameters": [ + { + "name": "snapshotId", + "description": "The id of the layer snapshot.", + "$ref": "SnapshotId" + }, + { + "name": "fromStep", + "description": "The first step to replay from (replay from the very start if not specified).", + "optional": true, + "type": "integer" + }, + { + "name": "toStep", + "description": "The last step to replay to (replay till the end if not specified).", + "optional": true, + "type": "integer" + }, + { + "name": "scale", + "description": "The scale to apply while replaying (defaults to 1).", + "optional": true, + "type": "number" + } + ], + "returns": [ + { + "name": "dataURL", + "description": "A data: URL for resulting image.", + "type": "string" + } + ] + }, + { + "name": "snapshotCommandLog", + "description": "Replays the layer snapshot and returns canvas log.", + "parameters": [ + { + "name": "snapshotId", + "description": "The id of the layer snapshot.", + "$ref": "SnapshotId" + } + ], + "returns": [ + { + "name": "commandLog", + "description": "The array of canvas function calls.", + "type": "array", + "items": { + "type": "object" + } + } + ] + } + ], + "events": [ + { + "name": "layerPainted", + "parameters": [ + { + "name": "layerId", + "description": "The id of the painted layer.", + "$ref": "LayerId" + }, + { + "name": "clip", + "description": "Clip rectangle.", + "$ref": "DOM.Rect" + } + ] + }, + { + "name": "layerTreeDidChange", + "parameters": [ + { + "name": "layers", + "description": "Layer tree, absent if not in the comspositing mode.", + "optional": true, + "type": "array", + "items": { + "$ref": "Layer" + } + } + ] + } + ] + }, + { + "domain": "Log", + "description": "Provides access to log entries.", + "dependencies": [ + "Runtime", + "Network" + ], + "types": [ + { + "id": "LogEntry", + "description": "Log entry.", + "type": "object", + "properties": [ + { + "name": "source", + "description": "Log entry source.", + "type": "string", + "enum": [ + "xml", + "javascript", + "network", + "storage", + "appcache", + "rendering", + "security", + "deprecation", + "worker", + "violation", + "intervention", + "recommendation", + "other" + ] + }, + { + "name": "level", + "description": "Log entry severity.", + "type": "string", + "enum": [ + "verbose", + "info", + "warning", + "error" + ] + }, + { + "name": "text", + "description": "Logged text.", + "type": "string" + }, + { + "name": "timestamp", + "description": "Timestamp when this entry was added.", + "$ref": "Runtime.Timestamp" + }, + { + "name": "url", + "description": "URL of the resource if known.", + "optional": true, + "type": "string" + }, + { + "name": "lineNumber", + "description": "Line number in the resource.", + "optional": true, + "type": "integer" + }, + { + "name": "stackTrace", + "description": "JavaScript stack trace.", + "optional": true, + "$ref": "Runtime.StackTrace" + }, + { + "name": "networkRequestId", + "description": "Identifier of the network request associated with this entry.", + "optional": true, + "$ref": "Network.RequestId" + }, + { + "name": "workerId", + "description": "Identifier of the worker associated with this entry.", + "optional": true, + "type": "string" + }, + { + "name": "args", + "description": "Call arguments.", + "optional": true, + "type": "array", + "items": { + "$ref": "Runtime.RemoteObject" + } + } + ] + }, + { + "id": "ViolationSetting", + "description": "Violation configuration setting.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Violation type.", + "type": "string", + "enum": [ + "longTask", + "longLayout", + "blockedEvent", + "blockedParser", + "discouragedAPIUse", + "handler", + "recurringHandler" + ] + }, + { + "name": "threshold", + "description": "Time threshold to trigger upon.", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "clear", + "description": "Clears the log." + }, + { + "name": "disable", + "description": "Disables log domain, prevents further log entries from being reported to the client." + }, + { + "name": "enable", + "description": "Enables log domain, sends the entries collected so far to the client by means of the\n`entryAdded` notification." + }, + { + "name": "startViolationsReport", + "description": "start violation reporting.", + "parameters": [ + { + "name": "config", + "description": "Configuration for violations.", + "type": "array", + "items": { + "$ref": "ViolationSetting" + } + } + ] + }, + { + "name": "stopViolationsReport", + "description": "Stop violation reporting." + } + ], + "events": [ + { + "name": "entryAdded", + "description": "Issued when new message was logged.", + "parameters": [ + { + "name": "entry", + "description": "The entry.", + "$ref": "LogEntry" + } + ] + } + ] + }, + { + "domain": "Memory", + "experimental": true, + "types": [ + { + "id": "PressureLevel", + "description": "Memory pressure level.", + "type": "string", + "enum": [ + "moderate", + "critical" + ] + }, + { + "id": "SamplingProfileNode", + "description": "Heap profile sample.", + "type": "object", + "properties": [ + { + "name": "size", + "description": "Size of the sampled allocation.", + "type": "number" + }, + { + "name": "total", + "description": "Total bytes attributed to this sample.", + "type": "number" + }, + { + "name": "stack", + "description": "Execution stack at the point of allocation.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "id": "SamplingProfile", + "description": "Array of heap profile samples.", + "type": "object", + "properties": [ + { + "name": "samples", + "type": "array", + "items": { + "$ref": "SamplingProfileNode" + } + }, + { + "name": "modules", + "type": "array", + "items": { + "$ref": "Module" + } + } + ] + }, + { + "id": "Module", + "description": "Executable module information", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Name of the module.", + "type": "string" + }, + { + "name": "uuid", + "description": "UUID of the module.", + "type": "string" + }, + { + "name": "baseAddress", + "description": "Base address where the module is loaded into memory. Encoded as a decimal\nor hexadecimal (0x prefixed) string.", + "type": "string" + }, + { + "name": "size", + "description": "Size of the module in bytes.", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "getDOMCounters", + "returns": [ + { + "name": "documents", + "type": "integer" + }, + { + "name": "nodes", + "type": "integer" + }, + { + "name": "jsEventListeners", + "type": "integer" + } + ] + }, + { + "name": "prepareForLeakDetection" + }, + { + "name": "setPressureNotificationsSuppressed", + "description": "Enable/disable suppressing memory pressure notifications in all processes.", + "parameters": [ + { + "name": "suppressed", + "description": "If true, memory pressure notifications will be suppressed.", + "type": "boolean" + } + ] + }, + { + "name": "simulatePressureNotification", + "description": "Simulate a memory pressure notification in all processes.", + "parameters": [ + { + "name": "level", + "description": "Memory pressure level of the notification.", + "$ref": "PressureLevel" + } + ] + }, + { + "name": "startSampling", + "description": "Start collecting native memory profile.", + "parameters": [ + { + "name": "samplingInterval", + "description": "Average number of bytes between samples.", + "optional": true, + "type": "integer" + }, + { + "name": "suppressRandomness", + "description": "Do not randomize intervals between samples.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "stopSampling", + "description": "Stop collecting native memory profile." + }, + { + "name": "getAllTimeSamplingProfile", + "description": "Retrieve native memory allocations profile\ncollected since renderer process startup.", + "returns": [ + { + "name": "profile", + "$ref": "SamplingProfile" + } + ] + }, + { + "name": "getBrowserSamplingProfile", + "description": "Retrieve native memory allocations profile\ncollected since browser process startup.", + "returns": [ + { + "name": "profile", + "$ref": "SamplingProfile" + } + ] + }, + { + "name": "getSamplingProfile", + "description": "Retrieve native memory allocations profile collected since last\n`startSampling` call.", + "returns": [ + { + "name": "profile", + "$ref": "SamplingProfile" + } + ] + } + ] + }, + { + "domain": "Network", + "description": "Network domain allows tracking network activities of the page. It exposes information about http,\nfile, data and other requests and responses, their headers, bodies, timing, etc.", + "dependencies": [ + "Debugger", + "Runtime", + "Security" + ], + "types": [ + { + "id": "ResourceType", + "description": "Resource type as it was perceived by the rendering engine.", + "type": "string", + "enum": [ + "Document", + "Stylesheet", + "Image", + "Media", + "Font", + "Script", + "TextTrack", + "XHR", + "Fetch", + "EventSource", + "WebSocket", + "Manifest", + "SignedExchange", + "Ping", + "CSPViolationReport", + "Other" + ] + }, + { + "id": "LoaderId", + "description": "Unique loader identifier.", + "type": "string" + }, + { + "id": "RequestId", + "description": "Unique request identifier.", + "type": "string" + }, + { + "id": "InterceptionId", + "description": "Unique intercepted request identifier.", + "type": "string" + }, + { + "id": "ErrorReason", + "description": "Network level fetch failure reason.", + "type": "string", + "enum": [ + "Failed", + "Aborted", + "TimedOut", + "AccessDenied", + "ConnectionClosed", + "ConnectionReset", + "ConnectionRefused", + "ConnectionAborted", + "ConnectionFailed", + "NameNotResolved", + "InternetDisconnected", + "AddressUnreachable", + "BlockedByClient", + "BlockedByResponse" + ] + }, + { + "id": "TimeSinceEpoch", + "description": "UTC time in seconds, counted from January 1, 1970.", + "type": "number" + }, + { + "id": "MonotonicTime", + "description": "Monotonically increasing time in seconds since an arbitrary point in the past.", + "type": "number" + }, + { + "id": "Headers", + "description": "Request / response headers as keys / values of JSON object.", + "type": "object" + }, + { + "id": "ConnectionType", + "description": "The underlying connection technology that the browser is supposedly using.", + "type": "string", + "enum": [ + "none", + "cellular2g", + "cellular3g", + "cellular4g", + "bluetooth", + "ethernet", + "wifi", + "wimax", + "other" + ] + }, + { + "id": "CookieSameSite", + "description": "Represents the cookie's 'SameSite' status:\nhttps://tools.ietf.org/html/draft-west-first-party-cookies", + "type": "string", + "enum": [ + "Strict", + "Lax" + ] + }, + { + "id": "ResourceTiming", + "description": "Timing information for the request.", + "type": "object", + "properties": [ + { + "name": "requestTime", + "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\nmilliseconds relatively to this requestTime.", + "type": "number" + }, + { + "name": "proxyStart", + "description": "Started resolving proxy.", + "type": "number" + }, + { + "name": "proxyEnd", + "description": "Finished resolving proxy.", + "type": "number" + }, + { + "name": "dnsStart", + "description": "Started DNS address resolve.", + "type": "number" + }, + { + "name": "dnsEnd", + "description": "Finished DNS address resolve.", + "type": "number" + }, + { + "name": "connectStart", + "description": "Started connecting to the remote host.", + "type": "number" + }, + { + "name": "connectEnd", + "description": "Connected to the remote host.", + "type": "number" + }, + { + "name": "sslStart", + "description": "Started SSL handshake.", + "type": "number" + }, + { + "name": "sslEnd", + "description": "Finished SSL handshake.", + "type": "number" + }, + { + "name": "workerStart", + "description": "Started running ServiceWorker.", + "experimental": true, + "type": "number" + }, + { + "name": "workerReady", + "description": "Finished Starting ServiceWorker.", + "experimental": true, + "type": "number" + }, + { + "name": "sendStart", + "description": "Started sending request.", + "type": "number" + }, + { + "name": "sendEnd", + "description": "Finished sending request.", + "type": "number" + }, + { + "name": "pushStart", + "description": "Time the server started pushing request.", + "experimental": true, + "type": "number" + }, + { + "name": "pushEnd", + "description": "Time the server finished pushing request.", + "experimental": true, + "type": "number" + }, + { + "name": "receiveHeadersEnd", + "description": "Finished receiving response headers.", + "type": "number" + } + ] + }, + { + "id": "ResourcePriority", + "description": "Loading priority of a resource request.", + "type": "string", + "enum": [ + "VeryLow", + "Low", + "Medium", + "High", + "VeryHigh" + ] + }, + { + "id": "Request", + "description": "HTTP request data.", + "type": "object", + "properties": [ + { + "name": "url", + "description": "Request URL (without fragment).", + "type": "string" + }, + { + "name": "urlFragment", + "description": "Fragment of the requested URL starting with hash, if present.", + "optional": true, + "type": "string" + }, + { + "name": "method", + "description": "HTTP request method.", + "type": "string" + }, + { + "name": "headers", + "description": "HTTP request headers.", + "$ref": "Headers" + }, + { + "name": "postData", + "description": "HTTP POST request data.", + "optional": true, + "type": "string" + }, + { + "name": "hasPostData", + "description": "True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.", + "optional": true, + "type": "boolean" + }, + { + "name": "mixedContentType", + "description": "The mixed content type of the request.", + "optional": true, + "$ref": "Security.MixedContentType" + }, + { + "name": "initialPriority", + "description": "Priority of the resource request at the time request is sent.", + "$ref": "ResourcePriority" + }, + { + "name": "referrerPolicy", + "description": "The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/", + "type": "string", + "enum": [ + "unsafe-url", + "no-referrer-when-downgrade", + "no-referrer", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin" + ] + }, + { + "name": "isLinkPreload", + "description": "Whether is loaded via link preload.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "id": "SignedCertificateTimestamp", + "description": "Details of a signed certificate timestamp (SCT).", + "type": "object", + "properties": [ + { + "name": "status", + "description": "Validation status.", + "type": "string" + }, + { + "name": "origin", + "description": "Origin.", + "type": "string" + }, + { + "name": "logDescription", + "description": "Log name / description.", + "type": "string" + }, + { + "name": "logId", + "description": "Log ID.", + "type": "string" + }, + { + "name": "timestamp", + "description": "Issuance date.", + "$ref": "TimeSinceEpoch" + }, + { + "name": "hashAlgorithm", + "description": "Hash algorithm.", + "type": "string" + }, + { + "name": "signatureAlgorithm", + "description": "Signature algorithm.", + "type": "string" + }, + { + "name": "signatureData", + "description": "Signature data.", + "type": "string" + } + ] + }, + { + "id": "SecurityDetails", + "description": "Security details about a request.", + "type": "object", + "properties": [ + { + "name": "protocol", + "description": "Protocol name (e.g. \"TLS 1.2\" or \"QUIC\").", + "type": "string" + }, + { + "name": "keyExchange", + "description": "Key Exchange used by the connection, or the empty string if not applicable.", + "type": "string" + }, + { + "name": "keyExchangeGroup", + "description": "(EC)DH group used by the connection, if applicable.", + "optional": true, + "type": "string" + }, + { + "name": "cipher", + "description": "Cipher name.", + "type": "string" + }, + { + "name": "mac", + "description": "TLS MAC. Note that AEAD ciphers do not have separate MACs.", + "optional": true, + "type": "string" + }, + { + "name": "certificateId", + "description": "Certificate ID value.", + "$ref": "Security.CertificateId" + }, + { + "name": "subjectName", + "description": "Certificate subject name.", + "type": "string" + }, + { + "name": "sanList", + "description": "Subject Alternative Name (SAN) DNS names and IP addresses.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "issuer", + "description": "Name of the issuing CA.", + "type": "string" + }, + { + "name": "validFrom", + "description": "Certificate valid from date.", + "$ref": "TimeSinceEpoch" + }, + { + "name": "validTo", + "description": "Certificate valid to (expiration) date", + "$ref": "TimeSinceEpoch" + }, + { + "name": "signedCertificateTimestampList", + "description": "List of signed certificate timestamps (SCTs).", + "type": "array", + "items": { + "$ref": "SignedCertificateTimestamp" + } + }, + { + "name": "certificateTransparencyCompliance", + "description": "Whether the request complied with Certificate Transparency policy", + "$ref": "CertificateTransparencyCompliance" + } + ] + }, + { + "id": "CertificateTransparencyCompliance", + "description": "Whether the request complied with Certificate Transparency policy.", + "type": "string", + "enum": [ + "unknown", + "not-compliant", + "compliant" + ] + }, + { + "id": "BlockedReason", + "description": "The reason why request was blocked.", + "type": "string", + "enum": [ + "other", + "csp", + "mixed-content", + "origin", + "inspector", + "subresource-filter", + "content-type", + "collapsed-by-client" + ] + }, + { + "id": "Response", + "description": "HTTP response data.", + "type": "object", + "properties": [ + { + "name": "url", + "description": "Response URL. This URL can be different from CachedResource.url in case of redirect.", + "type": "string" + }, + { + "name": "status", + "description": "HTTP response status code.", + "type": "integer" + }, + { + "name": "statusText", + "description": "HTTP response status text.", + "type": "string" + }, + { + "name": "headers", + "description": "HTTP response headers.", + "$ref": "Headers" + }, + { + "name": "headersText", + "description": "HTTP response headers text.", + "optional": true, + "type": "string" + }, + { + "name": "mimeType", + "description": "Resource mimeType as determined by the browser.", + "type": "string" + }, + { + "name": "requestHeaders", + "description": "Refined HTTP request headers that were actually transmitted over the network.", + "optional": true, + "$ref": "Headers" + }, + { + "name": "requestHeadersText", + "description": "HTTP request headers text.", + "optional": true, + "type": "string" + }, + { + "name": "connectionReused", + "description": "Specifies whether physical connection was actually reused for this request.", + "type": "boolean" + }, + { + "name": "connectionId", + "description": "Physical connection id that was actually used for this request.", + "type": "number" + }, + { + "name": "remoteIPAddress", + "description": "Remote IP address.", + "optional": true, + "type": "string" + }, + { + "name": "remotePort", + "description": "Remote port.", + "optional": true, + "type": "integer" + }, + { + "name": "fromDiskCache", + "description": "Specifies that the request was served from the disk cache.", + "optional": true, + "type": "boolean" + }, + { + "name": "fromServiceWorker", + "description": "Specifies that the request was served from the ServiceWorker.", + "optional": true, + "type": "boolean" + }, + { + "name": "encodedDataLength", + "description": "Total number of bytes received for this request so far.", + "type": "number" + }, + { + "name": "timing", + "description": "Timing information for the given request.", + "optional": true, + "$ref": "ResourceTiming" + }, + { + "name": "protocol", + "description": "Protocol used to fetch this request.", + "optional": true, + "type": "string" + }, + { + "name": "securityState", + "description": "Security state of the request resource.", + "$ref": "Security.SecurityState" + }, + { + "name": "securityDetails", + "description": "Security details for the request.", + "optional": true, + "$ref": "SecurityDetails" + } + ] + }, + { + "id": "WebSocketRequest", + "description": "WebSocket request data.", + "type": "object", + "properties": [ + { + "name": "headers", + "description": "HTTP request headers.", + "$ref": "Headers" + } + ] + }, + { + "id": "WebSocketResponse", + "description": "WebSocket response data.", + "type": "object", + "properties": [ + { + "name": "status", + "description": "HTTP response status code.", + "type": "integer" + }, + { + "name": "statusText", + "description": "HTTP response status text.", + "type": "string" + }, + { + "name": "headers", + "description": "HTTP response headers.", + "$ref": "Headers" + }, + { + "name": "headersText", + "description": "HTTP response headers text.", + "optional": true, + "type": "string" + }, + { + "name": "requestHeaders", + "description": "HTTP request headers.", + "optional": true, + "$ref": "Headers" + }, + { + "name": "requestHeadersText", + "description": "HTTP request headers text.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "WebSocketFrame", + "description": "WebSocket frame data.", + "type": "object", + "properties": [ + { + "name": "opcode", + "description": "WebSocket frame opcode.", + "type": "number" + }, + { + "name": "mask", + "description": "WebSocke frame mask.", + "type": "boolean" + }, + { + "name": "payloadData", + "description": "WebSocke frame payload data.", + "type": "string" + } + ] + }, + { + "id": "CachedResource", + "description": "Information about the cached resource.", + "type": "object", + "properties": [ + { + "name": "url", + "description": "Resource URL. This is the url of the original network request.", + "type": "string" + }, + { + "name": "type", + "description": "Type of this resource.", + "$ref": "ResourceType" + }, + { + "name": "response", + "description": "Cached response data.", + "optional": true, + "$ref": "Response" + }, + { + "name": "bodySize", + "description": "Cached response body size.", + "type": "number" + } + ] + }, + { + "id": "Initiator", + "description": "Information about the request initiator.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "Type of this initiator.", + "type": "string", + "enum": [ + "parser", + "script", + "preload", + "SignedExchange", + "other" + ] + }, + { + "name": "stack", + "description": "Initiator JavaScript stack trace, set for Script only.", + "optional": true, + "$ref": "Runtime.StackTrace" + }, + { + "name": "url", + "description": "Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.", + "optional": true, + "type": "string" + }, + { + "name": "lineNumber", + "description": "Initiator line number, set for Parser type or for Script type (when script is importing\nmodule) (0-based).", + "optional": true, + "type": "number" + } + ] + }, + { + "id": "Cookie", + "description": "Cookie object", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Cookie name.", + "type": "string" + }, + { + "name": "value", + "description": "Cookie value.", + "type": "string" + }, + { + "name": "domain", + "description": "Cookie domain.", + "type": "string" + }, + { + "name": "path", + "description": "Cookie path.", + "type": "string" + }, + { + "name": "expires", + "description": "Cookie expiration date as the number of seconds since the UNIX epoch.", + "type": "number" + }, + { + "name": "size", + "description": "Cookie size.", + "type": "integer" + }, + { + "name": "httpOnly", + "description": "True if cookie is http-only.", + "type": "boolean" + }, + { + "name": "secure", + "description": "True if cookie is secure.", + "type": "boolean" + }, + { + "name": "session", + "description": "True in case of session cookie.", + "type": "boolean" + }, + { + "name": "sameSite", + "description": "Cookie SameSite type.", + "optional": true, + "$ref": "CookieSameSite" + } + ] + }, + { + "id": "CookieParam", + "description": "Cookie parameter object", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Cookie name.", + "type": "string" + }, + { + "name": "value", + "description": "Cookie value.", + "type": "string" + }, + { + "name": "url", + "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain and path values of the created cookie.", + "optional": true, + "type": "string" + }, + { + "name": "domain", + "description": "Cookie domain.", + "optional": true, + "type": "string" + }, + { + "name": "path", + "description": "Cookie path.", + "optional": true, + "type": "string" + }, + { + "name": "secure", + "description": "True if cookie is secure.", + "optional": true, + "type": "boolean" + }, + { + "name": "httpOnly", + "description": "True if cookie is http-only.", + "optional": true, + "type": "boolean" + }, + { + "name": "sameSite", + "description": "Cookie SameSite type.", + "optional": true, + "$ref": "CookieSameSite" + }, + { + "name": "expires", + "description": "Cookie expiration date, session cookie if not set", + "optional": true, + "$ref": "TimeSinceEpoch" + } + ] + }, + { + "id": "AuthChallenge", + "description": "Authorization challenge for HTTP status code 401 or 407.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "source", + "description": "Source of the authentication challenge.", + "optional": true, + "type": "string", + "enum": [ + "Server", + "Proxy" + ] + }, + { + "name": "origin", + "description": "Origin of the challenger.", + "type": "string" + }, + { + "name": "scheme", + "description": "The authentication scheme used, such as basic or digest", + "type": "string" + }, + { + "name": "realm", + "description": "The realm of the challenge. May be empty.", + "type": "string" + } + ] + }, + { + "id": "AuthChallengeResponse", + "description": "Response to an AuthChallenge.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "response", + "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.", + "type": "string", + "enum": [ + "Default", + "CancelAuth", + "ProvideCredentials" + ] + }, + { + "name": "username", + "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.", + "optional": true, + "type": "string" + }, + { + "name": "password", + "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "InterceptionStage", + "description": "Stages of the interception to begin intercepting. Request will intercept before the request is\nsent. Response will intercept after the response is received.", + "experimental": true, + "type": "string", + "enum": [ + "Request", + "HeadersReceived" + ] + }, + { + "id": "RequestPattern", + "description": "Request pattern for interception.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "urlPattern", + "description": "Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to \"*\".", + "optional": true, + "type": "string" + }, + { + "name": "resourceType", + "description": "If set, only requests for matching resource types will be intercepted.", + "optional": true, + "$ref": "ResourceType" + }, + { + "name": "interceptionStage", + "description": "Stage at wich to begin intercepting requests. Default is Request.", + "optional": true, + "$ref": "InterceptionStage" + } + ] + }, + { + "id": "SignedExchangeSignature", + "description": "Information about a signed exchange signature.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "label", + "description": "Signed exchange signature label.", + "type": "string" + }, + { + "name": "signature", + "description": "The hex string of signed exchange signature.", + "type": "string" + }, + { + "name": "integrity", + "description": "Signed exchange signature integrity.", + "type": "string" + }, + { + "name": "certUrl", + "description": "Signed exchange signature cert Url.", + "optional": true, + "type": "string" + }, + { + "name": "certSha256", + "description": "The hex string of signed exchange signature cert sha256.", + "optional": true, + "type": "string" + }, + { + "name": "validityUrl", + "description": "Signed exchange signature validity Url.", + "type": "string" + }, + { + "name": "date", + "description": "Signed exchange signature date.", + "type": "integer" + }, + { + "name": "expires", + "description": "Signed exchange signature expires.", + "type": "integer" + }, + { + "name": "certificates", + "description": "The encoded certificates.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "id": "SignedExchangeHeader", + "description": "Information about a signed exchange header.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "requestUrl", + "description": "Signed exchange request URL.", + "type": "string" + }, + { + "name": "requestMethod", + "description": "Signed exchange request method.", + "type": "string" + }, + { + "name": "responseCode", + "description": "Signed exchange response code.", + "type": "integer" + }, + { + "name": "responseHeaders", + "description": "Signed exchange response headers.", + "$ref": "Headers" + }, + { + "name": "signatures", + "description": "Signed exchange response signature.", + "type": "array", + "items": { + "$ref": "SignedExchangeSignature" + } + } + ] + }, + { + "id": "SignedExchangeErrorField", + "description": "Field type for a signed exchange related error.", + "experimental": true, + "type": "string", + "enum": [ + "signatureSig", + "signatureIntegrity", + "signatureCertUrl", + "signatureCertSha256", + "signatureValidityUrl", + "signatureTimestamps" + ] + }, + { + "id": "SignedExchangeError", + "description": "Information about a signed exchange response.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "message", + "description": "Error message.", + "type": "string" + }, + { + "name": "signatureIndex", + "description": "The index of the signature which caused the error.", + "optional": true, + "type": "integer" + }, + { + "name": "errorField", + "description": "The field which caused the error.", + "optional": true, + "$ref": "SignedExchangeErrorField" + } + ] + }, + { + "id": "SignedExchangeInfo", + "description": "Information about a signed exchange response.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "outerResponse", + "description": "The outer response of signed HTTP exchange which was received from network.", + "$ref": "Response" + }, + { + "name": "header", + "description": "Information about the signed exchange header.", + "optional": true, + "$ref": "SignedExchangeHeader" + }, + { + "name": "securityDetails", + "description": "Security details for the signed exchange header.", + "optional": true, + "$ref": "SecurityDetails" + }, + { + "name": "errors", + "description": "Errors occurred while handling the signed exchagne.", + "optional": true, + "type": "array", + "items": { + "$ref": "SignedExchangeError" + } + } + ] + } + ], + "commands": [ + { + "name": "canClearBrowserCache", + "description": "Tells whether clearing browser cache is supported.", + "deprecated": true, + "returns": [ + { + "name": "result", + "description": "True if browser cache can be cleared.", + "type": "boolean" + } + ] + }, + { + "name": "canClearBrowserCookies", + "description": "Tells whether clearing browser cookies is supported.", + "deprecated": true, + "returns": [ + { + "name": "result", + "description": "True if browser cookies can be cleared.", + "type": "boolean" + } + ] + }, + { + "name": "canEmulateNetworkConditions", + "description": "Tells whether emulation of network conditions is supported.", + "deprecated": true, + "returns": [ + { + "name": "result", + "description": "True if emulation of network conditions is supported.", + "type": "boolean" + } + ] + }, + { + "name": "clearBrowserCache", + "description": "Clears browser cache." + }, + { + "name": "clearBrowserCookies", + "description": "Clears browser cookies." + }, + { + "name": "continueInterceptedRequest", + "description": "Response to Network.requestIntercepted which either modifies the request to continue with any\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\nevent will be sent with the same InterceptionId.", + "experimental": true, + "parameters": [ + { + "name": "interceptionId", + "$ref": "InterceptionId" + }, + { + "name": "errorReason", + "description": "If set this causes the request to fail with the given reason. Passing `Aborted` for requests\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\nto an authChallenge.", + "optional": true, + "$ref": "ErrorReason" + }, + { + "name": "rawResponse", + "description": "If set the requests completes using with the provided base64 encoded raw response, including\nHTTP status line and headers etc... Must not be set in response to an authChallenge.", + "optional": true, + "type": "binary" + }, + { + "name": "url", + "description": "If set the request url will be modified in a way that's not observable by page. Must not be\nset in response to an authChallenge.", + "optional": true, + "type": "string" + }, + { + "name": "method", + "description": "If set this allows the request method to be overridden. Must not be set in response to an\nauthChallenge.", + "optional": true, + "type": "string" + }, + { + "name": "postData", + "description": "If set this allows postData to be set. Must not be set in response to an authChallenge.", + "optional": true, + "type": "string" + }, + { + "name": "headers", + "description": "If set this allows the request headers to be changed. Must not be set in response to an\nauthChallenge.", + "optional": true, + "$ref": "Headers" + }, + { + "name": "authChallengeResponse", + "description": "Response to a requestIntercepted with an authChallenge. Must not be set otherwise.", + "optional": true, + "$ref": "AuthChallengeResponse" + } + ] + }, + { + "name": "deleteCookies", + "description": "Deletes browser cookies with matching name and url or domain/path pair.", + "parameters": [ + { + "name": "name", + "description": "Name of the cookies to remove.", + "type": "string" + }, + { + "name": "url", + "description": "If specified, deletes all the cookies with the given name where domain and path match\nprovided URL.", + "optional": true, + "type": "string" + }, + { + "name": "domain", + "description": "If specified, deletes only cookies with the exact domain.", + "optional": true, + "type": "string" + }, + { + "name": "path", + "description": "If specified, deletes only cookies with the exact path.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "disable", + "description": "Disables network tracking, prevents network events from being sent to the client." + }, + { + "name": "emulateNetworkConditions", + "description": "Activates emulation of network conditions.", + "parameters": [ + { + "name": "offline", + "description": "True to emulate internet disconnection.", + "type": "boolean" + }, + { + "name": "latency", + "description": "Minimum latency from request sent to response headers received (ms).", + "type": "number" + }, + { + "name": "downloadThroughput", + "description": "Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.", + "type": "number" + }, + { + "name": "uploadThroughput", + "description": "Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.", + "type": "number" + }, + { + "name": "connectionType", + "description": "Connection type if known.", + "optional": true, + "$ref": "ConnectionType" + } + ] + }, + { + "name": "enable", + "description": "Enables network tracking, network events will now be delivered to the client.", + "parameters": [ + { + "name": "maxTotalBufferSize", + "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "maxResourceBufferSize", + "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).", + "experimental": true, + "optional": true, + "type": "integer" + }, + { + "name": "maxPostDataSize", + "description": "Longest post body size (in bytes) that would be included in requestWillBeSent notification", + "optional": true, + "type": "integer" + } + ] + }, + { + "name": "getAllCookies", + "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie\ninformation in the `cookies` field.", + "returns": [ + { + "name": "cookies", + "description": "Array of cookie objects.", + "type": "array", + "items": { + "$ref": "Cookie" + } + } + ] + }, + { + "name": "getCertificate", + "description": "Returns the DER-encoded certificate.", + "experimental": true, + "parameters": [ + { + "name": "origin", + "description": "Origin to get certificate for.", + "type": "string" + } + ], + "returns": [ + { + "name": "tableNames", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "getCookies", + "description": "Returns all browser cookies for the current URL. Depending on the backend support, will return\ndetailed cookie information in the `cookies` field.", + "parameters": [ + { + "name": "urls", + "description": "The list of URLs for which applicable cookies will be fetched", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + } + ], + "returns": [ + { + "name": "cookies", + "description": "Array of cookie objects.", + "type": "array", + "items": { + "$ref": "Cookie" + } + } + ] + }, + { + "name": "getResponseBody", + "description": "Returns content served for the given request.", + "parameters": [ + { + "name": "requestId", + "description": "Identifier of the network request to get content for.", + "$ref": "RequestId" + } + ], + "returns": [ + { + "name": "body", + "description": "Response body.", + "type": "string" + }, + { + "name": "base64Encoded", + "description": "True, if content was sent as base64.", + "type": "boolean" + } + ] + }, + { + "name": "getRequestPostData", + "description": "Returns post data sent with the request. Returns an error when no data was sent with the request.", + "parameters": [ + { + "name": "requestId", + "description": "Identifier of the network request to get content for.", + "$ref": "RequestId" + } + ], + "returns": [ + { + "name": "postData", + "description": "Base64-encoded request body.", + "type": "string" + } + ] + }, + { + "name": "getResponseBodyForInterception", + "description": "Returns content served for the given currently intercepted request.", + "experimental": true, + "parameters": [ + { + "name": "interceptionId", + "description": "Identifier for the intercepted request to get body for.", + "$ref": "InterceptionId" + } + ], + "returns": [ + { + "name": "body", + "description": "Response body.", + "type": "string" + }, + { + "name": "base64Encoded", + "description": "True, if content was sent as base64.", + "type": "boolean" + } + ] + }, + { + "name": "takeResponseBodyForInterceptionAsStream", + "description": "Returns a handle to the stream representing the response body. Note that after this command,\nthe intercepted request can't be continued as is -- you either need to cancel it or to provide\nthe response body. The stream only supports sequential read, IO.read will fail if the position\nis specified.", + "experimental": true, + "parameters": [ + { + "name": "interceptionId", + "$ref": "InterceptionId" + } + ], + "returns": [ + { + "name": "stream", + "$ref": "IO.StreamHandle" + } + ] + }, + { + "name": "replayXHR", + "description": "This method sends a new XMLHttpRequest which is identical to the original one. The following\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\nattribute, user, password.", + "experimental": true, + "parameters": [ + { + "name": "requestId", + "description": "Identifier of XHR to replay.", + "$ref": "RequestId" + } + ] + }, + { + "name": "searchInResponseBody", + "description": "Searches for given string in response content.", + "experimental": true, + "parameters": [ + { + "name": "requestId", + "description": "Identifier of the network response to search.", + "$ref": "RequestId" + }, + { + "name": "query", + "description": "String to search for.", + "type": "string" + }, + { + "name": "caseSensitive", + "description": "If true, search is case sensitive.", + "optional": true, + "type": "boolean" + }, + { + "name": "isRegex", + "description": "If true, treats string parameter as regex.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "result", + "description": "List of search matches.", + "type": "array", + "items": { + "$ref": "Debugger.SearchMatch" + } + } + ] + }, + { + "name": "setBlockedURLs", + "description": "Blocks URLs from loading.", + "experimental": true, + "parameters": [ + { + "name": "urls", + "description": "URL patterns to block. Wildcards ('*') are allowed.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "setBypassServiceWorker", + "description": "Toggles ignoring of service worker for each request.", + "experimental": true, + "parameters": [ + { + "name": "bypass", + "description": "Bypass service worker and load from network.", + "type": "boolean" + } + ] + }, + { + "name": "setCacheDisabled", + "description": "Toggles ignoring cache for each request. If `true`, cache will not be used.", + "parameters": [ + { + "name": "cacheDisabled", + "description": "Cache disabled state.", + "type": "boolean" + } + ] + }, + { + "name": "setCookie", + "description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.", + "parameters": [ + { + "name": "name", + "description": "Cookie name.", + "type": "string" + }, + { + "name": "value", + "description": "Cookie value.", + "type": "string" + }, + { + "name": "url", + "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain and path values of the created cookie.", + "optional": true, + "type": "string" + }, + { + "name": "domain", + "description": "Cookie domain.", + "optional": true, + "type": "string" + }, + { + "name": "path", + "description": "Cookie path.", + "optional": true, + "type": "string" + }, + { + "name": "secure", + "description": "True if cookie is secure.", + "optional": true, + "type": "boolean" + }, + { + "name": "httpOnly", + "description": "True if cookie is http-only.", + "optional": true, + "type": "boolean" + }, + { + "name": "sameSite", + "description": "Cookie SameSite type.", + "optional": true, + "$ref": "CookieSameSite" + }, + { + "name": "expires", + "description": "Cookie expiration date, session cookie if not set", + "optional": true, + "$ref": "TimeSinceEpoch" + } + ], + "returns": [ + { + "name": "success", + "description": "True if successfully set cookie.", + "type": "boolean" + } + ] + }, + { + "name": "setCookies", + "description": "Sets given cookies.", + "parameters": [ + { + "name": "cookies", + "description": "Cookies to be set.", + "type": "array", + "items": { + "$ref": "CookieParam" + } + } + ] + }, + { + "name": "setDataSizeLimitsForTest", + "description": "For testing.", + "experimental": true, + "parameters": [ + { + "name": "maxTotalSize", + "description": "Maximum total buffer size.", + "type": "integer" + }, + { + "name": "maxResourceSize", + "description": "Maximum per-resource size.", + "type": "integer" + } + ] + }, + { + "name": "setExtraHTTPHeaders", + "description": "Specifies whether to always send extra HTTP headers with the requests from this page.", + "parameters": [ + { + "name": "headers", + "description": "Map with extra HTTP headers.", + "$ref": "Headers" + } + ] + }, + { + "name": "setRequestInterception", + "description": "Sets the requests to intercept that match a the provided patterns and optionally resource types.", + "experimental": true, + "parameters": [ + { + "name": "patterns", + "description": "Requests matching any of these patterns will be forwarded and wait for the corresponding\ncontinueInterceptedRequest call.", + "type": "array", + "items": { + "$ref": "RequestPattern" + } + } + ] + }, + { + "name": "setUserAgentOverride", + "description": "Allows overriding user agent with the given string.", + "redirect": "Emulation", + "parameters": [ + { + "name": "userAgent", + "description": "User agent to use.", + "type": "string" + }, + { + "name": "acceptLanguage", + "description": "Browser langugage to emulate.", + "optional": true, + "type": "string" + }, + { + "name": "platform", + "description": "The platform navigator.platform should return.", + "optional": true, + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "dataReceived", + "description": "Fired when data chunk was received over the network.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "dataLength", + "description": "Data chunk length.", + "type": "integer" + }, + { + "name": "encodedDataLength", + "description": "Actual bytes received (might be less than dataLength for compressed encodings).", + "type": "integer" + } + ] + }, + { + "name": "eventSourceMessageReceived", + "description": "Fired when EventSource message is received.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "eventName", + "description": "Message type.", + "type": "string" + }, + { + "name": "eventId", + "description": "Message identifier.", + "type": "string" + }, + { + "name": "data", + "description": "Message content.", + "type": "string" + } + ] + }, + { + "name": "loadingFailed", + "description": "Fired when HTTP request has failed to load.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "type", + "description": "Resource type.", + "$ref": "ResourceType" + }, + { + "name": "errorText", + "description": "User friendly error message.", + "type": "string" + }, + { + "name": "canceled", + "description": "True if loading was canceled.", + "optional": true, + "type": "boolean" + }, + { + "name": "blockedReason", + "description": "The reason why loading was blocked, if any.", + "optional": true, + "$ref": "BlockedReason" + } + ] + }, + { + "name": "loadingFinished", + "description": "Fired when HTTP request has finished loading.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "encodedDataLength", + "description": "Total number of bytes received for this request.", + "type": "number" + }, + { + "name": "shouldReportCorbBlocking", + "description": "Set when 1) response was blocked by Cross-Origin Read Blocking and also\n2) this needs to be reported to the DevTools console.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "requestIntercepted", + "description": "Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\nmocked.", + "experimental": true, + "parameters": [ + { + "name": "interceptionId", + "description": "Each request the page makes will have a unique id, however if any redirects are encountered\nwhile processing that fetch, they will be reported with the same id as the original fetch.\nLikewise if HTTP authentication is needed then the same fetch id will be used.", + "$ref": "InterceptionId" + }, + { + "name": "request", + "$ref": "Request" + }, + { + "name": "frameId", + "description": "The id of the frame that initiated the request.", + "$ref": "Page.FrameId" + }, + { + "name": "resourceType", + "description": "How the requested resource will be used.", + "$ref": "ResourceType" + }, + { + "name": "isNavigationRequest", + "description": "Whether this is a navigation request, which can abort the navigation completely.", + "type": "boolean" + }, + { + "name": "isDownload", + "description": "Set if the request is a navigation that will result in a download.\nOnly present after response is received from the server (i.e. HeadersReceived stage).", + "optional": true, + "type": "boolean" + }, + { + "name": "redirectUrl", + "description": "Redirect location, only sent if a redirect was intercepted.", + "optional": true, + "type": "string" + }, + { + "name": "authChallenge", + "description": "Details of the Authorization Challenge encountered. If this is set then\ncontinueInterceptedRequest must contain an authChallengeResponse.", + "optional": true, + "$ref": "AuthChallenge" + }, + { + "name": "responseErrorReason", + "description": "Response error if intercepted at response stage or if redirect occurred while intercepting\nrequest.", + "optional": true, + "$ref": "ErrorReason" + }, + { + "name": "responseStatusCode", + "description": "Response code if intercepted at response stage or if redirect occurred while intercepting\nrequest or auth retry occurred.", + "optional": true, + "type": "integer" + }, + { + "name": "responseHeaders", + "description": "Response headers if intercepted at the response stage or if redirect occurred while\nintercepting request or auth retry occurred.", + "optional": true, + "$ref": "Headers" + } + ] + }, + { + "name": "requestServedFromCache", + "description": "Fired if request ended up loading from cache.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + } + ] + }, + { + "name": "requestWillBeSent", + "description": "Fired when page is about to send HTTP request.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "loaderId", + "description": "Loader identifier. Empty string if the request is fetched from worker.", + "$ref": "LoaderId" + }, + { + "name": "documentURL", + "description": "URL of the document this request is loaded for.", + "type": "string" + }, + { + "name": "request", + "description": "Request data.", + "$ref": "Request" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "wallTime", + "description": "Timestamp.", + "$ref": "TimeSinceEpoch" + }, + { + "name": "initiator", + "description": "Request initiator.", + "$ref": "Initiator" + }, + { + "name": "redirectResponse", + "description": "Redirect response data.", + "optional": true, + "$ref": "Response" + }, + { + "name": "type", + "description": "Type of this resource.", + "optional": true, + "$ref": "ResourceType" + }, + { + "name": "frameId", + "description": "Frame identifier.", + "optional": true, + "$ref": "Page.FrameId" + }, + { + "name": "hasUserGesture", + "description": "Whether the request is initiated by a user gesture. Defaults to false.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "resourceChangedPriority", + "description": "Fired when resource loading priority is changed", + "experimental": true, + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "newPriority", + "description": "New priority", + "$ref": "ResourcePriority" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + } + ] + }, + { + "name": "signedExchangeReceived", + "description": "Fired when a signed exchange was received over the network", + "experimental": true, + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "info", + "description": "Information about the signed exchange response.", + "$ref": "SignedExchangeInfo" + } + ] + }, + { + "name": "responseReceived", + "description": "Fired when HTTP response is available.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "loaderId", + "description": "Loader identifier. Empty string if the request is fetched from worker.", + "$ref": "LoaderId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "type", + "description": "Resource type.", + "$ref": "ResourceType" + }, + { + "name": "response", + "description": "Response data.", + "$ref": "Response" + }, + { + "name": "frameId", + "description": "Frame identifier.", + "optional": true, + "$ref": "Page.FrameId" + } + ] + }, + { + "name": "webSocketClosed", + "description": "Fired when WebSocket is closed.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + } + ] + }, + { + "name": "webSocketCreated", + "description": "Fired upon WebSocket creation.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "url", + "description": "WebSocket request URL.", + "type": "string" + }, + { + "name": "initiator", + "description": "Request initiator.", + "optional": true, + "$ref": "Initiator" + } + ] + }, + { + "name": "webSocketFrameError", + "description": "Fired when WebSocket frame error occurs.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "errorMessage", + "description": "WebSocket frame error message.", + "type": "string" + } + ] + }, + { + "name": "webSocketFrameReceived", + "description": "Fired when WebSocket frame is received.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "response", + "description": "WebSocket response data.", + "$ref": "WebSocketFrame" + } + ] + }, + { + "name": "webSocketFrameSent", + "description": "Fired when WebSocket frame is sent.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "response", + "description": "WebSocket response data.", + "$ref": "WebSocketFrame" + } + ] + }, + { + "name": "webSocketHandshakeResponseReceived", + "description": "Fired when WebSocket handshake response becomes available.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "response", + "description": "WebSocket response data.", + "$ref": "WebSocketResponse" + } + ] + }, + { + "name": "webSocketWillSendHandshakeRequest", + "description": "Fired when WebSocket is about to initiate handshake.", + "parameters": [ + { + "name": "requestId", + "description": "Request identifier.", + "$ref": "RequestId" + }, + { + "name": "timestamp", + "description": "Timestamp.", + "$ref": "MonotonicTime" + }, + { + "name": "wallTime", + "description": "UTC Timestamp.", + "$ref": "TimeSinceEpoch" + }, + { + "name": "request", + "description": "WebSocket request data.", + "$ref": "WebSocketRequest" + } + ] + } + ] + }, + { + "domain": "Overlay", + "description": "This domain provides various functionality related to drawing atop the inspected page.", + "experimental": true, + "dependencies": [ + "DOM", + "Page", + "Runtime" + ], + "types": [ + { + "id": "HighlightConfig", + "description": "Configuration data for the highlighting of page elements.", + "type": "object", + "properties": [ + { + "name": "showInfo", + "description": "Whether the node info tooltip should be shown (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "showRulers", + "description": "Whether the rulers should be shown (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "showExtensionLines", + "description": "Whether the extension lines from node to the rulers should be shown (default: false).", + "optional": true, + "type": "boolean" + }, + { + "name": "displayAsMaterial", + "optional": true, + "type": "boolean" + }, + { + "name": "contentColor", + "description": "The content box highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "paddingColor", + "description": "The padding highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "borderColor", + "description": "The border highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "marginColor", + "description": "The margin highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "eventTargetColor", + "description": "The event target element highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "shapeColor", + "description": "The shape outside fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "shapeMarginColor", + "description": "The shape margin fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "selectorList", + "description": "Selectors to highlight relevant nodes.", + "optional": true, + "type": "string" + }, + { + "name": "cssGridColor", + "description": "The grid layout color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + } + ] + }, + { + "id": "InspectMode", + "type": "string", + "enum": [ + "searchForNode", + "searchForUAShadowDOM", + "none" + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables domain notifications." + }, + { + "name": "enable", + "description": "Enables domain notifications." + }, + { + "name": "getHighlightObjectForTest", + "description": "For testing.", + "parameters": [ + { + "name": "nodeId", + "description": "Id of the node to get highlight object for.", + "$ref": "DOM.NodeId" + } + ], + "returns": [ + { + "name": "highlight", + "description": "Highlight data for the node.", + "type": "object" + } + ] + }, + { + "name": "hideHighlight", + "description": "Hides any highlight." + }, + { + "name": "highlightFrame", + "description": "Highlights owner element of the frame with given id.", + "parameters": [ + { + "name": "frameId", + "description": "Identifier of the frame to highlight.", + "$ref": "Page.FrameId" + }, + { + "name": "contentColor", + "description": "The content box highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "contentOutlineColor", + "description": "The content box highlight outline color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + } + ] + }, + { + "name": "highlightNode", + "description": "Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\nobjectId must be specified.", + "parameters": [ + { + "name": "highlightConfig", + "description": "A descriptor for the highlight appearance.", + "$ref": "HighlightConfig" + }, + { + "name": "nodeId", + "description": "Identifier of the node to highlight.", + "optional": true, + "$ref": "DOM.NodeId" + }, + { + "name": "backendNodeId", + "description": "Identifier of the backend node to highlight.", + "optional": true, + "$ref": "DOM.BackendNodeId" + }, + { + "name": "objectId", + "description": "JavaScript object id of the node to be highlighted.", + "optional": true, + "$ref": "Runtime.RemoteObjectId" + } + ] + }, + { + "name": "highlightQuad", + "description": "Highlights given quad. Coordinates are absolute with respect to the main frame viewport.", + "parameters": [ + { + "name": "quad", + "description": "Quad to highlight", + "$ref": "DOM.Quad" + }, + { + "name": "color", + "description": "The highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "outlineColor", + "description": "The highlight outline color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + } + ] + }, + { + "name": "highlightRect", + "description": "Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.", + "parameters": [ + { + "name": "x", + "description": "X coordinate", + "type": "integer" + }, + { + "name": "y", + "description": "Y coordinate", + "type": "integer" + }, + { + "name": "width", + "description": "Rectangle width", + "type": "integer" + }, + { + "name": "height", + "description": "Rectangle height", + "type": "integer" + }, + { + "name": "color", + "description": "The highlight fill color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + }, + { + "name": "outlineColor", + "description": "The highlight outline color (default: transparent).", + "optional": true, + "$ref": "DOM.RGBA" + } + ] + }, + { + "name": "setInspectMode", + "description": "Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted.\nBackend then generates 'inspectNodeRequested' event upon element selection.", + "parameters": [ + { + "name": "mode", + "description": "Set an inspection mode.", + "$ref": "InspectMode" + }, + { + "name": "highlightConfig", + "description": "A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\n== false`.", + "optional": true, + "$ref": "HighlightConfig" + } + ] + }, + { + "name": "setPausedInDebuggerMessage", + "parameters": [ + { + "name": "message", + "description": "The message to display, also triggers resume and step over controls.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "setShowDebugBorders", + "description": "Requests that backend shows debug borders on layers", + "parameters": [ + { + "name": "show", + "description": "True for showing debug borders", + "type": "boolean" + } + ] + }, + { + "name": "setShowFPSCounter", + "description": "Requests that backend shows the FPS counter", + "parameters": [ + { + "name": "show", + "description": "True for showing the FPS counter", + "type": "boolean" + } + ] + }, + { + "name": "setShowPaintRects", + "description": "Requests that backend shows paint rectangles", + "parameters": [ + { + "name": "result", + "description": "True for showing paint rectangles", + "type": "boolean" + } + ] + }, + { + "name": "setShowScrollBottleneckRects", + "description": "Requests that backend shows scroll bottleneck rects", + "parameters": [ + { + "name": "show", + "description": "True for showing scroll bottleneck rects", + "type": "boolean" + } + ] + }, + { + "name": "setShowHitTestBorders", + "description": "Requests that backend shows hit-test borders on layers", + "parameters": [ + { + "name": "show", + "description": "True for showing hit-test borders", + "type": "boolean" + } + ] + }, + { + "name": "setShowViewportSizeOnResize", + "description": "Paints viewport size upon main frame resize.", + "parameters": [ + { + "name": "show", + "description": "Whether to paint size or not.", + "type": "boolean" + } + ] + }, + { + "name": "setSuspended", + "parameters": [ + { + "name": "suspended", + "description": "Whether overlay should be suspended and not consume any resources until resumed.", + "type": "boolean" + } + ] + } + ], + "events": [ + { + "name": "inspectNodeRequested", + "description": "Fired when the node should be inspected. This happens after call to `setInspectMode` or when\nuser manually inspects an element.", + "parameters": [ + { + "name": "backendNodeId", + "description": "Id of the node to inspect.", + "$ref": "DOM.BackendNodeId" + } + ] + }, + { + "name": "nodeHighlightRequested", + "description": "Fired when the node should be highlighted. This happens after call to `setInspectMode`.", + "parameters": [ + { + "name": "nodeId", + "$ref": "DOM.NodeId" + } + ] + }, + { + "name": "screenshotRequested", + "description": "Fired when user asks to capture screenshot of some area on the page.", + "parameters": [ + { + "name": "viewport", + "description": "Viewport to capture, in CSS.", + "$ref": "Page.Viewport" + } + ] + } + ] + }, + { + "domain": "Page", + "description": "Actions and events related to the inspected page belong to the page domain.", + "dependencies": [ + "Debugger", + "DOM", + "Network", + "Runtime" + ], + "types": [ + { + "id": "FrameId", + "description": "Unique frame identifier.", + "type": "string" + }, + { + "id": "Frame", + "description": "Information about the Frame on the page.", + "type": "object", + "properties": [ + { + "name": "id", + "description": "Frame unique identifier.", + "type": "string" + }, + { + "name": "parentId", + "description": "Parent frame identifier.", + "optional": true, + "type": "string" + }, + { + "name": "loaderId", + "description": "Identifier of the loader associated with this frame.", + "$ref": "Network.LoaderId" + }, + { + "name": "name", + "description": "Frame's name as specified in the tag.", + "optional": true, + "type": "string" + }, + { + "name": "url", + "description": "Frame document's URL.", + "type": "string" + }, + { + "name": "securityOrigin", + "description": "Frame document's security origin.", + "type": "string" + }, + { + "name": "mimeType", + "description": "Frame document's mimeType as determined by the browser.", + "type": "string" + }, + { + "name": "unreachableUrl", + "description": "If the frame failed to load, this contains the URL that could not be loaded.", + "experimental": true, + "optional": true, + "type": "string" + } + ] + }, + { + "id": "FrameResource", + "description": "Information about the Resource on the page.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "url", + "description": "Resource URL.", + "type": "string" + }, + { + "name": "type", + "description": "Type of this resource.", + "$ref": "Network.ResourceType" + }, + { + "name": "mimeType", + "description": "Resource mimeType as determined by the browser.", + "type": "string" + }, + { + "name": "lastModified", + "description": "last-modified timestamp as reported by server.", + "optional": true, + "$ref": "Network.TimeSinceEpoch" + }, + { + "name": "contentSize", + "description": "Resource content size.", + "optional": true, + "type": "number" + }, + { + "name": "failed", + "description": "True if the resource failed to load.", + "optional": true, + "type": "boolean" + }, + { + "name": "canceled", + "description": "True if the resource was canceled during loading.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "id": "FrameResourceTree", + "description": "Information about the Frame hierarchy along with their cached resources.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "frame", + "description": "Frame information for this tree item.", + "$ref": "Frame" + }, + { + "name": "childFrames", + "description": "Child frames.", + "optional": true, + "type": "array", + "items": { + "$ref": "FrameResourceTree" + } + }, + { + "name": "resources", + "description": "Information about frame resources.", + "type": "array", + "items": { + "$ref": "FrameResource" + } + } + ] + }, + { + "id": "FrameTree", + "description": "Information about the Frame hierarchy.", + "type": "object", + "properties": [ + { + "name": "frame", + "description": "Frame information for this tree item.", + "$ref": "Frame" + }, + { + "name": "childFrames", + "description": "Child frames.", + "optional": true, + "type": "array", + "items": { + "$ref": "FrameTree" + } + } + ] + }, + { + "id": "ScriptIdentifier", + "description": "Unique script identifier.", + "type": "string" + }, + { + "id": "TransitionType", + "description": "Transition type.", + "type": "string", + "enum": [ + "link", + "typed", + "address_bar", + "auto_bookmark", + "auto_subframe", + "manual_subframe", + "generated", + "auto_toplevel", + "form_submit", + "reload", + "keyword", + "keyword_generated", + "other" + ] + }, + { + "id": "NavigationEntry", + "description": "Navigation history entry.", + "type": "object", + "properties": [ + { + "name": "id", + "description": "Unique id of the navigation history entry.", + "type": "integer" + }, + { + "name": "url", + "description": "URL of the navigation history entry.", + "type": "string" + }, + { + "name": "userTypedURL", + "description": "URL that the user typed in the url bar.", + "type": "string" + }, + { + "name": "title", + "description": "Title of the navigation history entry.", + "type": "string" + }, + { + "name": "transitionType", + "description": "Transition type.", + "$ref": "TransitionType" + } + ] + }, + { + "id": "ScreencastFrameMetadata", + "description": "Screencast frame metadata.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "offsetTop", + "description": "Top offset in DIP.", + "type": "number" + }, + { + "name": "pageScaleFactor", + "description": "Page scale factor.", + "type": "number" + }, + { + "name": "deviceWidth", + "description": "Device screen width in DIP.", + "type": "number" + }, + { + "name": "deviceHeight", + "description": "Device screen height in DIP.", + "type": "number" + }, + { + "name": "scrollOffsetX", + "description": "Position of horizontal scroll in CSS pixels.", + "type": "number" + }, + { + "name": "scrollOffsetY", + "description": "Position of vertical scroll in CSS pixels.", + "type": "number" + }, + { + "name": "timestamp", + "description": "Frame swap timestamp.", + "optional": true, + "$ref": "Network.TimeSinceEpoch" + } + ] + }, + { + "id": "DialogType", + "description": "Javascript dialog type.", + "type": "string", + "enum": [ + "alert", + "confirm", + "prompt", + "beforeunload" + ] + }, + { + "id": "AppManifestError", + "description": "Error while paring app manifest.", + "type": "object", + "properties": [ + { + "name": "message", + "description": "Error message.", + "type": "string" + }, + { + "name": "critical", + "description": "If criticial, this is a non-recoverable parse error.", + "type": "integer" + }, + { + "name": "line", + "description": "Error line.", + "type": "integer" + }, + { + "name": "column", + "description": "Error column.", + "type": "integer" + } + ] + }, + { + "id": "LayoutViewport", + "description": "Layout viewport position and dimensions.", + "type": "object", + "properties": [ + { + "name": "pageX", + "description": "Horizontal offset relative to the document (CSS pixels).", + "type": "integer" + }, + { + "name": "pageY", + "description": "Vertical offset relative to the document (CSS pixels).", + "type": "integer" + }, + { + "name": "clientWidth", + "description": "Width (CSS pixels), excludes scrollbar if present.", + "type": "integer" + }, + { + "name": "clientHeight", + "description": "Height (CSS pixels), excludes scrollbar if present.", + "type": "integer" + } + ] + }, + { + "id": "VisualViewport", + "description": "Visual viewport position, dimensions, and scale.", + "type": "object", + "properties": [ + { + "name": "offsetX", + "description": "Horizontal offset relative to the layout viewport (CSS pixels).", + "type": "number" + }, + { + "name": "offsetY", + "description": "Vertical offset relative to the layout viewport (CSS pixels).", + "type": "number" + }, + { + "name": "pageX", + "description": "Horizontal offset relative to the document (CSS pixels).", + "type": "number" + }, + { + "name": "pageY", + "description": "Vertical offset relative to the document (CSS pixels).", + "type": "number" + }, + { + "name": "clientWidth", + "description": "Width (CSS pixels), excludes scrollbar if present.", + "type": "number" + }, + { + "name": "clientHeight", + "description": "Height (CSS pixels), excludes scrollbar if present.", + "type": "number" + }, + { + "name": "scale", + "description": "Scale relative to the ideal viewport (size at width=device-width).", + "type": "number" + } + ] + }, + { + "id": "Viewport", + "description": "Viewport for capturing screenshot.", + "type": "object", + "properties": [ + { + "name": "x", + "description": "X offset in CSS pixels.", + "type": "number" + }, + { + "name": "y", + "description": "Y offset in CSS pixels", + "type": "number" + }, + { + "name": "width", + "description": "Rectangle width in CSS pixels", + "type": "number" + }, + { + "name": "height", + "description": "Rectangle height in CSS pixels", + "type": "number" + }, + { + "name": "scale", + "description": "Page scale factor.", + "type": "number" + } + ] + }, + { + "id": "FontFamilies", + "description": "Generic font families collection.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "standard", + "description": "The standard font-family.", + "optional": true, + "type": "string" + }, + { + "name": "fixed", + "description": "The fixed font-family.", + "optional": true, + "type": "string" + }, + { + "name": "serif", + "description": "The serif font-family.", + "optional": true, + "type": "string" + }, + { + "name": "sansSerif", + "description": "The sansSerif font-family.", + "optional": true, + "type": "string" + }, + { + "name": "cursive", + "description": "The cursive font-family.", + "optional": true, + "type": "string" + }, + { + "name": "fantasy", + "description": "The fantasy font-family.", + "optional": true, + "type": "string" + }, + { + "name": "pictograph", + "description": "The pictograph font-family.", + "optional": true, + "type": "string" + } + ] + }, + { + "id": "FontSizes", + "description": "Default font sizes.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "standard", + "description": "Default standard font size.", + "optional": true, + "type": "integer" + }, + { + "name": "fixed", + "description": "Default fixed font size.", + "optional": true, + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "addScriptToEvaluateOnLoad", + "description": "Deprecated, please use addScriptToEvaluateOnNewDocument instead.", + "experimental": true, + "deprecated": true, + "parameters": [ + { + "name": "scriptSource", + "type": "string" + } + ], + "returns": [ + { + "name": "identifier", + "description": "Identifier of the added script.", + "$ref": "ScriptIdentifier" + } + ] + }, + { + "name": "addScriptToEvaluateOnNewDocument", + "description": "Evaluates given script in every frame upon creation (before loading frame's scripts).", + "parameters": [ + { + "name": "source", + "type": "string" + }, + { + "name": "worldName", + "description": "If specified, creates an isolated world with the given name and evaluates given script in it.\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\nevent is emitted.", + "experimental": true, + "optional": true, + "type": "string" + } + ], + "returns": [ + { + "name": "identifier", + "description": "Identifier of the added script.", + "$ref": "ScriptIdentifier" + } + ] + }, + { + "name": "bringToFront", + "description": "Brings page to front (activates tab)." + }, + { + "name": "captureScreenshot", + "description": "Capture page screenshot.", + "parameters": [ + { + "name": "format", + "description": "Image compression format (defaults to png).", + "optional": true, + "type": "string", + "enum": [ + "jpeg", + "png" + ] + }, + { + "name": "quality", + "description": "Compression quality from range [0..100] (jpeg only).", + "optional": true, + "type": "integer" + }, + { + "name": "clip", + "description": "Capture the screenshot of a given region only.", + "optional": true, + "$ref": "Viewport" + }, + { + "name": "fromSurface", + "description": "Capture the screenshot from the surface, rather than the view. Defaults to true.", + "experimental": true, + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "data", + "description": "Base64-encoded image data.", + "type": "binary" + } + ] + }, + { + "name": "captureSnapshot", + "description": "Returns a snapshot of the page as a string. For MHTML format, the serialization includes\niframes, shadow DOM, external resources, and element-inline styles.", + "experimental": true, + "parameters": [ + { + "name": "format", + "description": "Format (defaults to mhtml).", + "optional": true, + "type": "string", + "enum": [ + "mhtml" + ] + } + ], + "returns": [ + { + "name": "data", + "description": "Serialized page data.", + "type": "string" + } + ] + }, + { + "name": "clearDeviceMetricsOverride", + "description": "Clears the overriden device metrics.", + "experimental": true, + "deprecated": true, + "redirect": "Emulation" + }, + { + "name": "clearDeviceOrientationOverride", + "description": "Clears the overridden Device Orientation.", + "experimental": true, + "deprecated": true, + "redirect": "DeviceOrientation" + }, + { + "name": "clearGeolocationOverride", + "description": "Clears the overriden Geolocation Position and Error.", + "deprecated": true, + "redirect": "Emulation" + }, + { + "name": "createIsolatedWorld", + "description": "Creates an isolated world for the given frame.", + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame in which the isolated world should be created.", + "$ref": "FrameId" + }, + { + "name": "worldName", + "description": "An optional name which is reported in the Execution Context.", + "optional": true, + "type": "string" + }, + { + "name": "grantUniveralAccess", + "description": "Whether or not universal access should be granted to the isolated world. This is a powerful\noption, use with caution.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "executionContextId", + "description": "Execution context of the isolated world.", + "$ref": "Runtime.ExecutionContextId" + } + ] + }, + { + "name": "deleteCookie", + "description": "Deletes browser cookie with given name, domain and path.", + "experimental": true, + "deprecated": true, + "redirect": "Network", + "parameters": [ + { + "name": "cookieName", + "description": "Name of the cookie to remove.", + "type": "string" + }, + { + "name": "url", + "description": "URL to match cooke domain and path.", + "type": "string" + } + ] + }, + { + "name": "disable", + "description": "Disables page domain notifications." + }, + { + "name": "enable", + "description": "Enables page domain notifications." + }, + { + "name": "getAppManifest", + "returns": [ + { + "name": "url", + "description": "Manifest location.", + "type": "string" + }, + { + "name": "errors", + "type": "array", + "items": { + "$ref": "AppManifestError" + } + }, + { + "name": "data", + "description": "Manifest content.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "getCookies", + "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie\ninformation in the `cookies` field.", + "experimental": true, + "deprecated": true, + "redirect": "Network", + "returns": [ + { + "name": "cookies", + "description": "Array of cookie objects.", + "type": "array", + "items": { + "$ref": "Network.Cookie" + } + } + ] + }, + { + "name": "getFrameTree", + "description": "Returns present frame tree structure.", + "returns": [ + { + "name": "frameTree", + "description": "Present frame tree structure.", + "$ref": "FrameTree" + } + ] + }, + { + "name": "getLayoutMetrics", + "description": "Returns metrics relating to the layouting of the page, such as viewport bounds/scale.", + "returns": [ + { + "name": "layoutViewport", + "description": "Metrics relating to the layout viewport.", + "$ref": "LayoutViewport" + }, + { + "name": "visualViewport", + "description": "Metrics relating to the visual viewport.", + "$ref": "VisualViewport" + }, + { + "name": "contentSize", + "description": "Size of scrollable area.", + "$ref": "DOM.Rect" + } + ] + }, + { + "name": "getNavigationHistory", + "description": "Returns navigation history for the current page.", + "returns": [ + { + "name": "currentIndex", + "description": "Index of the current navigation history entry.", + "type": "integer" + }, + { + "name": "entries", + "description": "Array of navigation history entries.", + "type": "array", + "items": { + "$ref": "NavigationEntry" + } + } + ] + }, + { + "name": "getResourceContent", + "description": "Returns content of the given resource.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Frame id to get resource for.", + "$ref": "FrameId" + }, + { + "name": "url", + "description": "URL of the resource to get content for.", + "type": "string" + } + ], + "returns": [ + { + "name": "content", + "description": "Resource content.", + "type": "string" + }, + { + "name": "base64Encoded", + "description": "True, if content was served as base64.", + "type": "boolean" + } + ] + }, + { + "name": "getResourceTree", + "description": "Returns present frame / resource tree structure.", + "experimental": true, + "returns": [ + { + "name": "frameTree", + "description": "Present frame / resource tree structure.", + "$ref": "FrameResourceTree" + } + ] + }, + { + "name": "handleJavaScriptDialog", + "description": "Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).", + "parameters": [ + { + "name": "accept", + "description": "Whether to accept or dismiss the dialog.", + "type": "boolean" + }, + { + "name": "promptText", + "description": "The text to enter into the dialog prompt before accepting. Used only if this is a prompt\ndialog.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "navigate", + "description": "Navigates current page to the given URL.", + "parameters": [ + { + "name": "url", + "description": "URL to navigate the page to.", + "type": "string" + }, + { + "name": "referrer", + "description": "Referrer URL.", + "optional": true, + "type": "string" + }, + { + "name": "transitionType", + "description": "Intended transition type.", + "optional": true, + "$ref": "TransitionType" + }, + { + "name": "frameId", + "description": "Frame id to navigate, if not specified navigates the top frame.", + "optional": true, + "$ref": "FrameId" + } + ], + "returns": [ + { + "name": "frameId", + "description": "Frame id that has navigated (or failed to navigate)", + "$ref": "FrameId" + }, + { + "name": "loaderId", + "description": "Loader identifier.", + "optional": true, + "$ref": "Network.LoaderId" + }, + { + "name": "errorText", + "description": "User friendly error message, present if and only if navigation has failed.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "navigateToHistoryEntry", + "description": "Navigates current page to the given history entry.", + "parameters": [ + { + "name": "entryId", + "description": "Unique id of the entry to navigate to.", + "type": "integer" + } + ] + }, + { + "name": "printToPDF", + "description": "Print page as PDF.", + "parameters": [ + { + "name": "landscape", + "description": "Paper orientation. Defaults to false.", + "optional": true, + "type": "boolean" + }, + { + "name": "displayHeaderFooter", + "description": "Display header and footer. Defaults to false.", + "optional": true, + "type": "boolean" + }, + { + "name": "printBackground", + "description": "Print background graphics. Defaults to false.", + "optional": true, + "type": "boolean" + }, + { + "name": "scale", + "description": "Scale of the webpage rendering. Defaults to 1.", + "optional": true, + "type": "number" + }, + { + "name": "paperWidth", + "description": "Paper width in inches. Defaults to 8.5 inches.", + "optional": true, + "type": "number" + }, + { + "name": "paperHeight", + "description": "Paper height in inches. Defaults to 11 inches.", + "optional": true, + "type": "number" + }, + { + "name": "marginTop", + "description": "Top margin in inches. Defaults to 1cm (~0.4 inches).", + "optional": true, + "type": "number" + }, + { + "name": "marginBottom", + "description": "Bottom margin in inches. Defaults to 1cm (~0.4 inches).", + "optional": true, + "type": "number" + }, + { + "name": "marginLeft", + "description": "Left margin in inches. Defaults to 1cm (~0.4 inches).", + "optional": true, + "type": "number" + }, + { + "name": "marginRight", + "description": "Right margin in inches. Defaults to 1cm (~0.4 inches).", + "optional": true, + "type": "number" + }, + { + "name": "pageRanges", + "description": "Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means\nprint all pages.", + "optional": true, + "type": "string" + }, + { + "name": "ignoreInvalidPageRanges", + "description": "Whether to silently ignore invalid but successfully parsed page ranges, such as '3-2'.\nDefaults to false.", + "optional": true, + "type": "boolean" + }, + { + "name": "headerTemplate", + "description": "HTML template for the print header. Should be valid HTML markup with following\nclasses used to inject printing values into them:\n- `date`: formatted print date\n- `title`: document title\n- `url`: document location\n- `pageNumber`: current page number\n- `totalPages`: total pages in the document\n\nFor example, `<span class=title></span>` would generate span containing the title.", + "optional": true, + "type": "string" + }, + { + "name": "footerTemplate", + "description": "HTML template for the print footer. Should use the same format as the `headerTemplate`.", + "optional": true, + "type": "string" + }, + { + "name": "preferCSSPageSize", + "description": "Whether or not to prefer page size as defined by css. Defaults to false,\nin which case the content will be scaled to fit the paper size.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "data", + "description": "Base64-encoded pdf data.", + "type": "binary" + } + ] + }, + { + "name": "reload", + "description": "Reloads given page optionally ignoring the cache.", + "parameters": [ + { + "name": "ignoreCache", + "description": "If true, browser cache is ignored (as if the user pressed Shift+refresh).", + "optional": true, + "type": "boolean" + }, + { + "name": "scriptToEvaluateOnLoad", + "description": "If set, the script will be injected into all frames of the inspected page after reload.\nArgument will be ignored if reloading dataURL origin.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "removeScriptToEvaluateOnLoad", + "description": "Deprecated, please use removeScriptToEvaluateOnNewDocument instead.", + "experimental": true, + "deprecated": true, + "parameters": [ + { + "name": "identifier", + "$ref": "ScriptIdentifier" + } + ] + }, + { + "name": "removeScriptToEvaluateOnNewDocument", + "description": "Removes given script from the list.", + "parameters": [ + { + "name": "identifier", + "$ref": "ScriptIdentifier" + } + ] + }, + { + "name": "requestAppBanner", + "experimental": true + }, + { + "name": "screencastFrameAck", + "description": "Acknowledges that a screencast frame has been received by the frontend.", + "experimental": true, + "parameters": [ + { + "name": "sessionId", + "description": "Frame number.", + "type": "integer" + } + ] + }, + { + "name": "searchInResource", + "description": "Searches for given string in resource content.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Frame id for resource to search in.", + "$ref": "FrameId" + }, + { + "name": "url", + "description": "URL of the resource to search in.", + "type": "string" + }, + { + "name": "query", + "description": "String to search for.", + "type": "string" + }, + { + "name": "caseSensitive", + "description": "If true, search is case sensitive.", + "optional": true, + "type": "boolean" + }, + { + "name": "isRegex", + "description": "If true, treats string parameter as regex.", + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "result", + "description": "List of search matches.", + "type": "array", + "items": { + "$ref": "Debugger.SearchMatch" + } + } + ] + }, + { + "name": "setAdBlockingEnabled", + "description": "Enable Chrome's experimental ad filter on all sites.", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "description": "Whether to block ads.", + "type": "boolean" + } + ] + }, + { + "name": "setBypassCSP", + "description": "Enable page Content Security Policy by-passing.", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "description": "Whether to bypass page CSP.", + "type": "boolean" + } + ] + }, + { + "name": "setDeviceMetricsOverride", + "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).", + "experimental": true, + "deprecated": true, + "redirect": "Emulation", + "parameters": [ + { + "name": "width", + "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.", + "type": "integer" + }, + { + "name": "height", + "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.", + "type": "integer" + }, + { + "name": "deviceScaleFactor", + "description": "Overriding device scale factor value. 0 disables the override.", + "type": "number" + }, + { + "name": "mobile", + "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.", + "type": "boolean" + }, + { + "name": "scale", + "description": "Scale to apply to resulting view image.", + "optional": true, + "type": "number" + }, + { + "name": "screenWidth", + "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).", + "optional": true, + "type": "integer" + }, + { + "name": "screenHeight", + "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).", + "optional": true, + "type": "integer" + }, + { + "name": "positionX", + "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).", + "optional": true, + "type": "integer" + }, + { + "name": "positionY", + "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).", + "optional": true, + "type": "integer" + }, + { + "name": "dontSetVisibleSize", + "description": "Do not set visible view size, rely upon explicit setVisibleSize call.", + "optional": true, + "type": "boolean" + }, + { + "name": "screenOrientation", + "description": "Screen orientation override.", + "optional": true, + "$ref": "Emulation.ScreenOrientation" + }, + { + "name": "viewport", + "description": "The viewport dimensions and scale. If not set, the override is cleared.", + "optional": true, + "$ref": "Viewport" + } + ] + }, + { + "name": "setDeviceOrientationOverride", + "description": "Overrides the Device Orientation.", + "experimental": true, + "deprecated": true, + "redirect": "DeviceOrientation", + "parameters": [ + { + "name": "alpha", + "description": "Mock alpha", + "type": "number" + }, + { + "name": "beta", + "description": "Mock beta", + "type": "number" + }, + { + "name": "gamma", + "description": "Mock gamma", + "type": "number" + } + ] + }, + { + "name": "setFontFamilies", + "description": "Set generic font families.", + "experimental": true, + "parameters": [ + { + "name": "fontFamilies", + "description": "Specifies font families to set. If a font family is not specified, it won't be changed.", + "$ref": "FontFamilies" + } + ] + }, + { + "name": "setFontSizes", + "description": "Set default font sizes.", + "experimental": true, + "parameters": [ + { + "name": "fontSizes", + "description": "Specifies font sizes to set. If a font size is not specified, it won't be changed.", + "$ref": "FontSizes" + } + ] + }, + { + "name": "setDocumentContent", + "description": "Sets given markup as the document's HTML.", + "parameters": [ + { + "name": "frameId", + "description": "Frame id to set HTML for.", + "$ref": "FrameId" + }, + { + "name": "html", + "description": "HTML content to set.", + "type": "string" + } + ] + }, + { + "name": "setDownloadBehavior", + "description": "Set the behavior when downloading a file.", + "experimental": true, + "parameters": [ + { + "name": "behavior", + "description": "Whether to allow all or deny all download requests, or use default Chrome behavior if\navailable (otherwise deny).", + "type": "string", + "enum": [ + "deny", + "allow", + "default" + ] + }, + { + "name": "downloadPath", + "description": "The default path to save downloaded files to. This is requred if behavior is set to 'allow'", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "setGeolocationOverride", + "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.", + "deprecated": true, + "redirect": "Emulation", + "parameters": [ + { + "name": "latitude", + "description": "Mock latitude", + "optional": true, + "type": "number" + }, + { + "name": "longitude", + "description": "Mock longitude", + "optional": true, + "type": "number" + }, + { + "name": "accuracy", + "description": "Mock accuracy", + "optional": true, + "type": "number" + } + ] + }, + { + "name": "setLifecycleEventsEnabled", + "description": "Controls whether page will emit lifecycle events.", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "description": "If true, starts emitting lifecycle events.", + "type": "boolean" + } + ] + }, + { + "name": "setTouchEmulationEnabled", + "description": "Toggles mouse event-based touch event emulation.", + "experimental": true, + "deprecated": true, + "redirect": "Emulation", + "parameters": [ + { + "name": "enabled", + "description": "Whether the touch event emulation should be enabled.", + "type": "boolean" + }, + { + "name": "configuration", + "description": "Touch/gesture events configuration. Default: current platform.", + "optional": true, + "type": "string", + "enum": [ + "mobile", + "desktop" + ] + } + ] + }, + { + "name": "startScreencast", + "description": "Starts sending each frame using the `screencastFrame` event.", + "experimental": true, + "parameters": [ + { + "name": "format", + "description": "Image compression format.", + "optional": true, + "type": "string", + "enum": [ + "jpeg", + "png" + ] + }, + { + "name": "quality", + "description": "Compression quality from range [0..100].", + "optional": true, + "type": "integer" + }, + { + "name": "maxWidth", + "description": "Maximum screenshot width.", + "optional": true, + "type": "integer" + }, + { + "name": "maxHeight", + "description": "Maximum screenshot height.", + "optional": true, + "type": "integer" + }, + { + "name": "everyNthFrame", + "description": "Send every n-th frame.", + "optional": true, + "type": "integer" + } + ] + }, + { + "name": "stopLoading", + "description": "Force the page stop all navigations and pending resource fetches." + }, + { + "name": "crash", + "description": "Crashes renderer on the IO thread, generates minidumps.", + "experimental": true + }, + { + "name": "close", + "description": "Tries to close page, running its beforeunload hooks, if any.", + "experimental": true + }, + { + "name": "setWebLifecycleState", + "description": "Tries to update the web lifecycle state of the page.\nIt will transition the page to the given state according to:\nhttps://github.com/WICG/web-lifecycle/", + "experimental": true, + "parameters": [ + { + "name": "state", + "description": "Target lifecycle state", + "type": "string", + "enum": [ + "frozen", + "active" + ] + } + ] + }, + { + "name": "stopScreencast", + "description": "Stops sending each frame in the `screencastFrame`.", + "experimental": true + }, + { + "name": "setProduceCompilationCache", + "description": "Forces compilation cache to be generated for every subresource script.", + "experimental": true, + "parameters": [ + { + "name": "enabled", + "type": "boolean" + } + ] + }, + { + "name": "addCompilationCache", + "description": "Seeds compilation cache for given url. Compilation cache does not survive\ncross-process navigation.", + "experimental": true, + "parameters": [ + { + "name": "url", + "type": "string" + }, + { + "name": "data", + "description": "Base64-encoded data", + "type": "binary" + } + ] + }, + { + "name": "clearCompilationCache", + "description": "Clears seeded compilation cache.", + "experimental": true + }, + { + "name": "generateTestReport", + "description": "Generates a report for testing.", + "experimental": true, + "parameters": [ + { + "name": "message", + "description": "Message to be displayed in the report.", + "type": "string" + }, + { + "name": "group", + "description": "Specifies the endpoint group to deliver the report to.", + "optional": true, + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "domContentEventFired", + "parameters": [ + { + "name": "timestamp", + "$ref": "Network.MonotonicTime" + } + ] + }, + { + "name": "frameAttached", + "description": "Fired when frame has been attached to its parent.", + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has been attached.", + "$ref": "FrameId" + }, + { + "name": "parentFrameId", + "description": "Parent frame identifier.", + "$ref": "FrameId" + }, + { + "name": "stack", + "description": "JavaScript stack trace of when frame was attached, only set if frame initiated from script.", + "optional": true, + "$ref": "Runtime.StackTrace" + } + ] + }, + { + "name": "frameClearedScheduledNavigation", + "description": "Fired when frame no longer has a scheduled navigation.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has cleared its scheduled navigation.", + "$ref": "FrameId" + } + ] + }, + { + "name": "frameDetached", + "description": "Fired when frame has been detached from its parent.", + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has been detached.", + "$ref": "FrameId" + } + ] + }, + { + "name": "frameNavigated", + "description": "Fired once navigation of the frame has completed. Frame is now associated with the new loader.", + "parameters": [ + { + "name": "frame", + "description": "Frame object.", + "$ref": "Frame" + } + ] + }, + { + "name": "frameResized", + "experimental": true + }, + { + "name": "frameScheduledNavigation", + "description": "Fired when frame schedules a potential navigation.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has scheduled a navigation.", + "$ref": "FrameId" + }, + { + "name": "delay", + "description": "Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\nguaranteed to start.", + "type": "number" + }, + { + "name": "reason", + "description": "The reason for the navigation.", + "type": "string", + "enum": [ + "formSubmissionGet", + "formSubmissionPost", + "httpHeaderRefresh", + "scriptInitiated", + "metaTagRefresh", + "pageBlockInterstitial", + "reload" + ] + }, + { + "name": "url", + "description": "The destination URL for the scheduled navigation.", + "type": "string" + } + ] + }, + { + "name": "frameStartedLoading", + "description": "Fired when frame has started loading.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has started loading.", + "$ref": "FrameId" + } + ] + }, + { + "name": "frameStoppedLoading", + "description": "Fired when frame has stopped loading.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame that has stopped loading.", + "$ref": "FrameId" + } + ] + }, + { + "name": "interstitialHidden", + "description": "Fired when interstitial page was hidden" + }, + { + "name": "interstitialShown", + "description": "Fired when interstitial page was shown" + }, + { + "name": "javascriptDialogClosed", + "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\nclosed.", + "parameters": [ + { + "name": "result", + "description": "Whether dialog was confirmed.", + "type": "boolean" + }, + { + "name": "userInput", + "description": "User input in case of prompt.", + "type": "string" + } + ] + }, + { + "name": "javascriptDialogOpening", + "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\nopen.", + "parameters": [ + { + "name": "url", + "description": "Frame url.", + "type": "string" + }, + { + "name": "message", + "description": "Message that will be displayed by the dialog.", + "type": "string" + }, + { + "name": "type", + "description": "Dialog type.", + "$ref": "DialogType" + }, + { + "name": "hasBrowserHandler", + "description": "True iff browser is capable showing or acting on the given dialog. When browser has no\ndialog handler for given target, calling alert while Page domain is engaged will stall\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.", + "type": "boolean" + }, + { + "name": "defaultPrompt", + "description": "Default dialog prompt.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "lifecycleEvent", + "description": "Fired for top level page lifecycle events such as navigation, load, paint, etc.", + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame.", + "$ref": "FrameId" + }, + { + "name": "loaderId", + "description": "Loader identifier. Empty string if the request is fetched from worker.", + "$ref": "Network.LoaderId" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "timestamp", + "$ref": "Network.MonotonicTime" + } + ] + }, + { + "name": "loadEventFired", + "parameters": [ + { + "name": "timestamp", + "$ref": "Network.MonotonicTime" + } + ] + }, + { + "name": "navigatedWithinDocument", + "description": "Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.", + "experimental": true, + "parameters": [ + { + "name": "frameId", + "description": "Id of the frame.", + "$ref": "FrameId" + }, + { + "name": "url", + "description": "Frame's new url.", + "type": "string" + } + ] + }, + { + "name": "screencastFrame", + "description": "Compressed image data requested by the `startScreencast`.", + "experimental": true, + "parameters": [ + { + "name": "data", + "description": "Base64-encoded compressed image.", + "type": "binary" + }, + { + "name": "metadata", + "description": "Screencast frame metadata.", + "$ref": "ScreencastFrameMetadata" + }, + { + "name": "sessionId", + "description": "Frame number.", + "type": "integer" + } + ] + }, + { + "name": "screencastVisibilityChanged", + "description": "Fired when the page with currently enabled screencast was shown or hidden `.", + "experimental": true, + "parameters": [ + { + "name": "visible", + "description": "True if the page is visible.", + "type": "boolean" + } + ] + }, + { + "name": "windowOpen", + "description": "Fired when a new window is going to be opened, via window.open(), link click, form submission,\netc.", + "parameters": [ + { + "name": "url", + "description": "The URL for the new window.", + "type": "string" + }, + { + "name": "windowName", + "description": "Window name.", + "type": "string" + }, + { + "name": "windowFeatures", + "description": "An array of enabled window features.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "userGesture", + "description": "Whether or not it was triggered by user gesture.", + "type": "boolean" + } + ] + }, + { + "name": "compilationCacheProduced", + "description": "Issued for every compilation cache generated. Is only available\nif Page.setGenerateCompilationCache is enabled.", + "experimental": true, + "parameters": [ + { + "name": "url", + "type": "string" + }, + { + "name": "data", + "description": "Base64-encoded data", + "type": "binary" + } + ] + } + ] + }, + { + "domain": "Performance", + "types": [ + { + "id": "Metric", + "description": "Run-time execution metric.", + "type": "object", + "properties": [ + { + "name": "name", + "description": "Metric name.", + "type": "string" + }, + { + "name": "value", + "description": "Metric value.", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disable collecting and reporting metrics." + }, + { + "name": "enable", + "description": "Enable collecting and reporting metrics." + }, + { + "name": "setTimeDomain", + "description": "Sets time domain to use for collecting and reporting duration metrics.\nNote that this must be called before enabling metrics collection. Calling\nthis method while metrics collection is enabled returns an error.", + "experimental": true, + "parameters": [ + { + "name": "timeDomain", + "description": "Time domain", + "type": "string", + "enum": [ + "timeTicks", + "threadTicks" + ] + } + ] + }, + { + "name": "getMetrics", + "description": "Retrieve current values of run-time metrics.", + "returns": [ + { + "name": "metrics", + "description": "Current values for run-time metrics.", + "type": "array", + "items": { + "$ref": "Metric" + } + } + ] + } + ], + "events": [ + { + "name": "metrics", + "description": "Current values of the metrics.", + "parameters": [ + { + "name": "metrics", + "description": "Current values of the metrics.", + "type": "array", + "items": { + "$ref": "Metric" + } + }, + { + "name": "title", + "description": "Timestamp title.", + "type": "string" + } + ] + } + ] + }, + { + "domain": "Security", + "description": "Security", + "types": [ + { + "id": "CertificateId", + "description": "An internal certificate ID value.", + "type": "integer" + }, + { + "id": "MixedContentType", + "description": "A description of mixed content (HTTP resources on HTTPS pages), as defined by\nhttps://www.w3.org/TR/mixed-content/#categories", + "type": "string", + "enum": [ + "blockable", + "optionally-blockable", + "none" + ] + }, + { + "id": "SecurityState", + "description": "The security level of a page or resource.", + "type": "string", + "enum": [ + "unknown", + "neutral", + "insecure", + "secure", + "info" + ] + }, + { + "id": "SecurityStateExplanation", + "description": "An explanation of an factor contributing to the security state.", + "type": "object", + "properties": [ + { + "name": "securityState", + "description": "Security state representing the severity of the factor being explained.", + "$ref": "SecurityState" + }, + { + "name": "title", + "description": "Title describing the type of factor.", + "type": "string" + }, + { + "name": "summary", + "description": "Short phrase describing the type of factor.", + "type": "string" + }, + { + "name": "description", + "description": "Full text explanation of the factor.", + "type": "string" + }, + { + "name": "mixedContentType", + "description": "The type of mixed content described by the explanation.", + "$ref": "MixedContentType" + }, + { + "name": "certificate", + "description": "Page certificate.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "recommendations", + "description": "Recommendations to fix any issues.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "id": "InsecureContentStatus", + "description": "Information about insecure content on the page.", + "type": "object", + "properties": [ + { + "name": "ranMixedContent", + "description": "True if the page was loaded over HTTPS and ran mixed (HTTP) content such as scripts.", + "type": "boolean" + }, + { + "name": "displayedMixedContent", + "description": "True if the page was loaded over HTTPS and displayed mixed (HTTP) content such as images.", + "type": "boolean" + }, + { + "name": "containedMixedForm", + "description": "True if the page was loaded over HTTPS and contained a form targeting an insecure url.", + "type": "boolean" + }, + { + "name": "ranContentWithCertErrors", + "description": "True if the page was loaded over HTTPS without certificate errors, and ran content such as\nscripts that were loaded with certificate errors.", + "type": "boolean" + }, + { + "name": "displayedContentWithCertErrors", + "description": "True if the page was loaded over HTTPS without certificate errors, and displayed content\nsuch as images that were loaded with certificate errors.", + "type": "boolean" + }, + { + "name": "ranInsecureContentStyle", + "description": "Security state representing a page that ran insecure content.", + "$ref": "SecurityState" + }, + { + "name": "displayedInsecureContentStyle", + "description": "Security state representing a page that displayed insecure content.", + "$ref": "SecurityState" + } + ] + }, + { + "id": "CertificateErrorAction", + "description": "The action to take when a certificate error occurs. continue will continue processing the\nrequest and cancel will cancel the request.", + "type": "string", + "enum": [ + "continue", + "cancel" + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables tracking security state changes." + }, + { + "name": "enable", + "description": "Enables tracking security state changes." + }, + { + "name": "setIgnoreCertificateErrors", + "description": "Enable/disable whether all certificate errors should be ignored.", + "experimental": true, + "parameters": [ + { + "name": "ignore", + "description": "If true, all certificate errors will be ignored.", + "type": "boolean" + } + ] + }, + { + "name": "handleCertificateError", + "description": "Handles a certificate error that fired a certificateError event.", + "deprecated": true, + "parameters": [ + { + "name": "eventId", + "description": "The ID of the event.", + "type": "integer" + }, + { + "name": "action", + "description": "The action to take on the certificate error.", + "$ref": "CertificateErrorAction" + } + ] + }, + { + "name": "setOverrideCertificateErrors", + "description": "Enable/disable overriding certificate errors. If enabled, all certificate error events need to\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.", + "deprecated": true, + "parameters": [ + { + "name": "override", + "description": "If true, certificate errors will be overridden.", + "type": "boolean" + } + ] + } + ], + "events": [ + { + "name": "certificateError", + "description": "There is a certificate error. If overriding certificate errors is enabled, then it should be\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\ncertificate error has been allowed internally. Only one client per target should override\ncertificate errors at the same time.", + "deprecated": true, + "parameters": [ + { + "name": "eventId", + "description": "The ID of the event.", + "type": "integer" + }, + { + "name": "errorType", + "description": "The type of the error.", + "type": "string" + }, + { + "name": "requestURL", + "description": "The url that was requested.", + "type": "string" + } + ] + }, + { + "name": "securityStateChanged", + "description": "The security state of the page changed.", + "parameters": [ + { + "name": "securityState", + "description": "Security state.", + "$ref": "SecurityState" + }, + { + "name": "schemeIsCryptographic", + "description": "True if the page was loaded over cryptographic transport such as HTTPS.", + "type": "boolean" + }, + { + "name": "explanations", + "description": "List of explanations for the security state. If the overall security state is `insecure` or\n`warning`, at least one corresponding explanation should be included.", + "type": "array", + "items": { + "$ref": "SecurityStateExplanation" + } + }, + { + "name": "insecureContentStatus", + "description": "Information about insecure content on the page.", + "$ref": "InsecureContentStatus" + }, + { + "name": "summary", + "description": "Overrides user-visible description of the state.", + "optional": true, + "type": "string" + } + ] + } + ] + }, + { + "domain": "ServiceWorker", + "experimental": true, + "types": [ + { + "id": "ServiceWorkerRegistration", + "description": "ServiceWorker registration.", + "type": "object", + "properties": [ + { + "name": "registrationId", + "type": "string" + }, + { + "name": "scopeURL", + "type": "string" + }, + { + "name": "isDeleted", + "type": "boolean" + } + ] + }, + { + "id": "ServiceWorkerVersionRunningStatus", + "type": "string", + "enum": [ + "stopped", + "starting", + "running", + "stopping" + ] + }, + { + "id": "ServiceWorkerVersionStatus", + "type": "string", + "enum": [ + "new", + "installing", + "installed", + "activating", + "activated", + "redundant" + ] + }, + { + "id": "ServiceWorkerVersion", + "description": "ServiceWorker version.", + "type": "object", + "properties": [ + { + "name": "versionId", + "type": "string" + }, + { + "name": "registrationId", + "type": "string" + }, + { + "name": "scriptURL", + "type": "string" + }, + { + "name": "runningStatus", + "$ref": "ServiceWorkerVersionRunningStatus" + }, + { + "name": "status", + "$ref": "ServiceWorkerVersionStatus" + }, + { + "name": "scriptLastModified", + "description": "The Last-Modified header value of the main script.", + "optional": true, + "type": "number" + }, + { + "name": "scriptResponseTime", + "description": "The time at which the response headers of the main script were received from the server.\nFor cached script it is the last time the cache entry was validated.", + "optional": true, + "type": "number" + }, + { + "name": "controlledClients", + "optional": true, + "type": "array", + "items": { + "$ref": "Target.TargetID" + } + }, + { + "name": "targetId", + "optional": true, + "$ref": "Target.TargetID" + } + ] + }, + { + "id": "ServiceWorkerErrorMessage", + "description": "ServiceWorker error message.", + "type": "object", + "properties": [ + { + "name": "errorMessage", + "type": "string" + }, + { + "name": "registrationId", + "type": "string" + }, + { + "name": "versionId", + "type": "string" + }, + { + "name": "sourceURL", + "type": "string" + }, + { + "name": "lineNumber", + "type": "integer" + }, + { + "name": "columnNumber", + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "deliverPushMessage", + "parameters": [ + { + "name": "origin", + "type": "string" + }, + { + "name": "registrationId", + "type": "string" + }, + { + "name": "data", + "type": "string" + } + ] + }, + { + "name": "disable" + }, + { + "name": "dispatchSyncEvent", + "parameters": [ + { + "name": "origin", + "type": "string" + }, + { + "name": "registrationId", + "type": "string" + }, + { + "name": "tag", + "type": "string" + }, + { + "name": "lastChance", + "type": "boolean" + } + ] + }, + { + "name": "enable" + }, + { + "name": "inspectWorker", + "parameters": [ + { + "name": "versionId", + "type": "string" + } + ] + }, + { + "name": "setForceUpdateOnPageLoad", + "parameters": [ + { + "name": "forceUpdateOnPageLoad", + "type": "boolean" + } + ] + }, + { + "name": "skipWaiting", + "parameters": [ + { + "name": "scopeURL", + "type": "string" + } + ] + }, + { + "name": "startWorker", + "parameters": [ + { + "name": "scopeURL", + "type": "string" + } + ] + }, + { + "name": "stopAllWorkers" + }, + { + "name": "stopWorker", + "parameters": [ + { + "name": "versionId", + "type": "string" + } + ] + }, + { + "name": "unregister", + "parameters": [ + { + "name": "scopeURL", + "type": "string" + } + ] + }, + { + "name": "updateRegistration", + "parameters": [ + { + "name": "scopeURL", + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "workerErrorReported", + "parameters": [ + { + "name": "errorMessage", + "$ref": "ServiceWorkerErrorMessage" + } + ] + }, + { + "name": "workerRegistrationUpdated", + "parameters": [ + { + "name": "registrations", + "type": "array", + "items": { + "$ref": "ServiceWorkerRegistration" + } + } + ] + }, + { + "name": "workerVersionUpdated", + "parameters": [ + { + "name": "versions", + "type": "array", + "items": { + "$ref": "ServiceWorkerVersion" + } + } + ] + } + ] + }, + { + "domain": "Storage", + "experimental": true, + "types": [ + { + "id": "StorageType", + "description": "Enum of possible storage types.", + "type": "string", + "enum": [ + "appcache", + "cookies", + "file_systems", + "indexeddb", + "local_storage", + "shader_cache", + "websql", + "service_workers", + "cache_storage", + "all", + "other" + ] + }, + { + "id": "UsageForType", + "description": "Usage for a storage type.", + "type": "object", + "properties": [ + { + "name": "storageType", + "description": "Name of storage type.", + "$ref": "StorageType" + }, + { + "name": "usage", + "description": "Storage usage (bytes).", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "clearDataForOrigin", + "description": "Clears storage for origin.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + }, + { + "name": "storageTypes", + "description": "Comma separated origin names.", + "type": "string" + } + ] + }, + { + "name": "getUsageAndQuota", + "description": "Returns usage and quota in bytes.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + } + ], + "returns": [ + { + "name": "usage", + "description": "Storage usage (bytes).", + "type": "number" + }, + { + "name": "quota", + "description": "Storage quota (bytes).", + "type": "number" + }, + { + "name": "usageBreakdown", + "description": "Storage usage per type (bytes).", + "type": "array", + "items": { + "$ref": "UsageForType" + } + } + ] + }, + { + "name": "trackCacheStorageForOrigin", + "description": "Registers origin to be notified when an update occurs to its cache storage list.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + } + ] + }, + { + "name": "trackIndexedDBForOrigin", + "description": "Registers origin to be notified when an update occurs to its IndexedDB.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + } + ] + }, + { + "name": "untrackCacheStorageForOrigin", + "description": "Unregisters origin from receiving notifications for cache storage.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + } + ] + }, + { + "name": "untrackIndexedDBForOrigin", + "description": "Unregisters origin from receiving notifications for IndexedDB.", + "parameters": [ + { + "name": "origin", + "description": "Security origin.", + "type": "string" + } + ] + } + ], + "events": [ + { + "name": "cacheStorageContentUpdated", + "description": "A cache's contents have been modified.", + "parameters": [ + { + "name": "origin", + "description": "Origin to update.", + "type": "string" + }, + { + "name": "cacheName", + "description": "Name of cache in origin.", + "type": "string" + } + ] + }, + { + "name": "cacheStorageListUpdated", + "description": "A cache has been added/deleted.", + "parameters": [ + { + "name": "origin", + "description": "Origin to update.", + "type": "string" + } + ] + }, + { + "name": "indexedDBContentUpdated", + "description": "The origin's IndexedDB object store has been modified.", + "parameters": [ + { + "name": "origin", + "description": "Origin to update.", + "type": "string" + }, + { + "name": "databaseName", + "description": "Database to update.", + "type": "string" + }, + { + "name": "objectStoreName", + "description": "ObjectStore to update.", + "type": "string" + } + ] + }, + { + "name": "indexedDBListUpdated", + "description": "The origin's IndexedDB database list has been modified.", + "parameters": [ + { + "name": "origin", + "description": "Origin to update.", + "type": "string" + } + ] + } + ] + }, + { + "domain": "SystemInfo", + "description": "The SystemInfo domain defines methods and events for querying low-level system information.", + "experimental": true, + "types": [ + { + "id": "GPUDevice", + "description": "Describes a single graphics processor (GPU).", + "type": "object", + "properties": [ + { + "name": "vendorId", + "description": "PCI ID of the GPU vendor, if available; 0 otherwise.", + "type": "number" + }, + { + "name": "deviceId", + "description": "PCI ID of the GPU device, if available; 0 otherwise.", + "type": "number" + }, + { + "name": "vendorString", + "description": "String description of the GPU vendor, if the PCI ID is not available.", + "type": "string" + }, + { + "name": "deviceString", + "description": "String description of the GPU device, if the PCI ID is not available.", + "type": "string" + } + ] + }, + { + "id": "GPUInfo", + "description": "Provides information about the GPU(s) on the system.", + "type": "object", + "properties": [ + { + "name": "devices", + "description": "The graphics devices on the system. Element 0 is the primary GPU.", + "type": "array", + "items": { + "$ref": "GPUDevice" + } + }, + { + "name": "auxAttributes", + "description": "An optional dictionary of additional GPU related attributes.", + "optional": true, + "type": "object" + }, + { + "name": "featureStatus", + "description": "An optional dictionary of graphics features and their status.", + "optional": true, + "type": "object" + }, + { + "name": "driverBugWorkarounds", + "description": "An optional array of GPU driver bug workarounds.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "id": "ProcessInfo", + "description": "Represents process info.", + "type": "object", + "properties": [ + { + "name": "type", + "description": "Specifies process type.", + "type": "string" + }, + { + "name": "id", + "description": "Specifies process id.", + "type": "integer" + }, + { + "name": "cpuTime", + "description": "Specifies cumulative CPU usage in seconds across all threads of the\nprocess since the process start.", + "type": "number" + } + ] + } + ], + "commands": [ + { + "name": "getInfo", + "description": "Returns information about the system.", + "returns": [ + { + "name": "gpu", + "description": "Information about the GPUs on the system.", + "$ref": "GPUInfo" + }, + { + "name": "modelName", + "description": "A platform-dependent description of the model of the machine. On Mac OS, this is, for\nexample, 'MacBookPro'. Will be the empty string if not supported.", + "type": "string" + }, + { + "name": "modelVersion", + "description": "A platform-dependent description of the version of the machine. On Mac OS, this is, for\nexample, '10.1'. Will be the empty string if not supported.", + "type": "string" + }, + { + "name": "commandLine", + "description": "The command line string used to launch the browser. Will be the empty string if not\nsupported.", + "type": "string" + } + ] + }, + { + "name": "getProcessInfo", + "description": "Returns information about all running processes.", + "returns": [ + { + "name": "processInfo", + "description": "An array of process info blocks.", + "type": "array", + "items": { + "$ref": "ProcessInfo" + } + } + ] + } + ] + }, + { + "domain": "Target", + "description": "Supports additional targets discovery and allows to attach to them.", + "types": [ + { + "id": "TargetID", + "type": "string" + }, + { + "id": "SessionID", + "description": "Unique identifier of attached debugging session.", + "type": "string" + }, + { + "id": "BrowserContextID", + "experimental": true, + "type": "string" + }, + { + "id": "TargetInfo", + "type": "object", + "properties": [ + { + "name": "targetId", + "$ref": "TargetID" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "title", + "type": "string" + }, + { + "name": "url", + "type": "string" + }, + { + "name": "attached", + "description": "Whether the target has an attached client.", + "type": "boolean" + }, + { + "name": "openerId", + "description": "Opener target Id", + "optional": true, + "$ref": "TargetID" + }, + { + "name": "browserContextId", + "experimental": true, + "optional": true, + "$ref": "BrowserContextID" + } + ] + }, + { + "id": "RemoteLocation", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "host", + "type": "string" + }, + { + "name": "port", + "type": "integer" + } + ] + } + ], + "commands": [ + { + "name": "activateTarget", + "description": "Activates (focuses) the target.", + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + } + ] + }, + { + "name": "attachToTarget", + "description": "Attaches to the target with given id.", + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + }, + { + "name": "flatten", + "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.", + "experimental": true, + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "sessionId", + "description": "Id assigned to the session.", + "$ref": "SessionID" + } + ] + }, + { + "name": "attachToBrowserTarget", + "description": "Attaches to the browser target, only uses flat sessionId mode.", + "experimental": true, + "returns": [ + { + "name": "sessionId", + "description": "Id assigned to the session.", + "$ref": "SessionID" + } + ] + }, + { + "name": "closeTarget", + "description": "Closes the target. If the target is a page that gets closed too.", + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + } + ], + "returns": [ + { + "name": "success", + "type": "boolean" + } + ] + }, + { + "name": "exposeDevToolsProtocol", + "description": "Inject object to the target's main frame that provides a communication\nchannel with browser target.\n\nInjected object will be available as `window[bindingName]`.\n\nThe object has the follwing API:\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.", + "experimental": true, + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + }, + { + "name": "bindingName", + "description": "Binding name, 'cdp' if not specified.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "createBrowserContext", + "description": "Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\none.", + "experimental": true, + "returns": [ + { + "name": "browserContextId", + "description": "The id of the context created.", + "$ref": "BrowserContextID" + } + ] + }, + { + "name": "getBrowserContexts", + "description": "Returns all browser contexts created with `Target.createBrowserContext` method.", + "experimental": true, + "returns": [ + { + "name": "browserContextIds", + "description": "An array of browser context ids.", + "type": "array", + "items": { + "$ref": "BrowserContextID" + } + } + ] + }, + { + "name": "createTarget", + "description": "Creates a new page.", + "parameters": [ + { + "name": "url", + "description": "The initial URL the page will be navigated to.", + "type": "string" + }, + { + "name": "width", + "description": "Frame width in DIP (headless chrome only).", + "optional": true, + "type": "integer" + }, + { + "name": "height", + "description": "Frame height in DIP (headless chrome only).", + "optional": true, + "type": "integer" + }, + { + "name": "browserContextId", + "description": "The browser context to create the page in.", + "optional": true, + "$ref": "BrowserContextID" + }, + { + "name": "enableBeginFrameControl", + "description": "Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\nnot supported on MacOS yet, false by default).", + "experimental": true, + "optional": true, + "type": "boolean" + } + ], + "returns": [ + { + "name": "targetId", + "description": "The id of the page opened.", + "$ref": "TargetID" + } + ] + }, + { + "name": "detachFromTarget", + "description": "Detaches session with given id.", + "parameters": [ + { + "name": "sessionId", + "description": "Session to detach.", + "optional": true, + "$ref": "SessionID" + }, + { + "name": "targetId", + "description": "Deprecated.", + "deprecated": true, + "optional": true, + "$ref": "TargetID" + } + ] + }, + { + "name": "disposeBrowserContext", + "description": "Deletes a BrowserContext. All the belonging pages will be closed without calling their\nbeforeunload hooks.", + "experimental": true, + "parameters": [ + { + "name": "browserContextId", + "$ref": "BrowserContextID" + } + ] + }, + { + "name": "getTargetInfo", + "description": "Returns information about a target.", + "experimental": true, + "parameters": [ + { + "name": "targetId", + "optional": true, + "$ref": "TargetID" + } + ], + "returns": [ + { + "name": "targetInfo", + "$ref": "TargetInfo" + } + ] + }, + { + "name": "getTargets", + "description": "Retrieves a list of available targets.", + "returns": [ + { + "name": "targetInfos", + "description": "The list of targets.", + "type": "array", + "items": { + "$ref": "TargetInfo" + } + } + ] + }, + { + "name": "sendMessageToTarget", + "description": "Sends protocol message over session with given id.", + "parameters": [ + { + "name": "message", + "type": "string" + }, + { + "name": "sessionId", + "description": "Identifier of the session.", + "optional": true, + "$ref": "SessionID" + }, + { + "name": "targetId", + "description": "Deprecated.", + "deprecated": true, + "optional": true, + "$ref": "TargetID" + } + ] + }, + { + "name": "setAutoAttach", + "description": "Controls whether to automatically attach to new targets which are considered to be related to\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\nautomatically detaches from all currently attached targets.", + "experimental": true, + "parameters": [ + { + "name": "autoAttach", + "description": "Whether to auto-attach to related targets.", + "type": "boolean" + }, + { + "name": "waitForDebuggerOnStart", + "description": "Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\nto run paused targets.", + "type": "boolean" + }, + { + "name": "flatten", + "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.", + "experimental": true, + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "setDiscoverTargets", + "description": "Controls whether to discover available targets and notify via\n`targetCreated/targetInfoChanged/targetDestroyed` events.", + "parameters": [ + { + "name": "discover", + "description": "Whether to discover available targets.", + "type": "boolean" + } + ] + }, + { + "name": "setRemoteLocations", + "description": "Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\n`true`.", + "experimental": true, + "parameters": [ + { + "name": "locations", + "description": "List of remote locations.", + "type": "array", + "items": { + "$ref": "RemoteLocation" + } + } + ] + } + ], + "events": [ + { + "name": "attachedToTarget", + "description": "Issued when attached to target because of auto-attach or `attachToTarget` command.", + "experimental": true, + "parameters": [ + { + "name": "sessionId", + "description": "Identifier assigned to the session used to send/receive messages.", + "$ref": "SessionID" + }, + { + "name": "targetInfo", + "$ref": "TargetInfo" + }, + { + "name": "waitingForDebugger", + "type": "boolean" + } + ] + }, + { + "name": "detachedFromTarget", + "description": "Issued when detached from target for any reason (including `detachFromTarget` command). Can be\nissued multiple times per target if multiple sessions have been attached to it.", + "experimental": true, + "parameters": [ + { + "name": "sessionId", + "description": "Detached session identifier.", + "$ref": "SessionID" + }, + { + "name": "targetId", + "description": "Deprecated.", + "deprecated": true, + "optional": true, + "$ref": "TargetID" + } + ] + }, + { + "name": "receivedMessageFromTarget", + "description": "Notifies about a new protocol message received from the session (as reported in\n`attachedToTarget` event).", + "parameters": [ + { + "name": "sessionId", + "description": "Identifier of a session which sends a message.", + "$ref": "SessionID" + }, + { + "name": "message", + "type": "string" + }, + { + "name": "targetId", + "description": "Deprecated.", + "deprecated": true, + "optional": true, + "$ref": "TargetID" + } + ] + }, + { + "name": "targetCreated", + "description": "Issued when a possible inspection target is created.", + "parameters": [ + { + "name": "targetInfo", + "$ref": "TargetInfo" + } + ] + }, + { + "name": "targetDestroyed", + "description": "Issued when a target is destroyed.", + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + } + ] + }, + { + "name": "targetCrashed", + "description": "Issued when a target has crashed.", + "parameters": [ + { + "name": "targetId", + "$ref": "TargetID" + }, + { + "name": "status", + "description": "Termination status type.", + "type": "string" + }, + { + "name": "errorCode", + "description": "Termination error code.", + "type": "integer" + } + ] + }, + { + "name": "targetInfoChanged", + "description": "Issued when some information about a target has changed. This only happens between\n`targetCreated` and `targetDestroyed`.", + "parameters": [ + { + "name": "targetInfo", + "$ref": "TargetInfo" + } + ] + } + ] + }, + { + "domain": "Tethering", + "description": "The Tethering domain defines methods and events for browser port binding.", + "experimental": true, + "commands": [ + { + "name": "bind", + "description": "Request browser port binding.", + "parameters": [ + { + "name": "port", + "description": "Port number to bind.", + "type": "integer" + } + ] + }, + { + "name": "unbind", + "description": "Request browser port unbinding.", + "parameters": [ + { + "name": "port", + "description": "Port number to unbind.", + "type": "integer" + } + ] + } + ], + "events": [ + { + "name": "accepted", + "description": "Informs that port was successfully bound and got a specified connection id.", + "parameters": [ + { + "name": "port", + "description": "Port number that was successfully bound.", + "type": "integer" + }, + { + "name": "connectionId", + "description": "Connection id to be used.", + "type": "string" + } + ] + } + ] + }, + { + "domain": "Tracing", + "experimental": true, + "dependencies": [ + "IO" + ], + "types": [ + { + "id": "MemoryDumpConfig", + "description": "Configuration for memory dump. Used only when \"memory-infra\" category is enabled.", + "type": "object" + }, + { + "id": "TraceConfig", + "type": "object", + "properties": [ + { + "name": "recordMode", + "description": "Controls how the trace buffer stores data.", + "optional": true, + "type": "string", + "enum": [ + "recordUntilFull", + "recordContinuously", + "recordAsMuchAsPossible", + "echoToConsole" + ] + }, + { + "name": "enableSampling", + "description": "Turns on JavaScript stack sampling.", + "optional": true, + "type": "boolean" + }, + { + "name": "enableSystrace", + "description": "Turns on system tracing.", + "optional": true, + "type": "boolean" + }, + { + "name": "enableArgumentFilter", + "description": "Turns on argument filter.", + "optional": true, + "type": "boolean" + }, + { + "name": "includedCategories", + "description": "Included category filters.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "excludedCategories", + "description": "Excluded category filters.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "syntheticDelays", + "description": "Configuration to synthesize the delays in tracing.", + "optional": true, + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "memoryDumpConfig", + "description": "Configuration for memory dump triggers. Used only when \"memory-infra\" category is enabled.", + "optional": true, + "$ref": "MemoryDumpConfig" + } + ] + }, + { + "id": "StreamCompression", + "description": "Compression type to use for traces returned via streams.", + "type": "string", + "enum": [ + "none", + "gzip" + ] + } + ], + "commands": [ + { + "name": "end", + "description": "Stop trace events collection." + }, + { + "name": "getCategories", + "description": "Gets supported tracing categories.", + "returns": [ + { + "name": "categories", + "description": "A list of supported tracing categories.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "name": "recordClockSyncMarker", + "description": "Record a clock sync marker in the trace.", + "parameters": [ + { + "name": "syncId", + "description": "The ID of this clock sync marker", + "type": "string" + } + ] + }, + { + "name": "requestMemoryDump", + "description": "Request a global memory dump.", + "returns": [ + { + "name": "dumpGuid", + "description": "GUID of the resulting global memory dump.", + "type": "string" + }, + { + "name": "success", + "description": "True iff the global memory dump succeeded.", + "type": "boolean" + } + ] + }, + { + "name": "start", + "description": "Start trace events collection.", + "parameters": [ + { + "name": "categories", + "description": "Category/tag filter", + "deprecated": true, + "optional": true, + "type": "string" + }, + { + "name": "options", + "description": "Tracing options", + "deprecated": true, + "optional": true, + "type": "string" + }, + { + "name": "bufferUsageReportingInterval", + "description": "If set, the agent will issue bufferUsage events at this interval, specified in milliseconds", + "optional": true, + "type": "number" + }, + { + "name": "transferMode", + "description": "Whether to report trace events as series of dataCollected events or to save trace to a\nstream (defaults to `ReportEvents`).", + "optional": true, + "type": "string", + "enum": [ + "ReportEvents", + "ReturnAsStream" + ] + }, + { + "name": "streamCompression", + "description": "Compression format to use. This only applies when using `ReturnAsStream`\ntransfer mode (defaults to `none`)", + "optional": true, + "$ref": "StreamCompression" + }, + { + "name": "traceConfig", + "optional": true, + "$ref": "TraceConfig" + } + ] + } + ], + "events": [ + { + "name": "bufferUsage", + "parameters": [ + { + "name": "percentFull", + "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.", + "optional": true, + "type": "number" + }, + { + "name": "eventCount", + "description": "An approximate number of events in the trace log.", + "optional": true, + "type": "number" + }, + { + "name": "value", + "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.", + "optional": true, + "type": "number" + } + ] + }, + { + "name": "dataCollected", + "description": "Contains an bucket of collected trace events. When tracing is stopped collected events will be\nsend as a sequence of dataCollected events followed by tracingComplete event.", + "parameters": [ + { + "name": "value", + "type": "array", + "items": { + "type": "object" + } + } + ] + }, + { + "name": "tracingComplete", + "description": "Signals that tracing is stopped and there is no trace buffers pending flush, all data were\ndelivered via dataCollected events.", + "parameters": [ + { + "name": "stream", + "description": "A handle of the stream that holds resulting trace data.", + "optional": true, + "$ref": "IO.StreamHandle" + }, + { + "name": "streamCompression", + "description": "Compression format of returned stream.", + "optional": true, + "$ref": "StreamCompression" + } + ] + } + ] + }, + { + "domain": "Testing", + "description": "Testing domain is a dumping ground for the capabilities requires for browser or app testing that do not fit other\ndomains.", + "experimental": true, + "dependencies": [ + "Page" + ], + "commands": [ + { + "name": "generateTestReport", + "description": "Generates a report for testing.", + "parameters": [ + { + "name": "message", + "description": "Message to be displayed in the report.", + "type": "string" + }, + { + "name": "group", + "description": "Specifies the endpoint group to deliver the report to.", + "optional": true, + "type": "string" + } + ] + } + ] + }, + { + "domain": "Fetch", + "description": "A domain for letting clients substitute browser's network layer with client code.", + "experimental": true, + "dependencies": [ + "Network", + "IO", + "Page" + ], + "types": [ + { + "id": "RequestId", + "description": "Unique request identifier.", + "type": "string" + }, + { + "id": "RequestStage", + "description": "Stages of the request to handle. Request will intercept before the request is\nsent. Response will intercept after the response is received (but before response\nbody is received.", + "experimental": true, + "type": "string", + "enum": [ + "Request", + "Response" + ] + }, + { + "id": "RequestPattern", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "urlPattern", + "description": "Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to \"*\".", + "optional": true, + "type": "string" + }, + { + "name": "resourceType", + "description": "If set, only requests for matching resource types will be intercepted.", + "optional": true, + "$ref": "Network.ResourceType" + }, + { + "name": "requestStage", + "description": "Stage at wich to begin intercepting requests. Default is Request.", + "optional": true, + "$ref": "RequestStage" + } + ] + }, + { + "id": "HeaderEntry", + "description": "Response HTTP header entry", + "type": "object", + "properties": [ + { + "name": "name", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + }, + { + "id": "AuthChallenge", + "description": "Authorization challenge for HTTP status code 401 or 407.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "source", + "description": "Source of the authentication challenge.", + "optional": true, + "type": "string", + "enum": [ + "Server", + "Proxy" + ] + }, + { + "name": "origin", + "description": "Origin of the challenger.", + "type": "string" + }, + { + "name": "scheme", + "description": "The authentication scheme used, such as basic or digest", + "type": "string" + }, + { + "name": "realm", + "description": "The realm of the challenge. May be empty.", + "type": "string" + } + ] + }, + { + "id": "AuthChallengeResponse", + "description": "Response to an AuthChallenge.", + "experimental": true, + "type": "object", + "properties": [ + { + "name": "response", + "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.", + "type": "string", + "enum": [ + "Default", + "CancelAuth", + "ProvideCredentials" + ] + }, + { + "name": "username", + "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.", + "optional": true, + "type": "string" + }, + { + "name": "password", + "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.", + "optional": true, + "type": "string" + } + ] + } + ], + "commands": [ + { + "name": "disable", + "description": "Disables the fetch domain." + }, + { + "name": "enable", + "description": "Enables issuing of requestPaused events. A request will be paused until client\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.", + "parameters": [ + { + "name": "patterns", + "description": "If specified, only requests matching any of these patterns will produce\nfetchRequested event and will be paused until clients response. If not set,\nall requests will be affected.", + "optional": true, + "type": "array", + "items": { + "$ref": "RequestPattern" + } + }, + { + "name": "handleAuthRequests", + "description": "If true, authRequired events will be issued and requests will be paused\nexpecting a call to continueWithAuth.", + "optional": true, + "type": "boolean" + } + ] + }, + { + "name": "failRequest", + "description": "Causes the request to fail with specified reason.", + "parameters": [ + { + "name": "requestId", + "description": "An id the client received in requestPaused event.", + "$ref": "RequestId" + }, + { + "name": "errorReason", + "description": "Causes the request to fail with the given reason.", + "$ref": "Network.ErrorReason" + } + ] + }, + { + "name": "fulfillRequest", + "description": "Provides response to the request.", + "parameters": [ + { + "name": "requestId", + "description": "An id the client received in requestPaused event.", + "$ref": "RequestId" + }, + { + "name": "responseCode", + "description": "An HTTP response code.", + "type": "integer" + }, + { + "name": "responseHeaders", + "description": "Response headers.", + "type": "array", + "items": { + "$ref": "HeaderEntry" + } + }, + { + "name": "body", + "description": "A response body.", + "optional": true, + "type": "binary" + }, + { + "name": "responsePhrase", + "description": "A textual representation of responseCode.\nIf absent, a standard phrase mathcing responseCode is used.", + "optional": true, + "type": "string" + } + ] + }, + { + "name": "continueRequest", + "description": "Continues the request, optionally modifying some of its parameters.", + "parameters": [ + { + "name": "requestId", + "description": "An id the client received in requestPaused event.", + "$ref": "RequestId" + }, + { + "name": "url", + "description": "If set, the request url will be modified in a way that's not observable by page.", + "optional": true, + "type": "string" + }, + { + "name": "method", + "description": "If set, the request method is overridden.", + "optional": true, + "type": "string" + }, + { + "name": "postData", + "description": "If set, overrides the post data in the request.", + "optional": true, + "type": "string" + }, + { + "name": "headers", + "description": "If set, overrides the request headrts.", + "optional": true, + "type": "array", + "items": { + "$ref": "HeaderEntry" + } + } + ] + }, + { + "name": "continueWithAuth", + "description": "Continues a request supplying authChallengeResponse following authRequired event.", + "parameters": [ + { + "name": "requestId", + "description": "An id the client received in authRequired event.", + "$ref": "RequestId" + }, + { + "name": "authChallengeResponse", + "description": "Response to with an authChallenge.", + "$ref": "AuthChallengeResponse" + } + ] + }, + { + "name": "getResponseBody", + "description": "Causes the body of the response to be received from the server and\nreturned as a single string. May only be issued for a request that\nis paused in the Response stage and is mutually exclusive with\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\naffect the request or disabling fetch domain before body is received\nresults in an undefined behavior.", + "parameters": [ + { + "name": "requestId", + "description": "Identifier for the intercepted request to get body for.", + "$ref": "RequestId" + } + ], + "returns": [ + { + "name": "body", + "description": "Response body.", + "type": "string" + }, + { + "name": "base64Encoded", + "description": "True, if content was sent as base64.", + "type": "boolean" + } + ] + }, + { + "name": "takeResponseBodyAsStream", + "description": "Returns a handle to the stream representing the response body.\nThe request must be paused in the HeadersReceived stage.\nNote that after this command the request can't be continued\nas is -- client either needs to cancel it or to provide the\nresponse body.\nThe stream only supports sequential read, IO.read will fail if the position\nis specified.\nThis method is mutually exclusive with getResponseBody.\nCalling other methods that affect the request or disabling fetch\ndomain before body is received results in an undefined behavior.", + "parameters": [ + { + "name": "requestId", + "$ref": "RequestId" + } + ], + "returns": [ + { + "name": "stream", + "$ref": "IO.StreamHandle" + } + ] + } + ], + "events": [ + { + "name": "requestPaused", + "description": "Issued when the domain is enabled and the request URL matches the\nspecified filter. The request is paused until the client responds\nwith one of continueRequest, failRequest or fulfillRequest.\nThe stage of the request can be determined by presence of responseErrorReason\nand responseStatusCode -- the request is at the response stage if either\nof these fields is present and in the request stage otherwise.", + "parameters": [ + { + "name": "requestId", + "description": "Each request the page makes will have a unique id.", + "$ref": "RequestId" + }, + { + "name": "request", + "description": "The details of the request.", + "$ref": "Network.Request" + }, + { + "name": "frameId", + "description": "The id of the frame that initiated the request.", + "$ref": "Page.FrameId" + }, + { + "name": "resourceType", + "description": "How the requested resource will be used.", + "$ref": "Network.ResourceType" + }, + { + "name": "responseErrorReason", + "description": "Response error if intercepted at response stage.", + "optional": true, + "$ref": "Network.ErrorReason" + }, + { + "name": "responseStatusCode", + "description": "Response code if intercepted at response stage.", + "optional": true, + "type": "integer" + }, + { + "name": "responseHeaders", + "description": "Response headers if intercepted at the response stage.", + "optional": true, + "type": "array", + "items": { + "$ref": "HeaderEntry" + } + } + ] + }, + { + "name": "authRequired", + "description": "Issued when the domain is enabled with handleAuthRequests set to true.\nThe request is paused until client responds with continueWithAuth.", + "parameters": [ + { + "name": "requestId", + "description": "Each request the page makes will have a unique id.", + "$ref": "RequestId" + }, + { + "name": "request", + "description": "The details of the request.", + "$ref": "Network.Request" + }, + { + "name": "frameId", + "description": "The id of the frame that initiated the request.", + "$ref": "Page.FrameId" + }, + { + "name": "resourceType", + "description": "How the requested resource will be used.", + "$ref": "Network.ResourceType" + }, + { + "name": "authChallenge", + "description": "Details of the Authorization Challenge encountered.\nIf this is set, client should respond with continueRequest that\ncontains AuthChallengeResponse.", + "$ref": "AuthChallenge" + } + ] + } + ] + }, + { + "commands": [ + { + "name": "clearMessages", + "description": "Does nothing." + }, + { + "name": "disable", + "description": "Disables console domain, prevents further console messages from being reported to the client." + }, + { + "name": "enable", + "description": "Enables console domain, sends the messages collected so far to the client by means of the\n`messageAdded` notification." + } + ], + "description": "This domain is deprecated - use Runtime or Log instead.", + "deprecated": true, + "domain": "Console", + "dependencies": [ + "Runtime" + ], + "events": [ + { + "name": "messageAdded", + "parameters": [ + { + "$ref": "ConsoleMessage", + "name": "message", + "description": "Console message that has been added." + } + ], + "description": "Issued when new console message is added." + } + ], + "types": [ + { + "properties": [ + { + "enum": [ + "xml", + "javascript", + "network", + "console-api", + "storage", + "appcache", + "rendering", + "security", + "other", + "deprecation", + "worker" + ], + "type": "string", + "name": "source", + "description": "Message source." + }, + { + "enum": [ + "log", + "warning", + "error", + "debug", + "info" + ], + "type": "string", + "name": "level", + "description": "Message severity." + }, + { + "type": "string", + "name": "text", + "description": "Message text." + }, + { + "type": "string", + "optional": true, + "name": "url", + "description": "URL of the message origin." + }, + { + "type": "integer", + "optional": true, + "name": "line", + "description": "Line number in the resource that generated this message (1-based)." + }, + { + "type": "integer", + "optional": true, + "name": "column", + "description": "Column number in the resource that generated this message (1-based)." + } + ], + "type": "object", + "id": "ConsoleMessage", + "description": "Console message." + } + ] + }, + { + "commands": [ + { + "name": "continueToLocation", + "parameters": [ + { + "$ref": "Location", + "name": "location", + "description": "Location to continue to." + }, + { + "type": "string", + "enum": [ + "any", + "current" + ], + "optional": true, + "name": "targetCallFrames" + } + ], + "description": "Continues execution until specific location is reached." + }, + { + "name": "disable", + "description": "Disables debugger for given page." + }, + { + "returns": [ + { + "$ref": "Runtime.UniqueDebuggerId", + "name": "debuggerId", + "experimental": true, + "description": "Unique identifier of the debugger." + } + ], + "name": "enable", + "description": "Enables debugger for the given page. Clients should not assume that the debugging has been\nenabled until the result for this command is received." + }, + { + "returns": [ + { + "$ref": "Runtime.RemoteObject", + "name": "result", + "description": "Object wrapper for the evaluation result." + }, + { + "$ref": "Runtime.ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "evaluateOnCallFrame", + "parameters": [ + { + "$ref": "CallFrameId", + "name": "callFrameId", + "description": "Call frame identifier to evaluate on." + }, + { + "type": "string", + "name": "expression", + "description": "Expression to evaluate." + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "String object group name to put result into (allows rapid releasing resulting object handles\nusing `releaseObjectGroup`)." + }, + { + "type": "boolean", + "optional": true, + "name": "includeCommandLineAPI", + "description": "Specifies whether command line API should be available to the evaluated expression, defaults\nto false." + }, + { + "type": "boolean", + "optional": true, + "name": "silent", + "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state." + }, + { + "type": "boolean", + "optional": true, + "name": "returnByValue", + "description": "Whether the result is expected to be a JSON object that should be sent by value." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "experimental": true, + "description": "Whether preview should be generated for the result." + }, + { + "type": "boolean", + "optional": true, + "name": "throwOnSideEffect", + "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation." + }, + { + "$ref": "Runtime.TimeDelta", + "optional": true, + "name": "timeout", + "experimental": true, + "description": "Terminate execution after timing out (number of milliseconds)." + } + ], + "description": "Evaluates expression on a given call frame." + }, + { + "returns": [ + { + "items": { + "$ref": "BreakLocation" + }, + "type": "array", + "name": "locations", + "description": "List of the possible breakpoint locations." + } + ], + "name": "getPossibleBreakpoints", + "parameters": [ + { + "$ref": "Location", + "name": "start", + "description": "Start of range to search possible breakpoint locations in." + }, + { + "$ref": "Location", + "optional": true, + "name": "end", + "description": "End of range to search possible breakpoint locations in (excluding). When not specified, end\nof scripts is used as end of range." + }, + { + "type": "boolean", + "optional": true, + "name": "restrictToFunction", + "description": "Only consider locations which are in the same (non-nested) function as start." + } + ], + "description": "Returns possible locations for breakpoint. scriptId in start and end range locations should be\nthe same." + }, + { + "returns": [ + { + "type": "string", + "name": "scriptSource", + "description": "Script source." + } + ], + "name": "getScriptSource", + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Id of the script to get source for." + } + ], + "description": "Returns source for the script with given id." + }, + { + "returns": [ + { + "name": "stackTrace", + "$ref": "Runtime.StackTrace" + } + ], + "parameters": [ + { + "name": "stackTraceId", + "$ref": "Runtime.StackTraceId" + } + ], + "name": "getStackTrace", + "experimental": true, + "description": "Returns stack trace with given `stackTraceId`." + }, + { + "name": "pause", + "description": "Stops on the next JavaScript statement." + }, + { + "parameters": [ + { + "$ref": "Runtime.StackTraceId", + "name": "parentStackTraceId", + "description": "Debugger will pause when async call with given stack trace is started." + } + ], + "name": "pauseOnAsyncCall", + "experimental": true + }, + { + "name": "removeBreakpoint", + "parameters": [ + { + "name": "breakpointId", + "$ref": "BreakpointId" + } + ], + "description": "Removes JavaScript breakpoint." + }, + { + "returns": [ + { + "items": { + "$ref": "CallFrame" + }, + "type": "array", + "name": "callFrames", + "description": "New stack trace." + }, + { + "$ref": "Runtime.StackTrace", + "optional": true, + "name": "asyncStackTrace", + "description": "Async stack trace, if any." + }, + { + "$ref": "Runtime.StackTraceId", + "optional": true, + "name": "asyncStackTraceId", + "experimental": true, + "description": "Async stack trace, if any." + } + ], + "name": "restartFrame", + "parameters": [ + { + "$ref": "CallFrameId", + "name": "callFrameId", + "description": "Call frame identifier to evaluate on." + } + ], + "description": "Restarts particular call frame from the beginning." + }, + { + "name": "resume", + "description": "Resumes JavaScript execution." + }, + { + "name": "scheduleStepIntoAsync", + "experimental": true, + "description": "This method is deprecated - use Debugger.stepInto with breakOnAsyncCall and\nDebugger.pauseOnAsyncTask instead. Steps into next scheduled async task if any is scheduled\nbefore next pause. Returns success when async task is actually scheduled, returns error if no\ntask were scheduled or another scheduleStepIntoAsync was called." + }, + { + "returns": [ + { + "items": { + "$ref": "SearchMatch" + }, + "type": "array", + "name": "result", + "description": "List of search matches." + } + ], + "name": "searchInContent", + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Id of the script to search in." + }, + { + "type": "string", + "name": "query", + "description": "String to search for." + }, + { + "type": "boolean", + "optional": true, + "name": "caseSensitive", + "description": "If true, search is case sensitive." + }, + { + "type": "boolean", + "optional": true, + "name": "isRegex", + "description": "If true, treats string parameter as regex." + } + ], + "description": "Searches for given string in script content." + }, + { + "name": "setAsyncCallStackDepth", + "parameters": [ + { + "type": "integer", + "name": "maxDepth", + "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default)." + } + ], + "description": "Enables or disables async call stacks tracking." + }, + { + "parameters": [ + { + "items": { + "type": "string" + }, + "type": "array", + "name": "patterns", + "description": "Array of regexps that will be used to check script url for blackbox state." + } + ], + "name": "setBlackboxPatterns", + "experimental": true, + "description": "Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\nperforming 'step in' several times, finally resorting to 'step out' if unsuccessful." + }, + { + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Id of the script." + }, + { + "items": { + "$ref": "ScriptPosition" + }, + "type": "array", + "name": "positions" + } + ], + "name": "setBlackboxedRanges", + "experimental": true, + "description": "Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\nscripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful.\nPositions array contains positions where blackbox state is changed. First interval isn't\nblackboxed. Array should be sorted." + }, + { + "returns": [ + { + "$ref": "BreakpointId", + "name": "breakpointId", + "description": "Id of the created breakpoint for further reference." + }, + { + "$ref": "Location", + "name": "actualLocation", + "description": "Location this breakpoint resolved into." + } + ], + "name": "setBreakpoint", + "parameters": [ + { + "$ref": "Location", + "name": "location", + "description": "Location to set breakpoint in." + }, + { + "type": "string", + "optional": true, + "name": "condition", + "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true." + } + ], + "description": "Sets JavaScript breakpoint at a given location." + }, + { + "returns": [ + { + "$ref": "BreakpointId", + "name": "breakpointId", + "description": "Id of the created breakpoint for further reference." + }, + { + "items": { + "$ref": "Location" + }, + "type": "array", + "name": "locations", + "description": "List of the locations this breakpoint resolved into upon addition." + } + ], + "name": "setBreakpointByUrl", + "parameters": [ + { + "type": "integer", + "name": "lineNumber", + "description": "Line number to set breakpoint at." + }, + { + "type": "string", + "optional": true, + "name": "url", + "description": "URL of the resources to set breakpoint on." + }, + { + "type": "string", + "optional": true, + "name": "urlRegex", + "description": "Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\n`urlRegex` must be specified." + }, + { + "type": "string", + "optional": true, + "name": "scriptHash", + "description": "Script hash of the resources to set breakpoint on." + }, + { + "type": "integer", + "optional": true, + "name": "columnNumber", + "description": "Offset in the line to set breakpoint at." + }, + { + "type": "string", + "optional": true, + "name": "condition", + "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true." + } + ], + "description": "Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\n`locations` property. Further matching script parsing will result in subsequent\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads." + }, + { + "returns": [ + { + "$ref": "BreakpointId", + "name": "breakpointId", + "description": "Id of the created breakpoint for further reference." + } + ], + "parameters": [ + { + "$ref": "Runtime.RemoteObjectId", + "name": "objectId", + "description": "Function object id." + }, + { + "type": "string", + "optional": true, + "name": "condition", + "description": "Expression to use as a breakpoint condition. When specified, debugger will\nstop on the breakpoint if this expression evaluates to true." + } + ], + "name": "setBreakpointOnFunctionCall", + "experimental": true, + "description": "Sets JavaScript breakpoint before each call to the given function.\nIf another function was created from the same source as a given one,\ncalling it will also trigger the breakpoint." + }, + { + "name": "setBreakpointsActive", + "parameters": [ + { + "type": "boolean", + "name": "active", + "description": "New value for breakpoints active state." + } + ], + "description": "Activates / deactivates all breakpoints on the page." + }, + { + "name": "setPauseOnExceptions", + "parameters": [ + { + "enum": [ + "none", + "uncaught", + "all" + ], + "type": "string", + "name": "state", + "description": "Pause on exceptions mode." + } + ], + "description": "Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or\nno exceptions. Initial pause on exceptions state is `none`." + }, + { + "parameters": [ + { + "$ref": "Runtime.CallArgument", + "name": "newValue", + "description": "New return value." + } + ], + "name": "setReturnValue", + "experimental": true, + "description": "Changes return value in top frame. Available only at return break position." + }, + { + "returns": [ + { + "type": "array", + "items": { + "$ref": "CallFrame" + }, + "optional": true, + "name": "callFrames", + "description": "New stack trace in case editing has happened while VM was stopped." + }, + { + "type": "boolean", + "optional": true, + "name": "stackChanged", + "description": "Whether current call stack was modified after applying the changes." + }, + { + "$ref": "Runtime.StackTrace", + "optional": true, + "name": "asyncStackTrace", + "description": "Async stack trace, if any." + }, + { + "$ref": "Runtime.StackTraceId", + "optional": true, + "name": "asyncStackTraceId", + "experimental": true, + "description": "Async stack trace, if any." + }, + { + "$ref": "Runtime.ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details if any." + } + ], + "name": "setScriptSource", + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Id of the script to edit." + }, + { + "type": "string", + "name": "scriptSource", + "description": "New content of the script." + }, + { + "type": "boolean", + "optional": true, + "name": "dryRun", + "description": "If true the change will not actually be applied. Dry run may be used to get result\ndescription without actually modifying the code." + } + ], + "description": "Edits JavaScript source live." + }, + { + "name": "setSkipAllPauses", + "parameters": [ + { + "type": "boolean", + "name": "skip", + "description": "New value for skip pauses state." + } + ], + "description": "Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc)." + }, + { + "name": "setVariableValue", + "parameters": [ + { + "type": "integer", + "name": "scopeNumber", + "description": "0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch'\nscope types are allowed. Other scopes could be manipulated manually." + }, + { + "type": "string", + "name": "variableName", + "description": "Variable name." + }, + { + "$ref": "Runtime.CallArgument", + "name": "newValue", + "description": "New variable value." + }, + { + "$ref": "CallFrameId", + "name": "callFrameId", + "description": "Id of callframe that holds variable." + } + ], + "description": "Changes value of variable in a callframe. Object-based scopes are not supported and must be\nmutated manually." + }, + { + "name": "stepInto", + "parameters": [ + { + "type": "boolean", + "optional": true, + "name": "breakOnAsyncCall", + "experimental": true, + "description": "Debugger will issue additional Debugger.paused notification if any async task is scheduled\nbefore next pause." + } + ], + "description": "Steps into the function call." + }, + { + "name": "stepOut", + "description": "Steps out of the function call." + }, + { + "name": "stepOver", + "description": "Steps over the statement." + } + ], + "description": "Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\nbreakpoints, stepping through execution, exploring stack traces, etc.", + "domain": "Debugger", + "dependencies": [ + "Runtime" + ], + "events": [ + { + "name": "breakpointResolved", + "parameters": [ + { + "$ref": "BreakpointId", + "name": "breakpointId", + "description": "Breakpoint unique identifier." + }, + { + "$ref": "Location", + "name": "location", + "description": "Actual breakpoint location." + } + ], + "description": "Fired when breakpoint is resolved to an actual script and location." + }, + { + "name": "paused", + "parameters": [ + { + "items": { + "$ref": "CallFrame" + }, + "type": "array", + "name": "callFrames", + "description": "Call stack the virtual machine stopped on." + }, + { + "enum": [ + "XHR", + "DOM", + "EventListener", + "exception", + "assert", + "debugCommand", + "promiseRejection", + "OOM", + "other", + "ambiguous" + ], + "type": "string", + "name": "reason", + "description": "Pause reason." + }, + { + "type": "object", + "optional": true, + "name": "data", + "description": "Object containing break-specific auxiliary properties." + }, + { + "type": "array", + "items": { + "type": "string" + }, + "optional": true, + "name": "hitBreakpoints", + "description": "Hit breakpoints IDs" + }, + { + "$ref": "Runtime.StackTrace", + "optional": true, + "name": "asyncStackTrace", + "description": "Async stack trace, if any." + }, + { + "$ref": "Runtime.StackTraceId", + "optional": true, + "name": "asyncStackTraceId", + "experimental": true, + "description": "Async stack trace, if any." + }, + { + "$ref": "Runtime.StackTraceId", + "optional": true, + "name": "asyncCallStackTraceId", + "experimental": true, + "description": "Just scheduled async call will have this stack trace as parent stack during async execution.\nThis field is available only after `Debugger.stepInto` call with `breakOnAsynCall` flag." + } + ], + "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria." + }, + { + "name": "resumed", + "description": "Fired when the virtual machine resumed execution." + }, + { + "name": "scriptFailedToParse", + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Identifier of the script parsed." + }, + { + "type": "string", + "name": "url", + "description": "URL or name of the script parsed (if any)." + }, + { + "type": "integer", + "name": "startLine", + "description": "Line offset of the script within the resource with given URL (for script tags)." + }, + { + "type": "integer", + "name": "startColumn", + "description": "Column offset of the script within the resource with given URL." + }, + { + "type": "integer", + "name": "endLine", + "description": "Last line of the script." + }, + { + "type": "integer", + "name": "endColumn", + "description": "Length of the last line of the script." + }, + { + "$ref": "Runtime.ExecutionContextId", + "name": "executionContextId", + "description": "Specifies script creation context." + }, + { + "type": "string", + "name": "hash", + "description": "Content hash of the script." + }, + { + "type": "object", + "optional": true, + "name": "executionContextAuxData", + "description": "Embedder-specific auxiliary data." + }, + { + "type": "string", + "optional": true, + "name": "sourceMapURL", + "description": "URL of source map associated with script (if any)." + }, + { + "type": "boolean", + "optional": true, + "name": "hasSourceURL", + "description": "True, if this script has sourceURL." + }, + { + "type": "boolean", + "optional": true, + "name": "isModule", + "description": "True, if this script is ES6 module." + }, + { + "type": "integer", + "optional": true, + "name": "length", + "description": "This script length." + }, + { + "$ref": "Runtime.StackTrace", + "optional": true, + "name": "stackTrace", + "experimental": true, + "description": "JavaScript top stack frame of where the script parsed event was triggered if available." + } + ], + "description": "Fired when virtual machine fails to parse the script." + }, + { + "name": "scriptParsed", + "parameters": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Identifier of the script parsed." + }, + { + "type": "string", + "name": "url", + "description": "URL or name of the script parsed (if any)." + }, + { + "type": "integer", + "name": "startLine", + "description": "Line offset of the script within the resource with given URL (for script tags)." + }, + { + "type": "integer", + "name": "startColumn", + "description": "Column offset of the script within the resource with given URL." + }, + { + "type": "integer", + "name": "endLine", + "description": "Last line of the script." + }, + { + "type": "integer", + "name": "endColumn", + "description": "Length of the last line of the script." + }, + { + "$ref": "Runtime.ExecutionContextId", + "name": "executionContextId", + "description": "Specifies script creation context." + }, + { + "type": "string", + "name": "hash", + "description": "Content hash of the script." + }, + { + "type": "object", + "optional": true, + "name": "executionContextAuxData", + "description": "Embedder-specific auxiliary data." + }, + { + "type": "boolean", + "optional": true, + "name": "isLiveEdit", + "experimental": true, + "description": "True, if this script is generated as a result of the live edit operation." + }, + { + "type": "string", + "optional": true, + "name": "sourceMapURL", + "description": "URL of source map associated with script (if any)." + }, + { + "type": "boolean", + "optional": true, + "name": "hasSourceURL", + "description": "True, if this script has sourceURL." + }, + { + "type": "boolean", + "optional": true, + "name": "isModule", + "description": "True, if this script is ES6 module." + }, + { + "type": "integer", + "optional": true, + "name": "length", + "description": "This script length." + }, + { + "$ref": "Runtime.StackTrace", + "optional": true, + "name": "stackTrace", + "experimental": true, + "description": "JavaScript top stack frame of where the script parsed event was triggered if available." + } + ], + "description": "Fired when virtual machine parses script. This event is also fired for all known and uncollected\nscripts upon enabling debugger." + } + ], + "types": [ + { + "type": "string", + "id": "BreakpointId", + "description": "Breakpoint identifier." + }, + { + "type": "string", + "id": "CallFrameId", + "description": "Call frame identifier." + }, + { + "properties": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Script identifier as reported in the `Debugger.scriptParsed`." + }, + { + "type": "integer", + "name": "lineNumber", + "description": "Line number in the script (0-based)." + }, + { + "type": "integer", + "optional": true, + "name": "columnNumber", + "description": "Column number in the script (0-based)." + } + ], + "type": "object", + "id": "Location", + "description": "Location in the source code." + }, + { + "properties": [ + { + "type": "integer", + "name": "lineNumber" + }, + { + "type": "integer", + "name": "columnNumber" + } + ], + "type": "object", + "id": "ScriptPosition", + "experimental": true, + "description": "Location in the source code." + }, + { + "properties": [ + { + "$ref": "CallFrameId", + "name": "callFrameId", + "description": "Call frame identifier. This identifier is only valid while the virtual machine is paused." + }, + { + "type": "string", + "name": "functionName", + "description": "Name of the JavaScript function called on this call frame." + }, + { + "$ref": "Location", + "optional": true, + "name": "functionLocation", + "description": "Location in the source code." + }, + { + "$ref": "Location", + "name": "location", + "description": "Location in the source code." + }, + { + "type": "string", + "name": "url", + "description": "JavaScript script name or url." + }, + { + "items": { + "$ref": "Scope" + }, + "type": "array", + "name": "scopeChain", + "description": "Scope chain for this call frame." + }, + { + "$ref": "Runtime.RemoteObject", + "name": "this", + "description": "`this` object for this call frame." + }, + { + "$ref": "Runtime.RemoteObject", + "optional": true, + "name": "returnValue", + "description": "The value being returned, if the function is at return point." + } + ], + "type": "object", + "id": "CallFrame", + "description": "JavaScript call frame. Array of call frames form the call stack." + }, + { + "properties": [ + { + "enum": [ + "global", + "local", + "with", + "closure", + "catch", + "block", + "script", + "eval", + "module" + ], + "type": "string", + "name": "type", + "description": "Scope type." + }, + { + "$ref": "Runtime.RemoteObject", + "name": "object", + "description": "Object representing the scope. For `global` and `with` scopes it represents the actual\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\nvariables as its properties." + }, + { + "type": "string", + "optional": true, + "name": "name" + }, + { + "$ref": "Location", + "optional": true, + "name": "startLocation", + "description": "Location in the source code where scope starts" + }, + { + "$ref": "Location", + "optional": true, + "name": "endLocation", + "description": "Location in the source code where scope ends" + } + ], + "type": "object", + "id": "Scope", + "description": "Scope description." + }, + { + "properties": [ + { + "type": "number", + "name": "lineNumber", + "description": "Line number in resource content." + }, + { + "type": "string", + "name": "lineContent", + "description": "Line with match content." + } + ], + "type": "object", + "id": "SearchMatch", + "description": "Search match for resource." + }, + { + "type": "object", + "id": "BreakLocation", + "properties": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "Script identifier as reported in the `Debugger.scriptParsed`." + }, + { + "type": "integer", + "name": "lineNumber", + "description": "Line number in the script (0-based)." + }, + { + "type": "integer", + "optional": true, + "name": "columnNumber", + "description": "Column number in the script (0-based)." + }, + { + "type": "string", + "enum": [ + "debuggerStatement", + "call", + "return" + ], + "optional": true, + "name": "type" + } + ] + } + ] + }, + { + "commands": [ + { + "name": "addInspectedHeapObject", + "parameters": [ + { + "$ref": "HeapSnapshotObjectId", + "name": "heapObjectId", + "description": "Heap snapshot object id to be accessible by means of $x command line API." + } + ], + "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions)." + }, + { + "name": "collectGarbage" + }, + { + "name": "disable" + }, + { + "name": "enable" + }, + { + "returns": [ + { + "$ref": "HeapSnapshotObjectId", + "name": "heapSnapshotObjectId", + "description": "Id of the heap snapshot object corresponding to the passed remote object id." + } + ], + "name": "getHeapObjectId", + "parameters": [ + { + "$ref": "Runtime.RemoteObjectId", + "name": "objectId", + "description": "Identifier of the object to get heap object id for." + } + ] + }, + { + "returns": [ + { + "$ref": "Runtime.RemoteObject", + "name": "result", + "description": "Evaluation result." + } + ], + "name": "getObjectByHeapObjectId", + "parameters": [ + { + "name": "objectId", + "$ref": "HeapSnapshotObjectId" + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "Symbolic group name that can be used to release multiple objects." + } + ] + }, + { + "returns": [ + { + "$ref": "SamplingHeapProfile", + "name": "profile", + "description": "Return the sampling profile being collected." + } + ], + "name": "getSamplingProfile" + }, + { + "name": "startSampling", + "parameters": [ + { + "type": "number", + "optional": true, + "name": "samplingInterval", + "description": "Average sample interval in bytes. Poisson distribution is used for the intervals. The\ndefault value is 32768 bytes." + } + ] + }, + { + "name": "startTrackingHeapObjects", + "parameters": [ + { + "type": "boolean", + "optional": true, + "name": "trackAllocations" + } + ] + }, + { + "returns": [ + { + "$ref": "SamplingHeapProfile", + "name": "profile", + "description": "Recorded sampling heap profile." + } + ], + "name": "stopSampling" + }, + { + "name": "stopTrackingHeapObjects", + "parameters": [ + { + "type": "boolean", + "optional": true, + "name": "reportProgress", + "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken\nwhen the tracking is stopped." + } + ] + }, + { + "name": "takeHeapSnapshot", + "parameters": [ + { + "type": "boolean", + "optional": true, + "name": "reportProgress", + "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken." + } + ] + } + ], + "domain": "HeapProfiler", + "dependencies": [ + "Runtime" + ], + "experimental": true, + "events": [ + { + "name": "addHeapSnapshotChunk", + "parameters": [ + { + "type": "string", + "name": "chunk" + } + ] + }, + { + "name": "heapStatsUpdate", + "parameters": [ + { + "items": { + "type": "integer" + }, + "type": "array", + "name": "statsUpdate", + "description": "An array of triplets. Each triplet describes a fragment. The first integer is the fragment\nindex, the second integer is a total count of objects for the fragment, the third integer is\na total size of the objects for the fragment." + } + ], + "description": "If heap objects tracking has been started then backend may send update for one or more fragments" + }, + { + "name": "lastSeenObjectId", + "parameters": [ + { + "type": "integer", + "name": "lastSeenObjectId" + }, + { + "type": "number", + "name": "timestamp" + } + ], + "description": "If heap objects tracking has been started then backend regularly sends a current value for last\nseen object id and corresponding timestamp. If the were changes in the heap since last event\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event." + }, + { + "name": "reportHeapSnapshotProgress", + "parameters": [ + { + "type": "integer", + "name": "done" + }, + { + "type": "integer", + "name": "total" + }, + { + "type": "boolean", + "optional": true, + "name": "finished" + } + ] + }, + { + "name": "resetProfiles" + } + ], + "types": [ + { + "type": "string", + "id": "HeapSnapshotObjectId", + "description": "Heap snapshot object id." + }, + { + "properties": [ + { + "$ref": "Runtime.CallFrame", + "name": "callFrame", + "description": "Function location." + }, + { + "type": "number", + "name": "selfSize", + "description": "Allocations size in bytes for the node excluding children." + }, + { + "type": "integer", + "name": "id", + "description": "Node id. Ids are unique across all profiles collected between startSampling and stopSampling." + }, + { + "items": { + "$ref": "SamplingHeapProfileNode" + }, + "type": "array", + "name": "children", + "description": "Child nodes." + } + ], + "type": "object", + "id": "SamplingHeapProfileNode", + "description": "Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes." + }, + { + "properties": [ + { + "type": "number", + "name": "size", + "description": "Allocation size in bytes attributed to the sample." + }, + { + "type": "integer", + "name": "nodeId", + "description": "Id of the corresponding profile tree node." + }, + { + "type": "number", + "name": "ordinal", + "description": "Time-ordered sample ordinal number. It is unique across all profiles retrieved\nbetween startSampling and stopSampling." + } + ], + "type": "object", + "id": "SamplingHeapProfileSample", + "description": "A single sample from a sampling profile." + }, + { + "properties": [ + { + "name": "head", + "$ref": "SamplingHeapProfileNode" + }, + { + "items": { + "$ref": "SamplingHeapProfileSample" + }, + "type": "array", + "name": "samples" + } + ], + "type": "object", + "id": "SamplingHeapProfile", + "description": "Sampling profile." + } + ] + }, + { + "domain": "Profiler", + "dependencies": [ + "Runtime", + "Debugger" + ], + "commands": [ + { + "name": "disable" + }, + { + "name": "enable" + }, + { + "returns": [ + { + "items": { + "$ref": "ScriptCoverage" + }, + "type": "array", + "name": "result", + "description": "Coverage data for the current isolate." + } + ], + "name": "getBestEffortCoverage", + "description": "Collect coverage data for the current isolate. The coverage data may be incomplete due to\ngarbage collection." + }, + { + "name": "setSamplingInterval", + "parameters": [ + { + "type": "integer", + "name": "interval", + "description": "New sampling interval in microseconds." + } + ], + "description": "Changes CPU profiler sampling interval. Must be called before CPU profiles recording started." + }, + { + "name": "start" + }, + { + "name": "startPreciseCoverage", + "parameters": [ + { + "type": "boolean", + "optional": true, + "name": "callCount", + "description": "Collect accurate call counts beyond simple 'covered' or 'not covered'." + }, + { + "type": "boolean", + "optional": true, + "name": "detailed", + "description": "Collect block-based coverage." + } + ], + "description": "Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\ncounters." + }, + { + "name": "startTypeProfile", + "experimental": true, + "description": "Enable type profile." + }, + { + "returns": [ + { + "$ref": "Profile", + "name": "profile", + "description": "Recorded profile." + } + ], + "name": "stop" + }, + { + "name": "stopPreciseCoverage", + "description": "Disable precise code coverage. Disabling releases unnecessary execution count records and allows\nexecuting optimized code." + }, + { + "name": "stopTypeProfile", + "experimental": true, + "description": "Disable type profile. Disabling releases type profile data collected so far." + }, + { + "returns": [ + { + "items": { + "$ref": "ScriptCoverage" + }, + "type": "array", + "name": "result", + "description": "Coverage data for the current isolate." + } + ], + "name": "takePreciseCoverage", + "description": "Collect coverage data for the current isolate, and resets execution counters. Precise code\ncoverage needs to have started." + }, + { + "returns": [ + { + "items": { + "$ref": "ScriptTypeProfile" + }, + "type": "array", + "name": "result", + "description": "Type profile for all scripts since startTypeProfile() was turned on." + } + ], + "name": "takeTypeProfile", + "experimental": true, + "description": "Collect type profile." + } + ], + "types": [ + { + "properties": [ + { + "type": "integer", + "name": "id", + "description": "Unique id of the node." + }, + { + "$ref": "Runtime.CallFrame", + "name": "callFrame", + "description": "Function location." + }, + { + "type": "integer", + "optional": true, + "name": "hitCount", + "description": "Number of samples where this node was on top of the call stack." + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "optional": true, + "name": "children", + "description": "Child node ids." + }, + { + "type": "string", + "optional": true, + "name": "deoptReason", + "description": "The reason of being not optimized. The function may be deoptimized or marked as don't\noptimize." + }, + { + "type": "array", + "items": { + "$ref": "PositionTickInfo" + }, + "optional": true, + "name": "positionTicks", + "description": "An array of source position ticks." + } + ], + "type": "object", + "id": "ProfileNode", + "description": "Profile node. Holds callsite information, execution statistics and child nodes." + }, + { + "properties": [ + { + "items": { + "$ref": "ProfileNode" + }, + "type": "array", + "name": "nodes", + "description": "The list of profile nodes. First item is the root node." + }, + { + "type": "number", + "name": "startTime", + "description": "Profiling start timestamp in microseconds." + }, + { + "type": "number", + "name": "endTime", + "description": "Profiling end timestamp in microseconds." + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "optional": true, + "name": "samples", + "description": "Ids of samples top nodes." + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "optional": true, + "name": "timeDeltas", + "description": "Time intervals between adjacent samples in microseconds. The first delta is relative to the\nprofile startTime." + } + ], + "type": "object", + "id": "Profile", + "description": "Profile." + }, + { + "properties": [ + { + "type": "integer", + "name": "line", + "description": "Source line number (1-based)." + }, + { + "type": "integer", + "name": "ticks", + "description": "Number of samples attributed to the source line." + } + ], + "type": "object", + "id": "PositionTickInfo", + "description": "Specifies a number of samples attributed to a certain source position." + }, + { + "properties": [ + { + "type": "integer", + "name": "startOffset", + "description": "JavaScript script source offset for the range start." + }, + { + "type": "integer", + "name": "endOffset", + "description": "JavaScript script source offset for the range end." + }, + { + "type": "integer", + "name": "count", + "description": "Collected execution count of the source range." + } + ], + "type": "object", + "id": "CoverageRange", + "description": "Coverage data for a source range." + }, + { + "properties": [ + { + "type": "string", + "name": "functionName", + "description": "JavaScript function name." + }, + { + "items": { + "$ref": "CoverageRange" + }, + "type": "array", + "name": "ranges", + "description": "Source ranges inside the function with coverage data." + }, + { + "type": "boolean", + "name": "isBlockCoverage", + "description": "Whether coverage data for this function has block granularity." + } + ], + "type": "object", + "id": "FunctionCoverage", + "description": "Coverage data for a JavaScript function." + }, + { + "properties": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "JavaScript script id." + }, + { + "type": "string", + "name": "url", + "description": "JavaScript script name or url." + }, + { + "items": { + "$ref": "FunctionCoverage" + }, + "type": "array", + "name": "functions", + "description": "Functions contained in the script that has coverage data." + } + ], + "type": "object", + "id": "ScriptCoverage", + "description": "Coverage data for a JavaScript script." + }, + { + "properties": [ + { + "type": "string", + "name": "name", + "description": "Name of a type collected with type profiling." + } + ], + "type": "object", + "id": "TypeObject", + "experimental": true, + "description": "Describes a type collected during runtime." + }, + { + "properties": [ + { + "type": "integer", + "name": "offset", + "description": "Source offset of the parameter or end of function for return values." + }, + { + "items": { + "$ref": "TypeObject" + }, + "type": "array", + "name": "types", + "description": "The types for this parameter or return value." + } + ], + "type": "object", + "id": "TypeProfileEntry", + "experimental": true, + "description": "Source offset and types for a parameter or return value." + }, + { + "properties": [ + { + "$ref": "Runtime.ScriptId", + "name": "scriptId", + "description": "JavaScript script id." + }, + { + "type": "string", + "name": "url", + "description": "JavaScript script name or url." + }, + { + "items": { + "$ref": "TypeProfileEntry" + }, + "type": "array", + "name": "entries", + "description": "Type profile entries for parameters and return values of the functions in the script." + } + ], + "type": "object", + "id": "ScriptTypeProfile", + "experimental": true, + "description": "Type profile data collected during runtime for a JavaScript script." + } + ], + "events": [ + { + "name": "consoleProfileFinished", + "parameters": [ + { + "type": "string", + "name": "id" + }, + { + "$ref": "Debugger.Location", + "name": "location", + "description": "Location of console.profileEnd()." + }, + { + "name": "profile", + "$ref": "Profile" + }, + { + "type": "string", + "optional": true, + "name": "title", + "description": "Profile title passed as an argument to console.profile()." + } + ] + }, + { + "name": "consoleProfileStarted", + "parameters": [ + { + "type": "string", + "name": "id" + }, + { + "$ref": "Debugger.Location", + "name": "location", + "description": "Location of console.profile()." + }, + { + "type": "string", + "optional": true, + "name": "title", + "description": "Profile title passed as an argument to console.profile()." + } + ], + "description": "Sent when new profile recording is started using console.profile() call." + } + ] + }, + { + "commands": [ + { + "returns": [ + { + "$ref": "RemoteObject", + "name": "result", + "description": "Promise result. Will contain rejected value if promise was rejected." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details if stack strace is available." + } + ], + "name": "awaitPromise", + "parameters": [ + { + "$ref": "RemoteObjectId", + "name": "promiseObjectId", + "description": "Identifier of the promise." + }, + { + "type": "boolean", + "optional": true, + "name": "returnByValue", + "description": "Whether the result is expected to be a JSON object that should be sent by value." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "description": "Whether preview should be generated for the result." + } + ], + "description": "Add handler to promise with given promise object id." + }, + { + "returns": [ + { + "$ref": "RemoteObject", + "name": "result", + "description": "Call result." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "callFunctionOn", + "parameters": [ + { + "type": "string", + "name": "functionDeclaration", + "description": "Declaration of the function to call." + }, + { + "$ref": "RemoteObjectId", + "optional": true, + "name": "objectId", + "description": "Identifier of the object to call function on. Either objectId or executionContextId should\nbe specified." + }, + { + "type": "array", + "items": { + "$ref": "CallArgument" + }, + "optional": true, + "name": "arguments", + "description": "Call arguments. All call arguments must belong to the same JavaScript world as the target\nobject." + }, + { + "type": "boolean", + "optional": true, + "name": "silent", + "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state." + }, + { + "type": "boolean", + "optional": true, + "name": "returnByValue", + "description": "Whether the result is expected to be a JSON object which should be sent by value." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "experimental": true, + "description": "Whether preview should be generated for the result." + }, + { + "type": "boolean", + "optional": true, + "name": "userGesture", + "description": "Whether execution should be treated as initiated by user in the UI." + }, + { + "type": "boolean", + "optional": true, + "name": "awaitPromise", + "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved." + }, + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "executionContextId", + "description": "Specifies execution context which global object will be used to call function on. Either\nexecutionContextId or objectId should be specified." + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "Symbolic group name that can be used to release multiple objects. If objectGroup is not\nspecified and objectId is, objectGroup will be inherited from object." + } + ], + "description": "Calls function with given declaration on the given object. Object group of the result is\ninherited from the target object." + }, + { + "returns": [ + { + "$ref": "ScriptId", + "optional": true, + "name": "scriptId", + "description": "Id of the script." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "compileScript", + "parameters": [ + { + "type": "string", + "name": "expression", + "description": "Expression to compile." + }, + { + "type": "string", + "name": "sourceURL", + "description": "Source url to be set for the script." + }, + { + "type": "boolean", + "name": "persistScript", + "description": "Specifies whether the compiled script should be persisted." + }, + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "executionContextId", + "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page." + } + ], + "description": "Compiles expression." + }, + { + "name": "disable", + "description": "Disables reporting of execution contexts creation." + }, + { + "name": "discardConsoleEntries", + "description": "Discards collected exceptions and console API calls." + }, + { + "name": "enable", + "description": "Enables reporting of execution contexts creation by means of `executionContextCreated` event.\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\ncontext." + }, + { + "returns": [ + { + "$ref": "RemoteObject", + "name": "result", + "description": "Evaluation result." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "evaluate", + "parameters": [ + { + "type": "string", + "name": "expression", + "description": "Expression to evaluate." + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "Symbolic group name that can be used to release multiple objects." + }, + { + "type": "boolean", + "optional": true, + "name": "includeCommandLineAPI", + "description": "Determines whether Command Line API should be available during the evaluation." + }, + { + "type": "boolean", + "optional": true, + "name": "silent", + "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state." + }, + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "contextId", + "description": "Specifies in which execution context to perform evaluation. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page." + }, + { + "type": "boolean", + "optional": true, + "name": "returnByValue", + "description": "Whether the result is expected to be a JSON object that should be sent by value." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "experimental": true, + "description": "Whether preview should be generated for the result." + }, + { + "type": "boolean", + "optional": true, + "name": "userGesture", + "description": "Whether execution should be treated as initiated by user in the UI." + }, + { + "type": "boolean", + "optional": true, + "name": "awaitPromise", + "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved." + }, + { + "type": "boolean", + "optional": true, + "name": "throwOnSideEffect", + "experimental": true, + "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation." + }, + { + "$ref": "TimeDelta", + "optional": true, + "name": "timeout", + "experimental": true, + "description": "Terminate execution after timing out (number of milliseconds)." + } + ], + "description": "Evaluates expression on global object." + }, + { + "returns": [ + { + "type": "string", + "name": "id", + "description": "The isolate id." + } + ], + "name": "getIsolateId", + "experimental": true, + "description": "Returns the isolate id." + }, + { + "returns": [ + { + "type": "number", + "name": "usedSize", + "description": "Used heap size in bytes." + }, + { + "type": "number", + "name": "totalSize", + "description": "Allocated heap size in bytes." + } + ], + "name": "getHeapUsage", + "experimental": true, + "description": "Returns the JavaScript heap usage.\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime." + }, + { + "returns": [ + { + "items": { + "$ref": "PropertyDescriptor" + }, + "type": "array", + "name": "result", + "description": "Object properties." + }, + { + "type": "array", + "items": { + "$ref": "InternalPropertyDescriptor" + }, + "optional": true, + "name": "internalProperties", + "description": "Internal object properties (only of the element itself)." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "getProperties", + "parameters": [ + { + "$ref": "RemoteObjectId", + "name": "objectId", + "description": "Identifier of the object to return properties for." + }, + { + "type": "boolean", + "optional": true, + "name": "ownProperties", + "description": "If true, returns properties belonging only to the element itself, not to its prototype\nchain." + }, + { + "type": "boolean", + "optional": true, + "name": "accessorPropertiesOnly", + "experimental": true, + "description": "If true, returns accessor properties (with getter/setter) only; internal properties are not\nreturned either." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "experimental": true, + "description": "Whether preview should be generated for the results." + } + ], + "description": "Returns properties of a given object. Object group of the result is inherited from the target\nobject." + }, + { + "returns": [ + { + "items": { + "type": "string" + }, + "type": "array", + "name": "names" + } + ], + "name": "globalLexicalScopeNames", + "parameters": [ + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "executionContextId", + "description": "Specifies in which execution context to lookup global scope variables." + } + ], + "description": "Returns all let, const and class variables from global scope." + }, + { + "returns": [ + { + "$ref": "RemoteObject", + "name": "objects", + "description": "Array with objects." + } + ], + "name": "queryObjects", + "parameters": [ + { + "$ref": "RemoteObjectId", + "name": "prototypeObjectId", + "description": "Identifier of the prototype to return objects for." + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "Symbolic group name that can be used to release the results." + } + ] + }, + { + "name": "releaseObject", + "parameters": [ + { + "$ref": "RemoteObjectId", + "name": "objectId", + "description": "Identifier of the object to release." + } + ], + "description": "Releases remote object with given id." + }, + { + "name": "releaseObjectGroup", + "parameters": [ + { + "type": "string", + "name": "objectGroup", + "description": "Symbolic object group name." + } + ], + "description": "Releases all remote objects that belong to a given group." + }, + { + "name": "runIfWaitingForDebugger", + "description": "Tells inspected instance to run if it was waiting for debugger to attach." + }, + { + "returns": [ + { + "$ref": "RemoteObject", + "name": "result", + "description": "Run result." + }, + { + "$ref": "ExceptionDetails", + "optional": true, + "name": "exceptionDetails", + "description": "Exception details." + } + ], + "name": "runScript", + "parameters": [ + { + "$ref": "ScriptId", + "name": "scriptId", + "description": "Id of the script to run." + }, + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "executionContextId", + "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page." + }, + { + "type": "string", + "optional": true, + "name": "objectGroup", + "description": "Symbolic group name that can be used to release multiple objects." + }, + { + "type": "boolean", + "optional": true, + "name": "silent", + "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state." + }, + { + "type": "boolean", + "optional": true, + "name": "includeCommandLineAPI", + "description": "Determines whether Command Line API should be available during the evaluation." + }, + { + "type": "boolean", + "optional": true, + "name": "returnByValue", + "description": "Whether the result is expected to be a JSON object which should be sent by value." + }, + { + "type": "boolean", + "optional": true, + "name": "generatePreview", + "description": "Whether preview should be generated for the result." + }, + { + "type": "boolean", + "optional": true, + "name": "awaitPromise", + "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved." + } + ], + "description": "Runs script with given id in a given context." + }, + { + "redirect": "Debugger", + "name": "setAsyncCallStackDepth", + "parameters": [ + { + "type": "integer", + "name": "maxDepth", + "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default)." + } + ], + "description": "Enables or disables async call stacks tracking." + }, + { + "parameters": [ + { + "type": "boolean", + "name": "enabled" + } + ], + "name": "setCustomObjectFormatterEnabled", + "experimental": true + }, + { + "parameters": [ + { + "type": "integer", + "name": "size" + } + ], + "name": "setMaxCallStackSizeToCapture", + "experimental": true + }, + { + "name": "terminateExecution", + "experimental": true, + "description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends." + }, + { + "parameters": [ + { + "type": "string", + "name": "name" + }, + { + "optional": true, + "name": "executionContextId", + "$ref": "ExecutionContextId" + } + ], + "name": "addBinding", + "experimental": true, + "description": "If executionContextId is empty, adds binding with the given name on the\nglobal objects of all inspected contexts, including those created later,\nbindings survive reloads.\nIf executionContextId is specified, adds binding only on global object of\ngiven execution context.\nBinding function takes exactly one argument, this argument should be string,\nin case of any other input, function throws an exception.\nEach binding function call produces Runtime.bindingCalled notification." + }, + { + "parameters": [ + { + "type": "string", + "name": "name" + } + ], + "name": "removeBinding", + "experimental": true, + "description": "This method does not remove binding function from global object but\nunsubscribes current runtime agent from Runtime.bindingCalled notifications." + } + ], + "domain": "Runtime", + "description": "Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\nEvaluation results are returned as mirror object that expose object type, string representation\nand unique identifier that can be used for further object reference. Original objects are\nmaintained in memory unless they are either explicitly released or are released along with the\nother objects in their object group.", + "types": [ + { + "type": "string", + "id": "ScriptId", + "description": "Unique script identifier." + }, + { + "type": "string", + "id": "RemoteObjectId", + "description": "Unique object identifier." + }, + { + "type": "string", + "id": "UnserializableValue", + "description": "Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\n`-Infinity`, and bigint literals." + }, + { + "properties": [ + { + "enum": [ + "object", + "function", + "undefined", + "string", + "number", + "boolean", + "symbol", + "bigint" + ], + "type": "string", + "name": "type", + "description": "Object type." + }, + { + "type": "string", + "enum": [ + "array", + "null", + "node", + "regexp", + "date", + "map", + "set", + "weakmap", + "weakset", + "iterator", + "generator", + "error", + "proxy", + "promise", + "typedarray", + "arraybuffer", + "dataview" + ], + "optional": true, + "name": "subtype", + "description": "Object subtype hint. Specified for `object` type values only." + }, + { + "type": "string", + "optional": true, + "name": "className", + "description": "Object class (constructor) name. Specified for `object` type values only." + }, + { + "type": "any", + "optional": true, + "name": "value", + "description": "Remote object value in case of primitive values or JSON values (if it was requested)." + }, + { + "$ref": "UnserializableValue", + "optional": true, + "name": "unserializableValue", + "description": "Primitive value which can not be JSON-stringified does not have `value`, but gets this\nproperty." + }, + { + "type": "string", + "optional": true, + "name": "description", + "description": "String representation of the object." + }, + { + "$ref": "RemoteObjectId", + "optional": true, + "name": "objectId", + "description": "Unique object identifier (for non-primitive values)." + }, + { + "$ref": "ObjectPreview", + "optional": true, + "name": "preview", + "experimental": true, + "description": "Preview containing abbreviated property values. Specified for `object` type values only." + }, + { + "optional": true, + "name": "customPreview", + "experimental": true, + "$ref": "CustomPreview" + } + ], + "type": "object", + "id": "RemoteObject", + "description": "Mirror object referencing original JavaScript object." + }, + { + "type": "object", + "id": "CustomPreview", + "experimental": true, + "properties": [ + { + "type": "string", + "name": "header", + "description": "The JSON-stringified result of formatter.header(object, config) call.\nIt contains json ML array that represents RemoteObject." + }, + { + "$ref": "RemoteObjectId", + "optional": true, + "name": "bodyGetterId", + "description": "If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\nThe result value is json ML array." + } + ] + }, + { + "properties": [ + { + "enum": [ + "object", + "function", + "undefined", + "string", + "number", + "boolean", + "symbol", + "bigint" + ], + "type": "string", + "name": "type", + "description": "Object type." + }, + { + "type": "string", + "enum": [ + "array", + "null", + "node", + "regexp", + "date", + "map", + "set", + "weakmap", + "weakset", + "iterator", + "generator", + "error" + ], + "optional": true, + "name": "subtype", + "description": "Object subtype hint. Specified for `object` type values only." + }, + { + "type": "string", + "optional": true, + "name": "description", + "description": "String representation of the object." + }, + { + "type": "boolean", + "name": "overflow", + "description": "True iff some of the properties or entries of the original object did not fit." + }, + { + "items": { + "$ref": "PropertyPreview" + }, + "type": "array", + "name": "properties", + "description": "List of the properties." + }, + { + "type": "array", + "items": { + "$ref": "EntryPreview" + }, + "optional": true, + "name": "entries", + "description": "List of the entries. Specified for `map` and `set` subtype values only." + } + ], + "type": "object", + "id": "ObjectPreview", + "experimental": true, + "description": "Object containing abbreviated remote object value." + }, + { + "type": "object", + "id": "PropertyPreview", + "experimental": true, + "properties": [ + { + "type": "string", + "name": "name", + "description": "Property name." + }, + { + "enum": [ + "object", + "function", + "undefined", + "string", + "number", + "boolean", + "symbol", + "accessor", + "bigint" + ], + "type": "string", + "name": "type", + "description": "Object type. Accessor means that the property itself is an accessor property." + }, + { + "type": "string", + "optional": true, + "name": "value", + "description": "User-friendly property value string." + }, + { + "$ref": "ObjectPreview", + "optional": true, + "name": "valuePreview", + "description": "Nested value preview." + }, + { + "type": "string", + "enum": [ + "array", + "null", + "node", + "regexp", + "date", + "map", + "set", + "weakmap", + "weakset", + "iterator", + "generator", + "error" + ], + "optional": true, + "name": "subtype", + "description": "Object subtype hint. Specified for `object` type values only." + } + ] + }, + { + "type": "object", + "id": "EntryPreview", + "experimental": true, + "properties": [ + { + "$ref": "ObjectPreview", + "optional": true, + "name": "key", + "description": "Preview of the key. Specified for map-like collection entries." + }, + { + "$ref": "ObjectPreview", + "name": "value", + "description": "Preview of the value." + } + ] + }, + { + "properties": [ + { + "type": "string", + "name": "name", + "description": "Property name or symbol description." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "value", + "description": "The value associated with the property." + }, + { + "type": "boolean", + "optional": true, + "name": "writable", + "description": "True if the value associated with the property may be changed (data descriptors only)." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "get", + "description": "A function which serves as a getter for the property, or `undefined` if there is no getter\n(accessor descriptors only)." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "set", + "description": "A function which serves as a setter for the property, or `undefined` if there is no setter\n(accessor descriptors only)." + }, + { + "type": "boolean", + "name": "configurable", + "description": "True if the type of this property descriptor may be changed and if the property may be\ndeleted from the corresponding object." + }, + { + "type": "boolean", + "name": "enumerable", + "description": "True if this property shows up during enumeration of the properties on the corresponding\nobject." + }, + { + "type": "boolean", + "optional": true, + "name": "wasThrown", + "description": "True if the result was thrown during the evaluation." + }, + { + "type": "boolean", + "optional": true, + "name": "isOwn", + "description": "True if the property is owned for the object." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "symbol", + "description": "Property symbol object, if the property is of the `symbol` type." + } + ], + "type": "object", + "id": "PropertyDescriptor", + "description": "Object property descriptor." + }, + { + "properties": [ + { + "type": "string", + "name": "name", + "description": "Conventional property name." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "value", + "description": "The value associated with the property." + } + ], + "type": "object", + "id": "InternalPropertyDescriptor", + "description": "Object internal property descriptor. This property isn't normally visible in JavaScript code." + }, + { + "properties": [ + { + "type": "any", + "optional": true, + "name": "value", + "description": "Primitive value or serializable javascript object." + }, + { + "$ref": "UnserializableValue", + "optional": true, + "name": "unserializableValue", + "description": "Primitive value which can not be JSON-stringified." + }, + { + "$ref": "RemoteObjectId", + "optional": true, + "name": "objectId", + "description": "Remote object handle." + } + ], + "type": "object", + "id": "CallArgument", + "description": "Represents function call argument. Either remote object id `objectId`, primitive `value`,\nunserializable primitive value or neither of (for undefined) them should be specified." + }, + { + "type": "integer", + "id": "ExecutionContextId", + "description": "Id of an execution context." + }, + { + "properties": [ + { + "$ref": "ExecutionContextId", + "name": "id", + "description": "Unique id of the execution context. It can be used to specify in which execution context\nscript evaluation should be performed." + }, + { + "type": "string", + "name": "origin", + "description": "Execution context origin." + }, + { + "type": "string", + "name": "name", + "description": "Human readable name describing given context." + }, + { + "type": "object", + "optional": true, + "name": "auxData", + "description": "Embedder-specific auxiliary data." + } + ], + "type": "object", + "id": "ExecutionContextDescription", + "description": "Description of an isolated world." + }, + { + "properties": [ + { + "type": "integer", + "name": "exceptionId", + "description": "Exception id." + }, + { + "type": "string", + "name": "text", + "description": "Exception text, which should be used together with exception object when available." + }, + { + "type": "integer", + "name": "lineNumber", + "description": "Line number of the exception location (0-based)." + }, + { + "type": "integer", + "name": "columnNumber", + "description": "Column number of the exception location (0-based)." + }, + { + "$ref": "ScriptId", + "optional": true, + "name": "scriptId", + "description": "Script ID of the exception location." + }, + { + "type": "string", + "optional": true, + "name": "url", + "description": "URL of the exception location, to be used when the script was not reported." + }, + { + "$ref": "StackTrace", + "optional": true, + "name": "stackTrace", + "description": "JavaScript stack trace if available." + }, + { + "$ref": "RemoteObject", + "optional": true, + "name": "exception", + "description": "Exception object if available." + }, + { + "$ref": "ExecutionContextId", + "optional": true, + "name": "executionContextId", + "description": "Identifier of the context where exception happened." + } + ], + "type": "object", + "id": "ExceptionDetails", + "description": "Detailed information about exception (or error) that was thrown during script compilation or\nexecution." + }, + { + "type": "number", + "id": "Timestamp", + "description": "Number of milliseconds since epoch." + }, + { + "type": "number", + "id": "TimeDelta", + "description": "Number of milliseconds." + }, + { + "properties": [ + { + "type": "string", + "name": "functionName", + "description": "JavaScript function name." + }, + { + "$ref": "ScriptId", + "name": "scriptId", + "description": "JavaScript script id." + }, + { + "type": "string", + "name": "url", + "description": "JavaScript script name or url." + }, + { + "type": "integer", + "name": "lineNumber", + "description": "JavaScript script line number (0-based)." + }, + { + "type": "integer", + "name": "columnNumber", + "description": "JavaScript script column number (0-based)." + } + ], + "type": "object", + "id": "CallFrame", + "description": "Stack entry for runtime errors and assertions." + }, + { + "properties": [ + { + "type": "string", + "optional": true, + "name": "description", + "description": "String label of this stack trace. For async traces this may be a name of the function that\ninitiated the async call." + }, + { + "items": { + "$ref": "CallFrame" + }, + "type": "array", + "name": "callFrames", + "description": "JavaScript function name." + }, + { + "$ref": "StackTrace", + "optional": true, + "name": "parent", + "description": "Asynchronous JavaScript stack trace that preceded this stack, if available." + }, + { + "$ref": "StackTraceId", + "optional": true, + "name": "parentId", + "experimental": true, + "description": "Asynchronous JavaScript stack trace that preceded this stack, if available." + } + ], + "type": "object", + "id": "StackTrace", + "description": "Call frames for assertions or error messages." + }, + { + "type": "string", + "id": "UniqueDebuggerId", + "experimental": true, + "description": "Unique identifier of current debugger." + }, + { + "properties": [ + { + "type": "string", + "name": "id" + }, + { + "optional": true, + "name": "debuggerId", + "$ref": "UniqueDebuggerId" + } + ], + "type": "object", + "id": "StackTraceId", + "experimental": true, + "description": "If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages." + } + ], + "events": [ + { + "parameters": [ + { + "type": "string", + "name": "name" + }, + { + "type": "string", + "name": "payload" + }, + { + "$ref": "ExecutionContextId", + "name": "executionContextId", + "description": "Identifier of the context where the call was made." + } + ], + "name": "bindingCalled", + "experimental": true, + "description": "Notification is issued every time when binding is called." + }, + { + "name": "consoleAPICalled", + "parameters": [ + { + "enum": [ + "log", + "debug", + "info", + "error", + "warning", + "dir", + "dirxml", + "table", + "trace", + "clear", + "startGroup", + "startGroupCollapsed", + "endGroup", + "assert", + "profile", + "profileEnd", + "count", + "timeEnd" + ], + "type": "string", + "name": "type", + "description": "Type of the call." + }, + { + "items": { + "$ref": "RemoteObject" + }, + "type": "array", + "name": "args", + "description": "Call arguments." + }, + { + "$ref": "ExecutionContextId", + "name": "executionContextId", + "description": "Identifier of the context where the call was made." + }, + { + "$ref": "Timestamp", + "name": "timestamp", + "description": "Call timestamp." + }, + { + "$ref": "StackTrace", + "optional": true, + "name": "stackTrace", + "description": "Stack trace captured when the call was made." + }, + { + "type": "string", + "optional": true, + "name": "context", + "experimental": true, + "description": "Console context descriptor for calls on non-default console context (not console.*):\n'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call\non named context." + } + ], + "description": "Issued when console API was called." + }, + { + "name": "exceptionRevoked", + "parameters": [ + { + "type": "string", + "name": "reason", + "description": "Reason describing why exception was revoked." + }, + { + "type": "integer", + "name": "exceptionId", + "description": "The id of revoked exception, as reported in `exceptionThrown`." + } + ], + "description": "Issued when unhandled exception was revoked." + }, + { + "name": "exceptionThrown", + "parameters": [ + { + "$ref": "Timestamp", + "name": "timestamp", + "description": "Timestamp of the exception." + }, + { + "name": "exceptionDetails", + "$ref": "ExceptionDetails" + } + ], + "description": "Issued when exception was thrown and unhandled." + }, + { + "name": "executionContextCreated", + "parameters": [ + { + "$ref": "ExecutionContextDescription", + "name": "context", + "description": "A newly created execution context." + } + ], + "description": "Issued when new execution context is created." + }, + { + "name": "executionContextDestroyed", + "parameters": [ + { + "$ref": "ExecutionContextId", + "name": "executionContextId", + "description": "Id of the destroyed context" + } + ], + "description": "Issued when execution context is destroyed." + }, + { + "name": "executionContextsCleared", + "description": "Issued when all executionContexts were cleared in browser" + }, + { + "name": "inspectRequested", + "parameters": [ + { + "name": "object", + "$ref": "RemoteObject" + }, + { + "type": "object", + "name": "hints" + } + ], + "description": "Issued when object should be inspected (for example, as a result of inspect() command line API\ncall)." + } + ] + }, + { + "deprecated": true, + "domain": "Schema", + "commands": [ + { + "returns": [ + { + "items": { + "$ref": "Domain" + }, + "type": "array", + "name": "domains", + "description": "List of supported domains." + } + ], + "name": "getDomains", + "description": "Returns supported domains." + } + ], + "description": "This domain is deprecated.", + "types": [ + { + "properties": [ + { + "type": "string", + "name": "name", + "description": "Domain name." + }, + { + "type": "string", + "name": "version", + "description": "Domain version." + } + ], + "type": "object", + "id": "Domain", + "description": "Description of the protocol domain." + } + ] + } + ], + "version": { + "major": "1", + "minor": "3" + } +}; + +export const Protocol = { Description }; diff --git a/remote/cdp/StreamRegistry.sys.mjs b/remote/cdp/StreamRegistry.sys.mjs new file mode 100644 index 0000000000..9474f16a57 --- /dev/null +++ b/remote/cdp/StreamRegistry.sys.mjs @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs", +}); + +export class Stream { + #path; + #offset; + #length; + + constructor(path) { + this.#path = path; + this.#offset = 0; + this.#length = null; + } + + async destroy() { + await IOUtils.remove(this.#path); + } + + async seek(seekTo) { + // To keep compatibility with Chrome clip invalid offsets + this.#offset = Math.max(0, Math.min(seekTo, await this.length())); + } + + async readBytes(count) { + const bytes = await IOUtils.read(this.#path, { + offset: this.#offset, + maxBytes: count, + }); + this.#offset += bytes.length; + return bytes; + } + + async available() { + const length = await this.length(); + return length - this.#offset; + } + + async length() { + if (this.#length === null) { + const info = await IOUtils.stat(this.#path); + this.#length = info.size; + } + + return this.#length; + } + + get path() { + return this.#path; + } +} + +export class StreamRegistry { + constructor() { + // handle => stream + this.streams = new Map(); + + // Register an async shutdown blocker to ensure all open IO streams are + // closed, and remaining temporary files removed. Needs to happen before + // IOUtils has been shutdown. + IOUtils.profileBeforeChange.addBlocker( + "Remote Agent: Clean-up of open streams", + async () => { + await this.destructor(); + } + ); + } + + async destructor() { + for (const stream of this.streams.values()) { + await stream.destroy(); + } + + this.streams.clear(); + } + + /** + * Add a new stream to the registry. + * + * @param {Stream} stream + * The stream to use. + * + * @returns {string} + * Stream handle (uuid) + */ + add(stream) { + if (!(stream instanceof Stream)) { + // Bug 1602731 - Implement support for blob + throw new lazy.UnsupportedError(`Unknown stream type for ${stream}`); + } + + const handle = lazy.generateUUID(); + + this.streams.set(handle, stream); + return handle; + } + + /** + * Get a stream from the registry. + * + * @param {string} handle + * Handle of the stream to retrieve. + * + * @returns {Stream} + * The requested stream. + */ + get(handle) { + const stream = this.streams.get(handle); + + if (!stream) { + throw new TypeError(`Invalid stream handle`); + } + + return stream; + } + + /** + * Remove a stream from the registry. + * + * @param {string} handle + * Handle of the stream to remove. + * + * @returns {boolean} + * true if successfully removed + */ + async remove(handle) { + const stream = this.get(handle); + await stream.destroy(); + + return this.streams.delete(handle); + } +} diff --git a/remote/cdp/domains/ContentProcessDomain.sys.mjs b/remote/cdp/domains/ContentProcessDomain.sys.mjs new file mode 100644 index 0000000000..fefe6aece5 --- /dev/null +++ b/remote/cdp/domains/ContentProcessDomain.sys.mjs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +export class ContentProcessDomain extends Domain { + destructor() { + super.destructor(); + } + + // helpers + + get content() { + return this.session.content; + } + + get docShell() { + return this.session.docShell; + } + + get chromeEventHandler() { + return this.docShell.chromeEventHandler; + } +} diff --git a/remote/cdp/domains/ContentProcessDomains.sys.mjs b/remote/cdp/domains/ContentProcessDomains.sys.mjs new file mode 100644 index 0000000000..a434dc0067 --- /dev/null +++ b/remote/cdp/domains/ContentProcessDomains.sys.mjs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const ContentProcessDomains = {}; + +// eslint-disable-next-line mozilla/lazy-getter-object-name +ChromeUtils.defineESModuleGetters(ContentProcessDomains, { + DOM: "chrome://remote/content/cdp/domains/content/DOM.sys.mjs", + Emulation: "chrome://remote/content/cdp/domains/content/Emulation.sys.mjs", + Input: "chrome://remote/content/cdp/domains/content/Input.sys.mjs", + Log: "chrome://remote/content/cdp/domains/content/Log.sys.mjs", + Network: "chrome://remote/content/cdp/domains/content/Network.sys.mjs", + Page: "chrome://remote/content/cdp/domains/content/Page.sys.mjs", + Performance: + "chrome://remote/content/cdp/domains/content/Performance.sys.mjs", + Runtime: "chrome://remote/content/cdp/domains/content/Runtime.sys.mjs", + Security: "chrome://remote/content/cdp/domains/content/Security.sys.mjs", +}); diff --git a/remote/cdp/domains/Domain.sys.mjs b/remote/cdp/domains/Domain.sys.mjs new file mode 100644 index 0000000000..d9b36c62b8 --- /dev/null +++ b/remote/cdp/domains/Domain.sys.mjs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class Domain { + constructor(session) { + this.session = session; + this.name = this.constructor.name; + + this.eventListeners_ = new Set(); + this._requestCounter = 0; + } + + destructor() {} + + emit(eventName, params = {}) { + for (const listener of this.eventListeners_) { + try { + if (isEventHandler(listener)) { + listener.onEvent(eventName, params); + } else { + listener.call(this, eventName, params); + } + } catch (e) { + console.error(e); + } + } + } + + /** + * Execute the provided method in the child domain that has the same domain + * name. eg. calling this.executeInChild from domains/parent/Input.jsm will + * attempt to execute the method in domains/content/Input.jsm. + * + * This can only be called from parent domains managed by a TabSession. + * + * @param {string} method + * Name of the method to call on the child domain. + * @param {object} params + * Optional parameters. Must be serializable. + */ + executeInChild(method, params) { + if (!this.session.executeInChild) { + throw new Error( + "executeInChild can only be used in Domains managed by a TabSession" + ); + } + this._requestCounter++; + const id = this.name + "-" + this._requestCounter; + return this.session.executeInChild(id, this.name, method, params); + } + + addEventListener(listener) { + if (typeof listener != "function" && !isEventHandler(listener)) { + throw new TypeError(); + } + this.eventListeners_.add(listener); + } + + // static + + static implements(command) { + return command && typeof this.prototype[command] == "function"; + } +} + +function isEventHandler(listener) { + return ( + listener && "onEvent" in listener && typeof listener.onEvent == "function" + ); +} diff --git a/remote/cdp/domains/DomainCache.sys.mjs b/remote/cdp/domains/DomainCache.sys.mjs new file mode 100644 index 0000000000..b4651cbe68 --- /dev/null +++ b/remote/cdp/domains/DomainCache.sys.mjs @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Domain: "chrome://remote/content/cdp/domains/Domain.sys.mjs", + UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs", +}); + +/** + * Lazy domain instance cache. + * + * Domains are loaded into each target's realm, and consequently + * there exists one domain cache per realm. Domains are preregistered + * with this cache and then constructed lazily upon request. + * + * @param {Session} session + * Session that domains should be associated with as they + * are constructed. + * @param {Map.<string, string>} modules + * Table defining JS modules available to this domain cache. + * This should be a mapping between domain name + * and JS module path passed to ChromeUtils.import. + */ +export class DomainCache { + constructor(session, modules) { + this.session = session; + this.modules = modules; + this.instances = new Map(); + } + + /** Test if domain supports method. */ + domainSupportsMethod(name, method) { + const domain = this.modules[name]; + if (domain) { + return domain.implements(method); + } + return false; + } + + /** + * Gets the current instance of the domain, or creates a new one, + * and associates it with the predefined session. + * + * @throws {UnknownMethodError} + * If domain is not preregistered with this domain cache. + */ + get(name) { + let inst = this.instances.get(name); + if (!inst) { + const Cls = this.modules[name]; + if (!Cls) { + throw new lazy.UnknownMethodError(name); + } + if (!isConstructor(Cls)) { + throw new TypeError("Domain cannot be constructed"); + } + + inst = new Cls(this.session); + if (!(inst instanceof lazy.Domain)) { + throw new TypeError("Instance not a domain"); + } + + inst.addEventListener(this.session); + + this.instances.set(name, inst); + } + + return inst; + } + + /** + * Tells if a Domain of the given name is available + */ + has(name) { + return name in this.modules; + } + + get size() { + return this.instances.size; + } + + /** + * Execute the given command (function) of a given domain with the given parameters. + * If the command doesn't exists, it will throw. + * It returns the returned value of the command, which is most likely a promise. + */ + execute(domain, command, params) { + if (!this.domainSupportsMethod(domain, command)) { + throw new lazy.UnknownMethodError(domain, command); + } + const inst = this.get(domain); + return inst[command](params); + } + + /** Calls destructor on each domain and clears the cache. */ + clear() { + for (const inst of this.instances.values()) { + inst.destructor(); + } + this.instances.clear(); + } + + toString() { + return `[object DomainCache ${this.size}]`; + } +} + +function isConstructor(obj) { + return !!obj.prototype && !!obj.prototype.constructor.name; +} diff --git a/remote/cdp/domains/ParentProcessDomains.sys.mjs b/remote/cdp/domains/ParentProcessDomains.sys.mjs new file mode 100644 index 0000000000..fcf806b884 --- /dev/null +++ b/remote/cdp/domains/ParentProcessDomains.sys.mjs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const ParentProcessDomains = {}; + +// eslint-disable-next-line mozilla/lazy-getter-object-name +ChromeUtils.defineESModuleGetters(ParentProcessDomains, { + Browser: "chrome://remote/content/cdp/domains/parent/Browser.sys.mjs", + Emulation: "chrome://remote/content/cdp/domains/parent/Emulation.sys.mjs", + Fetch: "chrome://remote/content/cdp/domains/parent/Fetch.sys.mjs", + Input: "chrome://remote/content/cdp/domains/parent/Input.sys.mjs", + IO: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs", + Network: "chrome://remote/content/cdp/domains/parent/Network.sys.mjs", + Page: "chrome://remote/content/cdp/domains/parent/Page.sys.mjs", + Security: "chrome://remote/content/cdp/domains/parent/Security.sys.mjs", + SystemInfo: "chrome://remote/content/cdp/domains/parent/SystemInfo.sys.mjs", + Target: "chrome://remote/content/cdp/domains/parent/Target.sys.mjs", +}); diff --git a/remote/cdp/domains/content/DOM.sys.mjs b/remote/cdp/domains/content/DOM.sys.mjs new file mode 100644 index 0000000000..34c4e96ea9 --- /dev/null +++ b/remote/cdp/domains/content/DOM.sys.mjs @@ -0,0 +1,245 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +export class DOM extends ContentProcessDomain { + constructor(session) { + super(session); + this.enabled = false; + } + + destructor() { + this.disable(); + } + + // commands + + async enable() { + if (!this.enabled) { + this.enabled = true; + } + } + + /** + * Describes node given its id. + * + * Does not require domain to be enabled. Does not start tracking any objects. + * + * @param {object} options + * @param {number=} options.backendNodeId [not supported] + * Identifier of the backend node. + * @param {number=} options.depth [not supported] + * The maximum depth at which children should be retrieved, defaults to 1. + * Use -1 for the entire subtree or provide an integer larger than 0. + * @param {number=} options.nodeId [not supported] + * Identifier of the node. + * @param {string} options.objectId + * JavaScript object id of the node wrapper. + * @param {boolean=} options.pierce [not supported] + * Whether or not iframes and shadow roots should be traversed + * when returning the subtree, defaults to false. + * + * @returns {DOM.Node} + * Node description. + */ + describeNode(options = {}) { + const { objectId } = options; + + // Until nodeId/backendNodeId is supported force usage of the objectId + if (!["string"].includes(typeof objectId)) { + throw new TypeError("objectId: string value expected"); + } + + const Runtime = this.session.domains.get("Runtime"); + const debuggerObj = Runtime._getRemoteObject(objectId); + if (!debuggerObj) { + throw new Error("Could not find object with given id"); + } + + if (typeof debuggerObj.nodeId == "undefined") { + throw new Error("Object id doesn't reference a Node"); + } + + const unsafeObj = debuggerObj.unsafeDereference(); + + const attributes = []; + if (unsafeObj.attributes) { + // Flatten the list of attributes for name and value + for (const attribute of unsafeObj.attributes) { + attributes.push(attribute.name, attribute.value); + } + } + + let context = this.docShell.browsingContext; + if (HTMLIFrameElement.isInstance(unsafeObj)) { + context = unsafeObj.contentWindow.docShell.browsingContext; + } + + const node = { + nodeId: debuggerObj.nodeId, + backendNodeId: debuggerObj.backendNodeId, + nodeType: unsafeObj.nodeType, + nodeName: unsafeObj.nodeName, + localName: unsafeObj.localName, + nodeValue: unsafeObj.nodeValue ? unsafeObj.nodeValue.toString() : "", + childNodeCount: unsafeObj.childElementCount, + attributes: attributes.length ? attributes : undefined, + frameId: context.id.toString(), + }; + + return { node }; + } + + disable() { + if (this.enabled) { + this.enabled = false; + } + } + + getContentQuads(options = {}) { + const { objectId } = options; + const Runtime = this.session.domains.get("Runtime"); + const debuggerObj = Runtime._getRemoteObject(objectId); + if (!debuggerObj) { + throw new Error(`Cannot find object with id: ${objectId}`); + } + const unsafeObject = debuggerObj.unsafeDereference(); + if (!unsafeObject.getBoxQuads) { + throw new Error("RemoteObject is not a node"); + } + let quads = unsafeObject.getBoxQuads({ relativeTo: this.content.document }); + quads = quads.map(quad => { + return [ + quad.p1.x, + quad.p1.y, + quad.p2.x, + quad.p2.y, + quad.p3.x, + quad.p3.y, + quad.p4.x, + quad.p4.y, + ].map(Math.round); + }); + return { quads }; + } + + getBoxModel(options = {}) { + const { objectId } = options; + const Runtime = this.session.domains.get("Runtime"); + const debuggerObj = Runtime._getRemoteObject(objectId); + if (!debuggerObj) { + throw new Error(`Cannot find object with id: ${objectId}`); + } + const unsafeObject = debuggerObj.unsafeDereference(); + const bounding = unsafeObject.getBoundingClientRect(); + const model = { + width: Math.round(bounding.width), + height: Math.round(bounding.height), + }; + for (const box of ["content", "padding", "border", "margin"]) { + const quads = unsafeObject.getBoxQuads({ + box, + relativeTo: this.content.document, + }); + + // getBoxQuads may return more than one element. In this case we have to compute the bounding box + // of all these boxes. + let bounding = { + p1: { x: Infinity, y: Infinity }, + p2: { x: -Infinity, y: Infinity }, + p3: { x: -Infinity, y: -Infinity }, + p4: { x: Infinity, y: -Infinity }, + }; + quads.forEach(quad => { + bounding = { + p1: { + x: Math.min(bounding.p1.x, quad.p1.x), + y: Math.min(bounding.p1.y, quad.p1.y), + }, + p2: { + x: Math.max(bounding.p2.x, quad.p2.x), + y: Math.min(bounding.p2.y, quad.p2.y), + }, + p3: { + x: Math.max(bounding.p3.x, quad.p3.x), + y: Math.max(bounding.p3.y, quad.p3.y), + }, + p4: { + x: Math.min(bounding.p4.x, quad.p4.x), + y: Math.max(bounding.p4.y, quad.p4.y), + }, + }; + }); + + model[box] = [ + bounding.p1.x, + bounding.p1.y, + bounding.p2.x, + bounding.p2.y, + bounding.p3.x, + bounding.p3.y, + bounding.p4.x, + bounding.p4.y, + ].map(Math.round); + } + return { + model, + }; + } + + /** + * Resolves the JavaScript node object for a given NodeId or BackendNodeId. + * + * @param {object} options + * @param {number} options.backendNodeId [required for now] + * Backend identifier of the node to resolve. + * @param {number=} options.executionContextId + * Execution context in which to resolve the node. + * @param {number=} options.nodeId [not supported] + * Id of the node to resolve. + * @param {string=} options.objectGroup [not supported] + * Symbolic group name that can be used to release multiple objects. + * + * @returns {Runtime.RemoteObject} + * JavaScript object wrapper for given node. + */ + resolveNode(options = {}) { + const { backendNodeId, executionContextId } = options; + + // Until nodeId is supported force usage of the backendNodeId + if (!["number"].includes(typeof backendNodeId)) { + throw new TypeError("backendNodeId: number value expected"); + } + if (!["undefined", "number"].includes(typeof executionContextId)) { + throw new TypeError("executionContextId: integer value expected"); + } + + const Runtime = this.session.domains.get("Runtime"); + + // Retrieve the node to resolve, and its context + const debuggerObj = Runtime._getRemoteObjectByNodeId(backendNodeId); + + if (!debuggerObj) { + throw new Error(`No node with given id found`); + } + + // If execution context isn't specified use the default one for the node + let context; + if (typeof executionContextId != "undefined") { + context = Runtime.contexts.get(executionContextId); + if (!context) { + throw new Error(`Node with given id does not belong to the document`); + } + } else { + context = Runtime._getDefaultContextForWindow(); + } + + Runtime._setRemoteObject(debuggerObj, context); + + return { + object: Runtime._serializeRemoteObject(debuggerObj, context.id), + }; + } +} diff --git a/remote/cdp/domains/content/Emulation.sys.mjs b/remote/cdp/domains/content/Emulation.sys.mjs new file mode 100644 index 0000000000..41bb0c76ea --- /dev/null +++ b/remote/cdp/domains/content/Emulation.sys.mjs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs", +}); + +export class Emulation extends ContentProcessDomain { + // commands + + /** + * Internal methods: the following methods are not part of CDP; + * note the _ prefix. + */ + + /** + * Waits until the viewport has reached the new dimensions. + */ + async _awaitViewportDimensions({ width, height }) { + const win = this.content; + let resized; + + // Updates for background tabs are throttled, and we also we have to make + // sure that the new browser dimensions have been received by the content + // process. As such wait for the next animation frame. + await lazy.AnimationFramePromise(win); + + const checkBrowserSize = () => { + if (win.innerWidth === width && win.innerHeight === height) { + resized(); + } + }; + + return new Promise(resolve => { + resized = resolve; + + win.addEventListener("resize", checkBrowserSize); + + // Trigger a layout flush in case none happened yet. + checkBrowserSize(); + }).finally(() => { + win.removeEventListener("resize", checkBrowserSize); + }); + } +} diff --git a/remote/cdp/domains/content/Input.sys.mjs b/remote/cdp/domains/content/Input.sys.mjs new file mode 100644 index 0000000000..e7a6ffc709 --- /dev/null +++ b/remote/cdp/domains/content/Input.sys.mjs @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +export class Input extends ContentProcessDomain { + constructor(session) { + super(session); + + // Internal id used to track existing event handlers. + this._eventId = 0; + + // Map of event id -> event handler promise. + this._eventPromises = new Map(); + } + + /** + * Internal methods: the following methods are not part of CDP; + * note the _ prefix. + */ + + /** + * Add an event listener in the content page for the provided eventName. + * This method will return a unique handler id that can be used to wait + * for the event. + * + * Example usage from a parent process domain: + * + * const id = await this.executeInChild("_addContentEventListener", "click"); + * // do something that triggers a click in content + * await this.executeInChild("_waitForContentEvent", id); + */ + _addContentEventListener(eventName) { + const eventPromise = new Promise(resolve => { + this.chromeEventHandler.addEventListener(eventName, resolve, { + mozSystemGroup: true, + once: true, + }); + }); + this._eventId++; + this._eventPromises.set(this._eventId, eventPromise); + return this._eventId; + } + + /** + * Wait for an event listener added via `addContentEventListener` to be fired. + */ + async _waitForContentEvent(eventId) { + const eventPromise = this._eventPromises.get(eventId); + if (!eventPromise) { + throw new Error("No event promise found for id " + eventId); + } + await eventPromise; + this._eventPromises.delete(eventId); + } +} diff --git a/remote/cdp/domains/content/Log.sys.mjs b/remote/cdp/domains/content/Log.sys.mjs new file mode 100644 index 0000000000..f8c561d429 --- /dev/null +++ b/remote/cdp/domains/content/Log.sys.mjs @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +const CONSOLE_MESSAGE_LEVEL_MAP = { + [Ci.nsIConsoleMessage.debug]: "verbose", + [Ci.nsIConsoleMessage.info]: "info", + [Ci.nsIConsoleMessage.warn]: "warning", + [Ci.nsIConsoleMessage.error]: "error", +}; + +export class Log extends ContentProcessDomain { + constructor(session) { + super(session); + this.enabled = false; + } + + destructor() { + this.disable(); + + super.destructor(); + } + + enable() { + if (!this.enabled) { + this.enabled = true; + + Services.console.registerListener(this); + } + } + + disable() { + if (this.enabled) { + this.enabled = false; + + Services.console.unregisterListener(this); + } + } + + _getLogCategory(category) { + if (category.startsWith("CORS")) { + return "network"; + } else if (category.includes("javascript")) { + return "javascript"; + } + + return "other"; + } + + // nsIObserver + + /** + * Takes all script error messages that do not have an exception attached, + * and emits a "Log.entryAdded" event. + * + * @param {nsIConsoleMessage} message + * Message originating from the nsIConsoleService. + */ + observe(message) { + if (message instanceof Ci.nsIScriptError && !message.hasException) { + let url; + if (message.sourceName !== "debugger eval code") { + url = message.sourceName; + } + + const entry = { + source: this._getLogCategory(message.category), + level: CONSOLE_MESSAGE_LEVEL_MAP[message.logLevel], + text: message.errorMessage, + timestamp: message.timeStamp, + url, + lineNumber: message.lineNumber, + }; + + this.emit("Log.entryAdded", { entry }); + } + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIConsoleListener"]); + } +} diff --git a/remote/cdp/domains/content/Network.sys.mjs b/remote/cdp/domains/content/Network.sys.mjs new file mode 100644 index 0000000000..91dc44cf46 --- /dev/null +++ b/remote/cdp/domains/content/Network.sys.mjs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +export class Network extends ContentProcessDomain { + // commands + + /** + * Internal methods: the following methods are not part of CDP; + * note the _ prefix. + */ + + _updateLoadFlags(flags) { + this.docShell.defaultLoadFlags = flags; + } +} diff --git a/remote/cdp/domains/content/Page.sys.mjs b/remote/cdp/domains/content/Page.sys.mjs new file mode 100644 index 0000000000..8e1abefe56 --- /dev/null +++ b/remote/cdp/domains/content/Page.sys.mjs @@ -0,0 +1,453 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", +}); + +const { LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY, LOAD_FLAGS_NONE } = + Ci.nsIWebNavigation; + +export class Page extends ContentProcessDomain { + constructor(session) { + super(session); + + this.enabled = false; + this.lifecycleEnabled = false; + // script id => { source, worldName } + this.scriptsToEvaluateOnLoad = new Map(); + this.worldsToEvaluateOnLoad = new Set(); + + // This map is used to keep a reference to the loader id for + // those Page events, which do not directly rely on + // Network events. This might be a temporary solution until + // the Network observer could be queried for that. But right + // now this lives in the parent process. + this.frameIdToLoaderId = new Map(); + + this._onFrameAttached = this._onFrameAttached.bind(this); + this._onFrameDetached = this._onFrameDetached.bind(this); + this._onFrameNavigated = this._onFrameNavigated.bind(this); + this._onScriptLoaded = this._onScriptLoaded.bind(this); + + this.session.contextObserver.on("script-loaded", this._onScriptLoaded); + } + + destructor() { + this.setLifecycleEventsEnabled({ enabled: false }); + this.session.contextObserver.off("script-loaded", this._onScriptLoaded); + this.disable(); + + super.destructor(); + } + + // commands + + async enable() { + if (!this.enabled) { + this.session.contextObserver.on("frame-attached", this._onFrameAttached); + this.session.contextObserver.on("frame-detached", this._onFrameDetached); + this.session.contextObserver.on( + "frame-navigated", + this._onFrameNavigated + ); + + this.chromeEventHandler.addEventListener("readystatechange", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.addEventListener("pagehide", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.addEventListener("unload", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.addEventListener("DOMContentLoaded", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.addEventListener("hashchange", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.addEventListener("load", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.addEventListener("pageshow", this, { + mozSystemGroup: true, + }); + + this.enabled = true; + } + } + + disable() { + if (this.enabled) { + this.session.contextObserver.off("frame-attached", this._onFrameAttached); + this.session.contextObserver.off("frame-detached", this._onFrameDetached); + this.session.contextObserver.off( + "frame-navigated", + this._onFrameNavigated + ); + + this.chromeEventHandler.removeEventListener("readystatechange", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.removeEventListener("pagehide", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.removeEventListener("unload", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.removeEventListener("DOMContentLoaded", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.removeEventListener("hashchange", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.removeEventListener("load", this, { + mozSystemGroup: true, + capture: true, + }); + this.chromeEventHandler.removeEventListener("pageshow", this, { + mozSystemGroup: true, + }); + this.enabled = false; + } + } + + async reload(options = {}) { + const { ignoreCache } = options; + let flags = LOAD_FLAGS_NONE; + if (ignoreCache) { + flags |= LOAD_FLAGS_BYPASS_CACHE; + flags |= LOAD_FLAGS_BYPASS_PROXY; + } + this.docShell.reload(flags); + } + + getFrameTree() { + const getFrames = context => { + const frameTree = { + frame: this._getFrameDetails({ context }), + }; + + if (context.children.length) { + const frames = []; + for (const childContext of context.children) { + frames.push(getFrames(childContext)); + } + frameTree.childFrames = frames; + } + + return frameTree; + }; + + return { + frameTree: getFrames(this.docShell.browsingContext), + }; + } + + /** + * Enqueues given script to be evaluated in every frame upon creation + * + * If `worldName` is specified, creates an execution context with the given name + * and evaluates given script in it. + * + * At this time, queued scripts do not get evaluated, hence `source` is marked as + * "unsupported". + * + * @param {object} options + * @param {string} options.source (not supported) + * @param {string=} options.worldName + * @returns {string} Page.ScriptIdentifier + */ + addScriptToEvaluateOnNewDocument(options = {}) { + const { source, worldName } = options; + if (worldName) { + this.worldsToEvaluateOnLoad.add(worldName); + } + const identifier = lazy.generateUUID(); + this.scriptsToEvaluateOnLoad.set(identifier, { worldName, source }); + + return { identifier }; + } + + /** + * Creates an isolated world for the given frame. + * + * Really it just creates an execution context with label "isolated". + * + * @param {object} options + * @param {string} options.frameId + * Id of the frame in which the isolated world should be created. + * @param {string=} options.worldName + * An optional name which is reported in the Execution Context. + * @param {boolean=} options.grantUniversalAccess (not supported) + * This is a powerful option, use with caution. + * + * @returns {number} Runtime.ExecutionContextId + * Execution context of the isolated world. + */ + createIsolatedWorld(options = {}) { + const { frameId, worldName } = options; + + if (typeof frameId != "string") { + throw new TypeError("frameId: string value expected"); + } + + if (!["undefined", "string"].includes(typeof worldName)) { + throw new TypeError("worldName: string value expected"); + } + + const Runtime = this.session.domains.get("Runtime"); + const contexts = Runtime._getContextsForFrame(frameId); + if (!contexts.length) { + throw new Error("No frame for given id found"); + } + + const defaultContext = Runtime._getDefaultContextForWindow( + contexts[0].windowId + ); + const window = defaultContext.window; + + const executionContextId = Runtime._onContextCreated("context-created", { + windowId: window.windowGlobalChild.innerWindowId, + window, + isDefault: false, + contextName: worldName, + contextType: "isolated", + }); + + return { executionContextId }; + } + + /** + * Controls whether page will emit lifecycle events. + * + * @param {object} options + * @param {boolean} options.enabled + * If true, starts emitting lifecycle events. + */ + setLifecycleEventsEnabled(options = {}) { + const { enabled } = options; + + this.lifecycleEnabled = enabled; + } + + url() { + return this.content.location.href; + } + + _onFrameAttached(name, { frameId, window }) { + const bc = BrowsingContext.get(frameId); + + // Don't emit for top-level browsing contexts + if (!bc.parent) { + return; + } + + // TODO: Use a unique identifier for frames (bug 1605359) + this.emit("Page.frameAttached", { + frameId: frameId.toString(), + parentFrameId: bc.parent.id.toString(), + stack: null, + }); + + // Usually both events are emitted when the "pagehide" event is received. + // But this wont happen for a new window or frame when the initial + // about:blank page has already loaded, and is being replaced with the + // final document. + if (!window.document.isInitialDocument) { + this.emit("Page.frameStartedLoading", { frameId: frameId.toString() }); + + const loaderId = this.frameIdToLoaderId.get(frameId); + const timestamp = Date.now() / 1000; + this.emitLifecycleEvent(frameId, loaderId, "init", timestamp); + } + } + + _onFrameDetached(name, { frameId }) { + const bc = BrowsingContext.get(frameId); + + // Don't emit for top-level browsing contexts + if (!bc.parent) { + return; + } + + // TODO: Use a unique identifier for frames (bug 1605359) + this.emit("Page.frameDetached", { frameId: frameId.toString() }); + } + + _onFrameNavigated(name, { frameId }) { + const bc = BrowsingContext.get(frameId); + + this.emit("Page.frameNavigated", { + frame: this._getFrameDetails({ context: bc }), + }); + } + + /** + * @param {string} name + * The event name. + * @param {object=} options + * @param {number} options.windowId + * The inner window id of the window the script has been loaded for. + * @param {Window} options.window + * The window object of the document. + */ + _onScriptLoaded(name, options = {}) { + const { windowId, window } = options; + + const Runtime = this.session.domains.get("Runtime"); + for (const world of this.worldsToEvaluateOnLoad) { + Runtime._onContextCreated("context-created", { + windowId, + window, + isDefault: false, + contextName: world, + contextType: "isolated", + }); + } + // TODO evaluate each onNewDoc script in the appropriate world + } + + emitLifecycleEvent(frameId, loaderId, name, timestamp) { + if (this.lifecycleEnabled) { + this.emit("Page.lifecycleEvent", { + frameId: frameId.toString(), + loaderId, + name, + timestamp, + }); + } + } + + handleEvent({ type, target }) { + const timestamp = Date.now() / 1000; + + // Some events such as "hashchange" use the window as the target, while + // others have a document. + const win = Window.isInstance(target) ? target : target.defaultView; + const frameId = win.docShell.browsingContext.id; + const isFrame = !!win.docShell.browsingContext.parent; + const loaderId = this.frameIdToLoaderId.get(frameId); + const url = win.location.href; + + switch (type) { + case "DOMContentLoaded": + if (!isFrame) { + this.emit("Page.domContentEventFired", { timestamp }); + } + this.emitLifecycleEvent( + frameId, + loaderId, + "DOMContentLoaded", + timestamp + ); + break; + + case "hashchange": + this.emit("Page.navigatedWithinDocument", { + frameId: frameId.toString(), + url, + }); + break; + + case "pagehide": + // Maybe better to bound to "unload" once we can register for this event + this.emit("Page.frameStartedLoading", { frameId: frameId.toString() }); + this.emitLifecycleEvent(frameId, loaderId, "init", timestamp); + break; + + case "load": + if (!isFrame) { + this.emit("Page.loadEventFired", { timestamp }); + } + this.emitLifecycleEvent(frameId, loaderId, "load", timestamp); + + // XXX this should most likely be sent differently + this.emit("Page.frameStoppedLoading", { frameId: frameId.toString() }); + break; + + case "readystatechange": + if (this.content.document.readyState === "loading") { + this.emitLifecycleEvent(frameId, loaderId, "init", timestamp); + } + } + } + + _updateLoaderId(data) { + const { frameId, loaderId } = data; + + this.frameIdToLoaderId.set(frameId, loaderId); + } + + _contentRect() { + const docEl = this.content.document.documentElement; + + return { + x: 0, + y: 0, + width: docEl.scrollWidth, + height: docEl.scrollHeight, + }; + } + + _devicePixelRatio() { + return ( + this.content.browsingContext.overrideDPPX || this.content.devicePixelRatio + ); + } + + _getFrameDetails({ context, id }) { + const bc = context || BrowsingContext.get(id); + const frame = bc.embedderElement; + + return { + id: bc.id.toString(), + parentId: bc.parent?.id.toString(), + loaderId: this.frameIdToLoaderId.get(bc.id), + url: bc.docShell.domWindow.location.href, + name: frame?.id || frame?.name, + securityOrigin: null, + mimeType: null, + }; + } + + _getScrollbarSize() { + const scrollbarHeight = {}; + const scrollbarWidth = {}; + + this.content.windowUtils.getScrollbarSize( + false, + scrollbarWidth, + scrollbarHeight + ); + + return { + width: scrollbarWidth.value, + height: scrollbarHeight.value, + }; + } + + _layoutViewport() { + const scrollbarSize = this._getScrollbarSize(); + + return { + pageX: this.content.pageXOffset, + pageY: this.content.pageYOffset, + clientWidth: this.content.innerWidth - scrollbarSize.width, + clientHeight: this.content.innerHeight - scrollbarSize.height, + }; + } +} diff --git a/remote/cdp/domains/content/Performance.sys.mjs b/remote/cdp/domains/content/Performance.sys.mjs new file mode 100644 index 0000000000..e5726725b5 --- /dev/null +++ b/remote/cdp/domains/content/Performance.sys.mjs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +export class Performance extends ContentProcessDomain { + constructor(session) { + super(session); + this.enabled = false; + } + + destructor() { + this.disable(); + + super.destructor(); + } + + // commands + + async enable() { + if (!this.enabled) { + this.enabled = true; + } + } + + disable() { + if (this.enabled) { + this.enabled = false; + } + } +} diff --git a/remote/cdp/domains/content/Runtime.sys.mjs b/remote/cdp/domains/content/Runtime.sys.mjs new file mode 100644 index 0000000000..35e0f16710 --- /dev/null +++ b/remote/cdp/domains/content/Runtime.sys.mjs @@ -0,0 +1,641 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs"; + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + executeSoon: "chrome://remote/content/shared/Sync.sys.mjs", + isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs", + ExecutionContext: + "chrome://remote/content/cdp/domains/content/runtime/ExecutionContext.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "ConsoleAPIStorage", () => { + return Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage + ); +}); + +// Import the `Debugger` constructor in the current scope +// eslint-disable-next-line mozilla/reject-globalThis-modification +addDebuggerToGlobal(globalThis); + +const CONSOLE_API_LEVEL_MAP = { + warn: "warning", +}; + +// Bug 1786299: Puppeteer needs specific error messages. +const ERROR_CONTEXT_NOT_FOUND = "Cannot find context with specified id"; + +class SetMap extends Map { + constructor() { + super(); + this._count = 1; + } + // Every key in the map is associated with a Set. + // The first time `key` is used `obj.set(key, value)` maps `key` to + // to `Set(value)`. Subsequent calls add more values to the Set for `key`. + // Note that `obj.get(key)` will return undefined if there's no such key, + // as in a regular Map. + set(key, value) { + const innerSet = this.get(key); + if (innerSet) { + innerSet.add(value); + } else { + super.set(key, new Set([value])); + } + this._count++; + return this; + } + // used as ExecutionContext id + get count() { + return this._count; + } +} + +export class Runtime extends ContentProcessDomain { + constructor(session) { + super(session); + this.enabled = false; + + // Map of all the ExecutionContext instances: + // [id (Number) => ExecutionContext instance] + this.contexts = new Map(); + // [innerWindowId (Number) => Set of ExecutionContext instances] + this.innerWindowIdToContexts = new SetMap(); + + this._onContextCreated = this._onContextCreated.bind(this); + this._onContextDestroyed = this._onContextDestroyed.bind(this); + + // TODO Bug 1602083 + this.session.contextObserver.on("context-created", this._onContextCreated); + this.session.contextObserver.on( + "context-destroyed", + this._onContextDestroyed + ); + } + + destructor() { + this.disable(); + + this.session.contextObserver.off("context-created", this._onContextCreated); + this.session.contextObserver.off( + "context-destroyed", + this._onContextDestroyed + ); + + super.destructor(); + } + + // commands + + async enable() { + if (!this.enabled) { + this.enabled = true; + + Services.console.registerListener(this); + this.onConsoleLogEvent = this.onConsoleLogEvent.bind(this); + lazy.ConsoleAPIStorage.addLogEventListener( + this.onConsoleLogEvent, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + + // Spin the event loop in order to send the `executionContextCreated` event right + // after we replied to `enable` request. + lazy.executeSoon(() => { + this._onContextCreated("context-created", { + windowId: this.content.windowGlobalChild.innerWindowId, + window: this.content, + isDefault: true, + }); + + for (const message of lazy.ConsoleAPIStorage.getEvents()) { + this.onConsoleLogEvent(message); + } + }); + } + } + + disable() { + if (this.enabled) { + this.enabled = false; + + Services.console.unregisterListener(this); + lazy.ConsoleAPIStorage.removeLogEventListener(this.onConsoleLogEvent); + } + } + + releaseObject(options = {}) { + const { objectId } = options; + + let context = null; + for (const ctx of this.contexts.values()) { + if (ctx.hasRemoteObject(objectId)) { + context = ctx; + break; + } + } + if (!context) { + throw new Error(ERROR_CONTEXT_NOT_FOUND); + } + context.releaseObject(objectId); + } + + /** + * Calls function with given declaration on the given object. + * + * Object group of the result is inherited from the target object. + * + * @param {object} options + * @param {string} options.functionDeclaration + * Declaration of the function to call. + * @param {Array.<object>=} options.arguments + * Call arguments. All call arguments must belong to the same + * JavaScript world as the target object. + * @param {boolean=} options.awaitPromise + * Whether execution should `await` for resulting value + * and return once awaited promise is resolved. + * @param {number=} options.executionContextId + * Specifies execution context which global object will be used + * to call function on. Either executionContextId or objectId + * should be specified. + * @param {string=} options.objectId + * Identifier of the object to call function on. + * Either objectId or executionContextId should be specified. + * @param {boolean=} options.returnByValue + * Whether the result is expected to be a JSON object + * which should be sent by value. + * + * @returns {Object<RemoteObject, ExceptionDetails>} + */ + callFunctionOn(options = {}) { + if (typeof options.functionDeclaration != "string") { + throw new TypeError("functionDeclaration: string value expected"); + } + if ( + typeof options.arguments != "undefined" && + !Array.isArray(options.arguments) + ) { + throw new TypeError("arguments: array value expected"); + } + if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) { + throw new TypeError("awaitPromise: boolean value expected"); + } + if (!["undefined", "number"].includes(typeof options.executionContextId)) { + throw new TypeError("executionContextId: number value expected"); + } + if (!["undefined", "string"].includes(typeof options.objectId)) { + throw new TypeError("objectId: string value expected"); + } + if (!["undefined", "boolean"].includes(typeof options.returnByValue)) { + throw new TypeError("returnByValue: boolean value expected"); + } + + if ( + typeof options.executionContextId == "undefined" && + typeof options.objectId == "undefined" + ) { + throw new Error( + "Either objectId or executionContextId must be specified" + ); + } + + let context = null; + // When an `objectId` is passed, we want to execute the function of a given object + // So we first have to find its ExecutionContext + if (options.objectId) { + for (const ctx of this.contexts.values()) { + if (ctx.hasRemoteObject(options.objectId)) { + context = ctx; + break; + } + } + } else { + context = this.contexts.get(options.executionContextId); + } + + if (!context) { + throw new Error(ERROR_CONTEXT_NOT_FOUND); + } + + return context.callFunctionOn( + options.functionDeclaration, + options.arguments, + options.returnByValue, + options.awaitPromise, + options.objectId + ); + } + + /** + * Evaluate expression on global object. + * + * @param {object} options + * @param {string} options.expression + * Expression to evaluate. + * @param {boolean=} options.awaitPromise + * Whether execution should `await` for resulting value + * and return once awaited promise is resolved. + * @param {number=} options.contextId + * Specifies in which execution context to perform evaluation. + * If the parameter is omitted the evaluation will be performed + * in the context of the inspected page. + * @param {boolean=} options.returnByValue + * Whether the result is expected to be a JSON object + * that should be sent by value. Defaults to false. + * @param {boolean=} options.userGesture [unsupported] + * Whether execution should be treated as initiated by user in the UI. + * + * @returns {Object<RemoteObject, exceptionDetails>} + * The evaluation result, and optionally exception details. + */ + evaluate(options = {}) { + const { + expression, + awaitPromise = false, + contextId, + returnByValue = false, + } = options; + + if (typeof expression != "string") { + throw new Error("expression: string value expected"); + } + if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) { + throw new TypeError("awaitPromise: boolean value expected"); + } + if (typeof returnByValue != "boolean") { + throw new Error("returnByValue: boolean value expected"); + } + + let context; + if (typeof contextId != "undefined") { + context = this.contexts.get(contextId); + if (!context) { + throw new Error(ERROR_CONTEXT_NOT_FOUND); + } + } else { + context = this._getDefaultContextForWindow(); + } + + return context.evaluate(expression, awaitPromise, returnByValue); + } + + getProperties(options = {}) { + const { objectId, ownProperties } = options; + + for (const ctx of this.contexts.values()) { + const debuggerObj = ctx.getRemoteObject(objectId); + if (debuggerObj) { + return ctx.getProperties({ objectId, ownProperties }); + } + } + return null; + } + + /** + * Internal methods: the following methods are not part of CDP; + * note the _ prefix. + */ + + get _debugger() { + if (this.__debugger) { + return this.__debugger; + } + this.__debugger = new Debugger(); + return this.__debugger; + } + + _buildExceptionStackTrace(stack) { + const callFrames = []; + + while ( + stack && + stack.source !== "debugger eval code" && + !stack.source.startsWith("chrome://") + ) { + callFrames.push({ + functionName: stack.functionDisplayName, + scriptId: stack.sourceId.toString(), + url: stack.source, + lineNumber: stack.line - 1, + columnNumber: stack.column - 1, + }); + stack = stack.parent || stack.asyncParent; + } + + return { + callFrames, + }; + } + + _buildConsoleStackTrace(stack = []) { + const callFrames = stack + .filter(frame => !lazy.isChromeFrame(frame)) + .map(frame => { + return { + functionName: frame.functionName, + scriptId: frame.sourceId.toString(), + url: frame.filename, + lineNumber: frame.lineNumber - 1, + columnNumber: frame.columnNumber - 1, + }; + }); + + return { + callFrames, + }; + } + + _getRemoteObject(objectId) { + for (const ctx of this.contexts.values()) { + const debuggerObj = ctx.getRemoteObject(objectId); + if (debuggerObj) { + return debuggerObj; + } + } + return null; + } + + _serializeRemoteObject(debuggerObj, executionContextId) { + const ctx = this.contexts.get(executionContextId); + return ctx._toRemoteObject(debuggerObj); + } + + _getRemoteObjectByNodeId(nodeId, executionContextId) { + let debuggerObj = null; + + if (typeof executionContextId != "undefined") { + const ctx = this.contexts.get(executionContextId); + debuggerObj = ctx.getRemoteObjectByNodeId(nodeId); + } else { + for (const ctx of this.contexts.values()) { + const obj = ctx.getRemoteObjectByNodeId(nodeId); + if (obj) { + debuggerObj = obj; + break; + } + } + } + + return debuggerObj; + } + + _setRemoteObject(debuggerObj, context) { + return context.setRemoteObject(debuggerObj); + } + + _getDefaultContextForWindow(innerWindowId) { + if (!innerWindowId) { + innerWindowId = this.content.windowGlobalChild.innerWindowId; + } + const curContexts = this.innerWindowIdToContexts.get(innerWindowId); + if (curContexts) { + for (const ctx of curContexts) { + if (ctx.isDefault) { + return ctx; + } + } + } + return null; + } + + _getContextsForFrame(frameId) { + const frameContexts = []; + for (const ctx of this.contexts.values()) { + if (ctx.frameId == frameId) { + frameContexts.push(ctx); + } + } + return frameContexts; + } + + _emitConsoleAPICalled(payload) { + // Filter out messages that aren't coming from a valid inner window, or from + // a different browser tab. Also messages of type "time", which are not + // getting reported by Chrome. + const curBrowserId = this.session.browsingContext.browserId; + const win = Services.wm.getCurrentInnerWindowWithId(payload.innerWindowId); + if ( + !win || + BrowsingContext.getFromWindow(win).browserId != curBrowserId || + payload.type === "time" + ) { + return; + } + + const context = this._getDefaultContextForWindow(); + this.emit("Runtime.consoleAPICalled", { + args: payload.arguments.map(arg => context._toRemoteObject(arg)), + executionContextId: context?.id || 0, + timestamp: payload.timestamp, + type: payload.type, + stackTrace: this._buildConsoleStackTrace(payload.stack), + }); + } + + _emitExceptionThrown(payload) { + // Filter out messages that aren't coming from a valid inner window, or from + // a different browser tab. Also messages of type "time", which are not + // getting reported by Chrome. + const curBrowserId = this.session.browsingContext.browserId; + const win = Services.wm.getCurrentInnerWindowWithId(payload.innerWindowId); + if (!win || BrowsingContext.getFromWindow(win).browserId != curBrowserId) { + return; + } + + const context = this._getDefaultContextForWindow(); + this.emit("Runtime.exceptionThrown", { + timestamp: payload.timestamp, + exceptionDetails: { + // Temporary placeholder to return a number. + exceptionId: 0, + text: payload.text, + lineNumber: payload.lineNumber, + columnNumber: payload.columnNumber, + url: payload.url, + stackTrace: this._buildExceptionStackTrace(payload.stack), + executionContextId: context?.id || undefined, + }, + }); + } + + /** + * Helper method in order to instantiate the ExecutionContext for a given + * DOM Window as well as emitting the related + * `Runtime.executionContextCreated` event + * + * @param {string} name + * Event name + * @param {object=} options + * @param {number} options.windowId + * The inner window id of the newly instantiated document. + * @param {Window} options.window + * The window object of the newly instantiated document. + * @param {string=} options.contextName + * Human-readable name to describe the execution context. + * @param {boolean=} options.isDefault + * Whether the execution context is the default one. + * @param {string=} options.contextType + * "default" or "isolated" + * + * @returns {number} ID of created context + * + */ + _onContextCreated(name, options = {}) { + const { + windowId, + window, + contextName = "", + isDefault = true, + contextType = "default", + } = options; + + if (windowId === undefined) { + throw new Error("windowId is required"); + } + + // allow only one default context per inner window + if (isDefault && this.innerWindowIdToContexts.has(windowId)) { + for (const ctx of this.innerWindowIdToContexts.get(windowId)) { + if (ctx.isDefault) { + return null; + } + } + } + + const context = new lazy.ExecutionContext( + this._debugger, + window, + this.innerWindowIdToContexts.count, + isDefault + ); + this.contexts.set(context.id, context); + this.innerWindowIdToContexts.set(windowId, context); + + if (this.enabled) { + this.emit("Runtime.executionContextCreated", { + context: { + id: context.id, + origin: window.origin, + name: contextName, + auxData: { + isDefault, + frameId: context.frameId, + type: contextType, + }, + }, + }); + } + + return context.id; + } + + /** + * Helper method to destroy the ExecutionContext of the given id. Also emit + * the related `Runtime.executionContextDestroyed` and + * `Runtime.executionContextsCleared` events. + * ContextObserver will call this method with either `id` or `frameId` argument + * being set. + * + * @param {string} name + * Event name + * @param {object=} options + * @param {number} options.id + * The execution context id to destroy. + * @param {number} options.windowId + * The inner-window id of the execution context to destroy. + * @param {number} options.frameId + * The frame id of execution context to destroy. + * Either `id` or `frameId` or `windowId` is passed. + */ + _onContextDestroyed(name, { id, frameId, windowId }) { + let contexts; + if ([id, frameId, windowId].filter(id => !!id).length > 1) { + throw new Error("Expects only *one* of id, frameId, windowId"); + } + + if (id) { + contexts = [this.contexts.get(id)]; + } else if (frameId) { + contexts = this._getContextsForFrame(frameId); + } else { + contexts = this.innerWindowIdToContexts.get(windowId) || []; + } + + for (const ctx of contexts) { + const isFrame = !!BrowsingContext.get(ctx.frameId).parent; + + ctx.destructor(); + this.contexts.delete(ctx.id); + this.innerWindowIdToContexts.get(ctx.windowId).delete(ctx); + + if (this.enabled) { + this.emit("Runtime.executionContextDestroyed", { + executionContextId: ctx.id, + }); + } + + if (this.innerWindowIdToContexts.get(ctx.windowId).size == 0) { + this.innerWindowIdToContexts.delete(ctx.windowId); + // Only emit when all the exeuction contexts were cleared for the + // current browser / target, which means it should only be emitted + // for a top-level browsing context reference. + if (this.enabled && !isFrame) { + this.emit("Runtime.executionContextsCleared"); + } + } + } + } + + onConsoleLogEvent(message) { + // From sendConsoleAPIMessage (toolkit/modules/Console.sys.mjs) + this._emitConsoleAPICalled({ + arguments: message.arguments, + innerWindowId: message.innerID, + stack: message.stacktrace, + timestamp: message.timeStamp, + type: CONSOLE_API_LEVEL_MAP[message.level] || message.level, + }); + } + + // nsIObserver + + /** + * Takes a console message belonging to the current window and emits a + * "exceptionThrown" event if it's a Javascript error, otherwise a + * "consoleAPICalled" event. + * + * @param {nsIConsoleMessage} subject + * Console message. + */ + observe(subject, topic, data) { + if (subject instanceof Ci.nsIScriptError && subject.hasException) { + let entry = fromScriptError(subject); + this._emitExceptionThrown(entry); + } + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIConsoleListener"]); + } +} + +function fromScriptError(error) { + // From dom/bindings/nsIScriptError.idl + return { + innerWindowId: error.innerWindowID, + columnNumber: error.columnNumber - 1, + lineNumber: error.lineNumber - 1, + stack: error.stack, + text: error.errorMessage, + timestamp: error.timeStamp, + url: error.sourceName, + }; +} diff --git a/remote/cdp/domains/content/Security.sys.mjs b/remote/cdp/domains/content/Security.sys.mjs new file mode 100644 index 0000000000..7d21d386b4 --- /dev/null +++ b/remote/cdp/domains/content/Security.sys.mjs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs"; + +export class Security extends ContentProcessDomain { + constructor(session) { + super(session); + this.enabled = false; + } + + destructor() { + this.disable(); + + super.destructor(); + } + + // commands + + async enable() { + if (!this.enabled) { + this.enabled = true; + } + } + + disable() { + if (this.enabled) { + this.enabled = false; + } + } +} diff --git a/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs new file mode 100644 index 0000000000..4d394f6bf9 --- /dev/null +++ b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs @@ -0,0 +1,564 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", +}); + +const TYPED_ARRAY_CLASSES = [ + "Uint8Array", + "Uint8ClampedArray", + "Uint16Array", + "Uint32Array", + "Int8Array", + "Int16Array", + "Int32Array", + "Float32Array", + "Float64Array", +]; + +// Bug 1786299: Puppeteer expects specific error messages. +const ERROR_CYCLIC_REFERENCE = "Object reference chain is too long"; +const ERROR_CANNOT_RETURN_BY_VALUE = "Object couldn't be returned by value"; + +function randomInt() { + return crypto.getRandomValues(new Uint32Array(1))[0]; +} + +/** + * This class represent a debuggable context onto which we can evaluate Javascript. + * This is typically a document, but it could also be a worker, an add-on, ... or + * any kind of context involving JS scripts. + * + * @param {Debugger} dbg + * A Debugger instance that we can use to inspect the given global. + * @param {GlobalObject} debuggee + * The debuggable context's global object. This is typically the document window + * object. But it can also be any global object, like a worker global scope object. + */ +export class ExecutionContext { + constructor(dbg, debuggee, id, isDefault) { + this._debugger = dbg; + this._debuggee = this._debugger.addDebuggee(debuggee); + + // Here, we assume that debuggee is a window object and we will propably have + // to adapt that once we cover workers or contexts that aren't a document. + this.window = debuggee; + this.windowId = this.window.windowGlobalChild.innerWindowId; + this.id = id; + this.frameId = this.window.browsingContext.id.toString(); + this.isDefault = isDefault; + + // objectId => Debugger.Object + this._remoteObjects = new Map(); + } + + destructor() { + this._debugger.removeDebuggee(this._debuggee); + } + + get browsingContext() { + return this.window.browsingContext; + } + + hasRemoteObject(objectId) { + return this._remoteObjects.has(objectId); + } + + getRemoteObject(objectId) { + return this._remoteObjects.get(objectId); + } + + getRemoteObjectByNodeId(nodeId) { + for (const value of this._remoteObjects.values()) { + if (value.nodeId == nodeId) { + return value; + } + } + + return null; + } + + releaseObject(objectId) { + return this._remoteObjects.delete(objectId); + } + + /** + * Add a new debuggerObj to the object cache. + * + * Whenever an object is returned as reference, a new entry is added + * to the internal object cache. It means the same underlying object or node + * can be represented via multiple references. + */ + setRemoteObject(debuggerObj) { + const objectId = lazy.generateUUID(); + + // TODO: Wrap Symbol into an object, + // which would allow us to set the objectId. + if (typeof debuggerObj == "object") { + debuggerObj.objectId = objectId; + } + + // For node objects add an unique identifier. + if ( + debuggerObj instanceof Debugger.Object && + Node.isInstance(debuggerObj.unsafeDereference()) + ) { + debuggerObj.nodeId = randomInt(); + // We do not differentiate between backendNodeId and nodeId (yet) + debuggerObj.backendNodeId = debuggerObj.nodeId; + } + + this._remoteObjects.set(objectId, debuggerObj); + + return objectId; + } + + /** + * Evaluate a Javascript expression. + * + * @param {string} expression + * The JS expression to evaluate against the JS context. + * @param {boolean} awaitPromise + * Whether execution should `await` for resulting value + * and return once awaited promise is resolved. + * @param {boolean} returnByValue + * Whether the result is expected to be a JSON object + * that should be sent by value. + * + * @returns {object} A multi-form object depending if the execution + * succeed or failed. If the expression failed to evaluate, + * it will return an object with an `exceptionDetails` attribute + * matching the `ExceptionDetails` CDP type. Otherwise it will + * return an object with `result` attribute whose type is + * `RemoteObject` CDP type. + */ + async evaluate(expression, awaitPromise, returnByValue) { + let rv = this._debuggee.executeInGlobal(expression); + if (!rv) { + return { + exceptionDetails: { + text: "Evaluation terminated!", + }, + }; + } + + if (rv.throw) { + return this._returnError(rv.throw); + } + + let result = rv.return; + + if (result && result.isPromise && awaitPromise) { + if (result.promiseState === "fulfilled") { + result = result.promiseValue; + } else if (result.promiseState === "rejected") { + return this._returnError(result.promiseReason); + } else { + try { + const promiseResult = await result.unsafeDereference(); + result = this._debuggee.makeDebuggeeValue(promiseResult); + } catch (e) { + // The promise has been rejected + return this._returnError(this._debuggee.makeDebuggeeValue(e)); + } + } + } + + if (returnByValue) { + result = this._toRemoteObjectByValue(result); + } else { + result = this._toRemoteObject(result); + } + + return { result }; + } + + /** + * Given a Debugger.Object reference for an Exception, return a JSON object + * describing the exception by following CDP ExceptionDetails specification. + */ + _returnError(exception) { + if ( + this._debuggee.executeInGlobalWithBindings("exception instanceof Error", { + exception, + }).return + ) { + const text = this._debuggee.executeInGlobalWithBindings( + "exception.message", + { exception } + ).return; + return { + exceptionDetails: { + text, + }, + }; + } + + // If that isn't an Error, consider the exception as a JS value + return { + exceptionDetails: { + exception: this._toRemoteObject(exception), + }, + }; + } + + async callFunctionOn( + functionDeclaration, + callArguments = [], + returnByValue = false, + awaitPromise = false, + objectId = null + ) { + // Map the given objectId to a JS reference. + let thisArg = null; + if (objectId) { + thisArg = this.getRemoteObject(objectId); + if (!thisArg) { + throw new Error(`Unable to get target object with id: ${objectId}`); + } + } + + // First evaluate the function + const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")"); + if (!fun) { + return { + exceptionDetails: { + text: "Evaluation terminated!", + }, + }; + } + if (fun.throw) { + return this._returnError(fun.throw); + } + + // Then map all input arguments, which are matching CDP's CallArguments type, + // into JS values + const args = callArguments.map(arg => this._fromCallArgument(arg)); + + // Finally, call the function with these arguments + const rv = fun.return.apply(thisArg, args); + if (rv.throw) { + return this._returnError(rv.throw); + } + + let result = rv.return; + + if (result && result.isPromise && awaitPromise) { + if (result.promiseState === "fulfilled") { + result = result.promiseValue; + } else if (result.promiseState === "rejected") { + return this._returnError(result.promiseReason); + } else { + try { + const promiseResult = await result.unsafeDereference(); + result = this._debuggee.makeDebuggeeValue(promiseResult); + } catch (e) { + // The promise has been rejected + return this._returnError(this._debuggee.makeDebuggeeValue(e)); + } + } + } + + if (returnByValue) { + result = this._toRemoteObjectByValue(result); + } else { + result = this._toRemoteObject(result); + } + + return { result }; + } + + getProperties({ objectId, ownProperties }) { + let debuggerObj = this.getRemoteObject(objectId); + if (!debuggerObj) { + throw new Error("Could not find object with given id"); + } + + const result = []; + const serializeObject = (debuggerObj, isOwn) => { + for (const propertyName of debuggerObj.getOwnPropertyNames()) { + const descriptor = debuggerObj.getOwnPropertyDescriptor(propertyName); + result.push({ + name: propertyName, + + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + writable: descriptor.writable, + value: this._toRemoteObject(descriptor.value), + get: descriptor.get + ? this._toRemoteObject(descriptor.get) + : undefined, + set: descriptor.set + ? this._toRemoteObject(descriptor.set) + : undefined, + + isOwn, + }); + } + }; + + // When `ownProperties` is set to true, we only iterate over own properties. + // Otherwise, we also iterate over propreties inherited from the prototype chain. + serializeObject(debuggerObj, true); + + if (!ownProperties) { + while (true) { + debuggerObj = debuggerObj.proto; + if (!debuggerObj) { + break; + } + serializeObject(debuggerObj, false); + } + } + + return { + result, + }; + } + + /** + * Given a CDP `CallArgument`, return a JS value that represent this argument. + * Note that `CallArgument` is actually very similar to `RemoteObject` + */ + _fromCallArgument(arg) { + if (arg.objectId) { + if (!this.hasRemoteObject(arg.objectId)) { + throw new Error("Could not find object with given id"); + } + return this.getRemoteObject(arg.objectId); + } + + if (arg.unserializableValue) { + switch (arg.unserializableValue) { + case "-0": + return -0; + case "Infinity": + return Infinity; + case "-Infinity": + return -Infinity; + case "NaN": + return NaN; + default: + if (/^\d+n$/.test(arg.unserializableValue)) { + // eslint-disable-next-line no-undef + return BigInt(arg.unserializableValue.slice(0, -1)); + } + throw new Error("Couldn't parse value object in call argument"); + } + } + + return this._deserialize(arg.value); + } + + /** + * Given a JS value, create a copy of it within the debugee compartment. + */ + _deserialize(obj) { + if (typeof obj !== "object") { + return obj; + } + const result = this._debuggee.executeInGlobalWithBindings( + "JSON.parse(obj)", + { obj: JSON.stringify(obj) } + ); + if (result.throw) { + throw new Error("Unable to deserialize object"); + } + return result.return; + } + + /** + * Given a `Debugger.Object` object, return a JSON-serializable description of it + * matching `RemoteObject` CDP type. + * + * @param {Debugger.Object} debuggerObj + * The object to serialize + * @returns {RemoteObject} + * The serialized description of the given object + */ + _toRemoteObject(debuggerObj) { + const result = {}; + + // First handle all non-primitive values which are going to be wrapped by the + // Debugger API into Debugger.Object instances + if (debuggerObj instanceof Debugger.Object) { + const rawObj = debuggerObj.unsafeDereference(); + + result.objectId = this.setRemoteObject(debuggerObj); + result.type = typeof rawObj; + + // Map the Debugger API `class` attribute to CDP `subtype` + const cls = debuggerObj.class; + if (debuggerObj.isProxy) { + result.subtype = "proxy"; + } else if (cls == "Array") { + result.subtype = "array"; + } else if (cls == "RegExp") { + result.subtype = "regexp"; + } else if (cls == "Date") { + result.subtype = "date"; + } else if (cls == "Map") { + result.subtype = "map"; + } else if (cls == "Set") { + result.subtype = "set"; + } else if (cls == "WeakMap") { + result.subtype = "weakmap"; + } else if (cls == "WeakSet") { + result.subtype = "weakset"; + } else if (cls == "Error") { + result.subtype = "error"; + } else if (cls == "Promise") { + result.subtype = "promise"; + } else if (TYPED_ARRAY_CLASSES.includes(cls)) { + result.subtype = "typedarray"; + } else if (Node.isInstance(rawObj)) { + result.subtype = "node"; + result.className = ChromeUtils.getClassName(rawObj); + result.description = rawObj.localName || rawObj.nodeName; + if (rawObj.id) { + result.description += `#${rawObj.id}`; + } + } + return result; + } + + // Now, handle all values that Debugger API isn't wrapping into Debugger.API. + // This is all the primitive JS types. + result.type = typeof debuggerObj; + + // Symbol and BigInt are primitive values but aren't serializable. + // CDP expects them to be considered as objects, with an objectId to later inspect + // them. + if (result.type == "symbol") { + result.description = debuggerObj.toString(); + result.objectId = this.setRemoteObject(debuggerObj); + + return result; + } + + // A few primitive type can't be serialized and CDP has special case for them + if (Object.is(debuggerObj, NaN)) { + result.unserializableValue = "NaN"; + } else if (Object.is(debuggerObj, -0)) { + result.unserializableValue = "-0"; + } else if (Object.is(debuggerObj, Infinity)) { + result.unserializableValue = "Infinity"; + } else if (Object.is(debuggerObj, -Infinity)) { + result.unserializableValue = "-Infinity"; + } else if (result.type == "bigint") { + result.unserializableValue = `${debuggerObj}n`; + } + + if (result.unserializableValue) { + result.description = result.unserializableValue; + return result; + } + + // Otherwise, we serialize the primitive values as-is via `value` attribute + result.value = debuggerObj; + + // null is special as it has a dedicated subtype + if (debuggerObj === null) { + result.subtype = "null"; + } + + return result; + } + + /** + * Given a `Debugger.Object` object, return a JSON-serializable description of it + * matching `RemoteObject` CDP type. + * + * @param {Debugger.Object} debuggerObj + * The object to serialize + * @returns {RemoteObject} + * The serialized description of the given object + */ + _toRemoteObjectByValue(debuggerObj) { + const type = typeof debuggerObj; + + if (type == "undefined") { + return { type }; + } + + let unserializableValue; + if (Object.is(debuggerObj, -0)) { + unserializableValue = "-0"; + } else if (Object.is(debuggerObj, NaN)) { + unserializableValue = "NaN"; + } else if (Object.is(debuggerObj, Infinity)) { + unserializableValue = "Infinity"; + } else if (Object.is(debuggerObj, -Infinity)) { + unserializableValue = "-Infinity"; + } else if (typeof debuggerObj == "bigint") { + unserializableValue = `${debuggerObj}n`; + } + + if (unserializableValue) { + return { + type, + unserializableValue, + description: unserializableValue, + }; + } + + const value = this._serialize(debuggerObj); + return { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }; + } + + /** + * Convert a given `Debugger.Object` to an object. + * + * @param {Debugger.Object} debuggerObj + * The object to convert + * + * @returns {object} + * The converted object + */ + _serialize(debuggerObj) { + const result = this._debuggee.executeInGlobalWithBindings( + ` + JSON.stringify(e, (key, value) => { + if (typeof value === "symbol") { + // CDP cannot return Symbols + throw new Error(); + } + + return value; + }); + `, + { e: debuggerObj } + ); + if (result.throw) { + const exception = this._toRawObject(result.throw); + if (exception.message === "cyclic object value") { + throw new Error(ERROR_CYCLIC_REFERENCE); + } + + throw new Error(ERROR_CANNOT_RETURN_BY_VALUE); + } + + return JSON.parse(result.return); + } + + _toRawObject(maybeDebuggerObject) { + if (maybeDebuggerObject instanceof Debugger.Object) { + // Retrieve the referent for the provided Debugger.object. + // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html + const rawObject = maybeDebuggerObject.unsafeDereference(); + return Cu.waiveXrays(rawObject); + } + + // If maybeDebuggerObject was not a Debugger.Object, it is a primitive value + // which can be used as is. + return maybeDebuggerObject; + } +} diff --git a/remote/cdp/domains/parent/Browser.sys.mjs b/remote/cdp/domains/parent/Browser.sys.mjs new file mode 100644 index 0000000000..ecf93d4d8d --- /dev/null +++ b/remote/cdp/domains/parent/Browser.sys.mjs @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +export class Browser extends Domain { + getVersion() { + const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService( + Ci.nsIGfxInfo + ); + const { userAgent } = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + return { + jsVersion: Services.appinfo.version, + protocolVersion: "1.3", + product: + (isHeadless ? "Headless" : "") + + `${Services.appinfo.name}/${Services.appinfo.version}`, + revision: Services.appinfo.sourceURL.split("/").pop(), + userAgent, + }; + } + + close() { + // Notify all windows that an application quit has been requested. + const cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested"); + + // If the shutdown of the application is prevented force quit it instead. + const mode = cancelQuit.data + ? Ci.nsIAppStartup.eForceQuit + : Ci.nsIAppStartup.eAttemptQuit; + + Services.startup.quit(mode); + } +} diff --git a/remote/cdp/domains/parent/Emulation.sys.mjs b/remote/cdp/domains/parent/Emulation.sys.mjs new file mode 100644 index 0000000000..21aaf2f965 --- /dev/null +++ b/remote/cdp/domains/parent/Emulation.sys.mjs @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", +}); + +const MAX_WINDOW_SIZE = 10000000; + +export class Emulation extends Domain { + destructor() { + this.setUserAgentOverride({ userAgent: "", platform: "" }); + + super.destructor(); + } + + /** + * Overrides the values of device screen dimensions. + * + * Values as modified are: + * - window.screen.width + * - window.screen.height + * - window.innerWidth + * - window.innerHeight + * - "device-width"/"device-height"-related CSS media query results + * + * @param {object} options + * @param {number} options.width + * Overriding width value in pixels. 0 disables the override. + * @param {number} options.height + * Overriding height value in pixels. 0 disables the override. + * @param {number} options.deviceScaleFactor + * Overriding device scale factor value. 0 disables the override. + * @param {number} options.mobile [not supported] + * Whether to emulate a mobile device. This includes viewport meta tag, + * overlay scrollbars, text autosizing and more. + * @param {number} options.screenOrientation + * Screen orientation override [not supported] + */ + async setDeviceMetricsOverride(options = {}) { + const { width, height, deviceScaleFactor } = options; + + if ( + width < 0 || + width > MAX_WINDOW_SIZE || + height < 0 || + height > MAX_WINDOW_SIZE + ) { + throw new TypeError( + `Width and height values must be positive, not greater than ${MAX_WINDOW_SIZE}` + ); + } + + if (typeof deviceScaleFactor != "number") { + throw new TypeError("deviceScaleFactor: number expected"); + } + + if (deviceScaleFactor < 0) { + throw new TypeError("deviceScaleFactor: must be positive"); + } + + const { tab } = this.session.target; + const { linkedBrowser: browser } = tab; + + const { browsingContext } = this.session.target; + browsingContext.overrideDPPX = deviceScaleFactor; + + // With a value of 0 the current size is used + const { layoutViewport } = await this.session.execute( + this.session.id, + "Page", + "getLayoutMetrics" + ); + + const targetWidth = width > 0 ? width : layoutViewport.clientWidth; + const targetHeight = height > 0 ? height : layoutViewport.clientHeight; + + browser.style.setProperty("min-width", targetWidth + "px"); + browser.style.setProperty("max-width", targetWidth + "px"); + browser.style.setProperty("min-height", targetHeight + "px"); + browser.style.setProperty("max-height", targetHeight + "px"); + + // Wait until the viewport has been resized + await this.executeInChild("_awaitViewportDimensions", { + width: targetWidth, + height: targetHeight, + }); + } + + /** + * Enables touch on platforms which do not support them. + * + * @param {object} options + * @param {boolean} options.enabled + * Whether the touch event emulation should be enabled. + * @param {number=} options.maxTouchPoints [not yet supported] + * Maximum touch points supported. Defaults to one. + */ + async setTouchEmulationEnabled(options = {}) { + const { enabled } = options; + + if (typeof enabled != "boolean") { + throw new TypeError( + "Invalid parameters (enabled: boolean value expected)" + ); + } + + const { browsingContext } = this.session.target; + if (enabled) { + browsingContext.touchEventsOverride = "enabled"; + } else { + browsingContext.touchEventsOverride = "none"; + } + } + + /** + * Allows overriding user agent with the given string. + * + * @param {object} options + * @param {string} options.userAgent + * User agent to use. + * @param {string=} options.acceptLanguage [not yet supported] + * Browser langugage to emulate. + * @param {string=} options.platform + * The platform navigator.platform should return. + */ + async setUserAgentOverride(options = {}) { + const { userAgent, platform } = options; + + if (typeof userAgent != "string") { + throw new TypeError( + "Invalid parameters (userAgent: string value expected)" + ); + } + + if (!["undefined", "string"].includes(typeof platform)) { + throw new TypeError("platform: string value expected"); + } + + const { browsingContext } = this.session.target; + + if (!userAgent.length) { + browsingContext.customUserAgent = null; + } else if (this._isValidHTTPRequestHeaderValue(userAgent)) { + browsingContext.customUserAgent = userAgent; + } else { + throw new TypeError("Invalid characters found in userAgent"); + } + + if (platform?.length > 0) { + browsingContext.customPlatform = platform; + } else { + browsingContext.customPlatform = null; + } + } + + _isValidHTTPRequestHeaderValue(value) { + try { + const channel = lazy.NetUtil.newChannel({ + uri: "http://localhost", + loadUsingSystemPrincipal: true, + }); + channel.QueryInterface(Ci.nsIHttpChannel); + channel.setRequestHeader("X-check", value, false); + return true; + } catch (e) { + return false; + } + } +} diff --git a/remote/cdp/domains/parent/Fetch.sys.mjs b/remote/cdp/domains/parent/Fetch.sys.mjs new file mode 100644 index 0000000000..39e6965ccd --- /dev/null +++ b/remote/cdp/domains/parent/Fetch.sys.mjs @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +// Note: For now this domain has only been added so that clients using CDP +// (like Selenium) don't break when trying to disable Fetch events. + +export class Fetch extends Domain { + constructor(session) { + super(session); + + this.enabled = false; + } + + destructor() { + this.disable(); + + super.destructor(); + } + + disable() { + if (!this.enabled) { + return; + } + + this.enabled = false; + } +} diff --git a/remote/cdp/domains/parent/IO.sys.mjs b/remote/cdp/domains/parent/IO.sys.mjs new file mode 100644 index 0000000000..b7eeb75774 --- /dev/null +++ b/remote/cdp/domains/parent/IO.sys.mjs @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; +import { StreamRegistry } from "chrome://remote/content/cdp/StreamRegistry.sys.mjs"; + +const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024; + +// Global singleton for managing open streams +export const streamRegistry = new StreamRegistry(); + +export class IO extends Domain { + // commands + + /** + * Close the stream, discard any temporary backing storage. + * + * @param {object} options + * @param {string} options.handle + * Handle of the stream to close. + */ + async close(options = {}) { + const { handle } = options; + + if (typeof handle != "string") { + throw new TypeError(`handle: string value expected`); + } + + await streamRegistry.remove(handle); + } + + /** + * Read a chunk of the stream. + * + * @param {object} options + * @param {string} options.handle + * Handle of the stream to read. + * @param {number=} options.offset + * Seek to the specified offset before reading. If not specificed, + * proceed with offset following the last read. + * Some types of streams may only support sequential reads. + * @param {number=} options.size + * Maximum number of bytes to read (left upon the agent + * discretion if not specified). + * + * @returns {object} + * Data that were read, including flags for base64-encoded, and end-of-file reached. + */ + async read(options = {}) { + const { handle, offset, size } = options; + + if (typeof handle != "string") { + throw new TypeError(`handle: string value expected`); + } + + const stream = streamRegistry.get(handle); + + if (typeof offset != "undefined") { + if (typeof offset != "number") { + throw new TypeError(`offset: integer value expected`); + } + + await stream.seek(offset); + } + + const remainingBytes = await stream.available(); + + let chunkSize; + if (typeof size != "undefined") { + if (typeof size != "number") { + throw new TypeError(`size: integer value expected`); + } + + // Chromium currently crashes for negative sizes (https://bit.ly/2P6h0Fv), + // but might behave similar to the offset and clip invalid values + chunkSize = Math.max(0, Math.min(size, remainingBytes)); + } else { + chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remainingBytes); + } + + const bytes = await stream.readBytes(chunkSize); + + // Each UCS2 character has an upper byte of 0 and a lower byte matching + // the binary data. Using a loop here prevents us from hitting the browser's + // internal `arguments.length` limit. + const ARGS_MAX = 262144; + const stringData = []; + for (let i = 0; i < bytes.length; i += ARGS_MAX) { + let argsChunk = Math.min(bytes.length, i + ARGS_MAX); + stringData.push( + String.fromCharCode.apply(null, bytes.slice(i, argsChunk)) + ); + } + const data = btoa(stringData.join("")); + + return { + data, + base64Encoded: true, + eof: remainingBytes - bytes.length == 0, + }; + } +} diff --git a/remote/cdp/domains/parent/Input.sys.mjs b/remote/cdp/domains/parent/Input.sys.mjs new file mode 100644 index 0000000000..4121298d50 --- /dev/null +++ b/remote/cdp/domains/parent/Input.sys.mjs @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +export class Input extends Domain { + // commands + + /** + * Simulate key events. + * + * @param {object} options + * - autoRepeat (not supported) + * - code (not supported) + * - key + * - isKeypad (not supported) + * - location (not supported) + * - modifiers + * - text (not supported) + * - type + * - unmodifiedText (not supported) + * - windowsVirtualKeyCode + * - nativeVirtualKeyCode (not supported) + * - keyIdentifier (not supported) + * - isSystemKey (not supported) + */ + async dispatchKeyEvent(options = {}) { + // missing code, text, unmodifiedText, autorepeat, location, iskeypad + const { key, modifiers, type, windowsVirtualKeyCode } = options; + const { alt, ctrl, meta, shift } = Input.Modifier; + + let domType; + if (type == "keyDown" || type == "rawKeyDown") { + // 'rawKeyDown' is passed as type by puppeteer for all non-text keydown events: + // See https://github.com/GoogleChrome/puppeteer/blob/2d99d85976dcb28cc6e3bad4b6a00cd61a67a2cf/lib/Input.js#L52 + // For now we simply map rawKeyDown to keydown. + domType = "keydown"; + } else if (type == "keyUp" || type == "char") { + // 'char' is fired as a single key event. Behind the scenes it will trigger keydown, + // keypress and keyup. `domType` will only be used as the event to wait for. + domType = "keyup"; + } else { + throw new Error(`Unknown key event type ${type}`); + } + + const { browser } = this.session.target; + const browserWindow = browser.ownerGlobal; + + const EventUtils = this._getEventUtils(browserWindow); + const eventId = await this.executeInChild( + "_addContentEventListener", + domType + ); + + if (type == "char") { + // type == "char" is used when doing `await page.keyboard.type( 'I’m a list' );` + // the ’ character will be calling dispatchKeyEvent only once with type=char. + EventUtils.synthesizeKey(key, {}, browserWindow); + } else { + // Non printable keys should be prefixed with `KEY_` + const eventUtilsKey = key.length == 1 ? key : "KEY_" + key; + const eventInfo = { + keyCode: windowsVirtualKeyCode, + type: domType, + altKey: !!(modifiers & alt), + ctrlKey: !!(modifiers & ctrl), + metaKey: !!(modifiers & meta), + shiftKey: !!(modifiers & shift), + }; + EventUtils.synthesizeKey(eventUtilsKey, eventInfo, browserWindow); + } + + await this.executeInChild("_waitForContentEvent", eventId); + } + + /** + * Simulate mouse events. + * + * @param {object} options + * @param {string} options.type + * @param {number} options.x + * @param {number} options.y + * @param {number} options.modifiers + * @param {number} options.timestamp [Not Supported] + * @param {string} options.button + * @param {number} options.buttons [Not Supported] + * @param {string} options.clickCount + * @param {number} options.deltaX [Not Supported] + * @param {number} options.deltaY [Not Supported] + * @param {string} options.pointerType [Not Supported] + */ + async dispatchMouseEvent(options = {}) { + const { button, clickCount, modifiers, type, x, y } = options; + const { alt, ctrl, meta, shift } = Input.Modifier; + + let domType; + if (type === "mousePressed") { + domType = "mousedown"; + } else if (type === "mouseReleased") { + domType = "mouseup"; + } else if (type === "mouseMoved") { + domType = "mousemove"; + } else { + throw new Error(`Mouse type is not supported: ${type}`); + } + + if (domType === "mousedown" && button === "right") { + domType = "contextmenu"; + } + + const buttonID = Input.Button[button] || Input.Button.left; + const { browser } = this.session.target; + const currentWindow = browser.ownerGlobal; + + const EventUtils = this._getEventUtils(currentWindow); + const eventId = await this.executeInChild( + "_addContentEventListener", + domType + ); + + EventUtils.synthesizeMouse(browser, x, y, { + type: domType, + button: buttonID, + clickCount: clickCount || 1, + altKey: !!(modifiers & alt), + ctrlKey: !!(modifiers & ctrl), + metaKey: !!(modifiers & meta), + shiftKey: !!(modifiers & shift), + }); + + await this.executeInChild("_waitForContentEvent", eventId); + } + + /** + * Memoized EventUtils getter. + */ + _getEventUtils(win) { + if (!this._eventUtils) { + this._eventUtils = { + window: win, + parent: win, + _EU_Ci: Ci, + _EU_Cc: Cc, + }; + Services.scriptloader.loadSubScript( + "chrome://remote/content/external/EventUtils.js", + this._eventUtils + ); + } + return this._eventUtils; + } +} + +Input.Button = { + left: 0, + middle: 1, + right: 2, + back: 3, + forward: 4, +}; + +Input.Modifier = { + alt: 1, + ctrl: 2, + meta: 4, + shift: 8, +}; diff --git a/remote/cdp/domains/parent/Network.sys.mjs b/remote/cdp/domains/parent/Network.sys.mjs new file mode 100644 index 0000000000..4d36cf994e --- /dev/null +++ b/remote/cdp/domains/parent/Network.sys.mjs @@ -0,0 +1,538 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +const MAX_COOKIE_EXPIRY = Number.MAX_SAFE_INTEGER; + +const LOAD_CAUSE_STRINGS = { + [Ci.nsIContentPolicy.TYPE_INVALID]: "Invalid", + [Ci.nsIContentPolicy.TYPE_OTHER]: "Other", + [Ci.nsIContentPolicy.TYPE_SCRIPT]: "Script", + [Ci.nsIContentPolicy.TYPE_IMAGE]: "Img", + [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "Stylesheet", + [Ci.nsIContentPolicy.TYPE_OBJECT]: "Object", + [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "Document", + [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "Subdocument", + [Ci.nsIContentPolicy.TYPE_PING]: "Ping", + [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "Xhr", + [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "ObjectSubdoc", + [Ci.nsIContentPolicy.TYPE_DTD]: "Dtd", + [Ci.nsIContentPolicy.TYPE_FONT]: "Font", + [Ci.nsIContentPolicy.TYPE_MEDIA]: "Media", + [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "Websocket", + [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "Csp", + [Ci.nsIContentPolicy.TYPE_XSLT]: "Xslt", + [Ci.nsIContentPolicy.TYPE_BEACON]: "Beacon", + [Ci.nsIContentPolicy.TYPE_FETCH]: "Fetch", + [Ci.nsIContentPolicy.TYPE_IMAGESET]: "Imageset", + [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "WebManifest", + [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "Webidentity", +}; + +export class Network extends Domain { + constructor(session) { + super(session); + this.enabled = false; + + this._onRequest = this._onRequest.bind(this); + this._onResponse = this._onResponse.bind(this); + } + + destructor() { + this.disable(); + + super.destructor(); + } + + enable() { + if (this.enabled) { + return; + } + this.enabled = true; + this.session.networkObserver.startTrackingBrowserNetwork( + this.session.target.browser + ); + this.session.networkObserver.on("request", this._onRequest); + this.session.networkObserver.on("response", this._onResponse); + } + + disable() { + if (!this.enabled) { + return; + } + this.session.networkObserver.stopTrackingBrowserNetwork( + this.session.target.browser + ); + this.session.networkObserver.off("request", this._onRequest); + this.session.networkObserver.off("response", this._onResponse); + this.enabled = false; + } + + /** + * Deletes browser cookies with matching name and url or domain/path pair. + * + * @param {object} options + * @param {string} options.name + * Name of the cookies to remove. + * @param {string=} options.url + * If specified, deletes all the cookies with the given name + * where domain and path match provided URL. + * @param {string=} options.domain + * If specified, deletes only cookies with the exact domain. + * @param {string=} options.path + * If specified, deletes only cookies with the exact path. + */ + async deleteCookies(options = {}) { + const { domain, name, path = "/", url } = options; + + if (typeof name != "string") { + throw new TypeError("name: string value expected"); + } + + if (!url && !domain) { + throw new TypeError( + "At least one of the url and domain needs to be specified" + ); + } + + // Retrieve host. Check domain first because it has precedence. + let hostname = domain || ""; + if (!hostname.length) { + const cookieURL = new URL(url); + if (!["http:", "https:"].includes(cookieURL.protocol)) { + throw new TypeError("An http or https url must be specified"); + } + hostname = cookieURL.hostname; + } + + const cookiesFound = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify({}), + hostname + ); + + for (const cookie of cookiesFound) { + if (cookie.name == name && cookie.path.startsWith(path)) { + Services.cookies.remove( + cookie.host, + cookie.name, + cookie.path, + cookie.originAttributes + ); + } + } + } + + /** + * Activates emulation of network conditions. + * + * @param {object} options + * @param {boolean} options.offline + * True to emulate internet disconnection. + */ + emulateNetworkConditions(options = {}) { + const { offline } = options; + + if (typeof offline != "boolean") { + throw new TypeError("offline: boolean value expected"); + } + + Services.io.offline = offline; + } + + /** + * Returns all browser cookies. + * + * Depending on the backend support, will return detailed cookie information in the cookies field. + * + * @param {object} options + * + * @returns {Array<Cookie>} + * Array of cookie objects. + */ + async getAllCookies(options = {}) { + const cookies = []; + for (const cookie of Services.cookies.cookies) { + cookies.push(_buildCookie(cookie)); + } + + return { cookies }; + } + + /** + * Returns all browser cookies for the current URL. + * + * @param {object} options + * @param {Array<string>=} options.urls + * The list of URLs for which applicable cookies will be fetched. + * Defaults to the currently open URL. + * + * @returns {Array<Cookie>} + * Array of cookie objects. + */ + async getCookies(options = {}) { + const { urls = this._getDefaultUrls() } = options; + + if (!Array.isArray(urls)) { + throw new TypeError("urls: array expected"); + } + + for (const [index, url] of urls.entries()) { + if (typeof url !== "string") { + throw new TypeError(`urls: string value expected at index ${index}`); + } + } + + const cookies = []; + for (let url of urls) { + url = new URL(url); + + const secureProtocol = ["https:", "wss:"].includes(url.protocol); + + const cookiesFound = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify({}), + url.hostname + ); + + for (const cookie of cookiesFound) { + // Ignore secure cookies for non-secure protocols + if (cookie.isSecure && !secureProtocol) { + continue; + } + + // Ignore cookies which do not match the given path + if (!url.pathname.startsWith(cookie.path)) { + continue; + } + + const builtCookie = _buildCookie(cookie); + const duplicateCookie = cookies.some(value => { + return ( + value.name === builtCookie.name && + value.path === builtCookie.path && + value.domain === builtCookie.domain + ); + }); + + if (duplicateCookie) { + continue; + } + + cookies.push(builtCookie); + } + } + + return { cookies }; + } + + /** + * Sets a cookie with the given cookie data. + * + * Note that it may overwrite equivalent cookies if they exist. + * + * @param {object} cookie + * @param {string} cookie.name + * Cookie name. + * @param {string} cookie.value + * Cookie value. + * @param {string=} cookie.domain + * Cookie domain. + * @param {number=} cookie.expires + * Cookie expiration date, session cookie if not set. + * @param {boolean=} cookie.httpOnly + * True if cookie is http-only. + * @param {string=} cookie.path + * Cookie path. + * @param {string=} cookie.sameSite + * Cookie SameSite type. + * @param {boolean=} cookie.secure + * True if cookie is secure. + * @param {string=} cookie.url + * The request-URI to associate with the setting of the cookie. + * This value can affect the default domain and path values of the + * created cookie. + * + * @returns {boolean} + * True if successfully set cookie. + */ + setCookie(cookie) { + if (typeof cookie.name != "string") { + throw new TypeError("name: string value expected"); + } + + if (typeof cookie.value != "string") { + throw new TypeError("value: string value expected"); + } + + if ( + typeof cookie.url == "undefined" && + typeof cookie.domain == "undefined" + ) { + throw new TypeError( + "At least one of the url and domain needs to be specified" + ); + } + + // Retrieve host. Check domain first because it has precedence. + let hostname = cookie.domain || ""; + let cookieURL; + let schemeType = Ci.nsICookie.SCHEME_UNSET; + if (!hostname.length) { + try { + cookieURL = new URL(cookie.url); + } catch (e) { + return { success: false }; + } + + if (!["http:", "https:"].includes(cookieURL.protocol)) { + throw new TypeError(`Invalid protocol ${cookieURL.protocol}`); + } + + if (cookieURL.protocol == "https:") { + cookie.secure = true; + schemeType = Ci.nsICookie.SCHEME_HTTPS; + } else { + schemeType = Ci.nsICookie.SCHEME_HTTP; + } + + hostname = cookieURL.hostname; + } + + if (typeof cookie.path == "undefined") { + cookie.path = "/"; + } + + let isSession = false; + if (typeof cookie.expires == "undefined") { + isSession = true; + cookie.expires = MAX_COOKIE_EXPIRY; + } + + const sameSiteMap = new Map([ + ["None", Ci.nsICookie.SAMESITE_NONE], + ["Lax", Ci.nsICookie.SAMESITE_LAX], + ["Strict", Ci.nsICookie.SAMESITE_STRICT], + ]); + + let success = true; + try { + Services.cookies.add( + hostname, + cookie.path, + cookie.name, + cookie.value, + cookie.secure, + cookie.httpOnly || false, + isSession, + cookie.expires, + {} /* originAttributes */, + sameSiteMap.get(cookie.sameSite), + schemeType + ); + } catch (e) { + success = false; + } + + return { success }; + } + + /** + * Sets given cookies. + * + * @param {object} options + * @param {Array.<Cookie>} options.cookies + * Cookies to be set. + */ + setCookies(options = {}) { + const { cookies } = options; + + if (!Array.isArray(cookies)) { + throw new TypeError("Invalid parameters (cookies: array expected)"); + } + + cookies.forEach(cookie => { + const { success } = this.setCookie(cookie); + if (!success) { + throw new Error("Invalid cookie fields"); + } + }); + } + + /** + * Toggles ignoring cache for each request. If true, cache will not be used. + * + * @param {object} options + * @param {boolean} options.cacheDisabled + * Cache disabled state. + */ + async setCacheDisabled(options = {}) { + const { cacheDisabled = false } = options; + + const { INHIBIT_CACHING, LOAD_BYPASS_CACHE, LOAD_NORMAL } = Ci.nsIRequest; + + let loadFlags = LOAD_NORMAL; + if (cacheDisabled) { + loadFlags = LOAD_BYPASS_CACHE | INHIBIT_CACHING; + } + + await this.executeInChild("_updateLoadFlags", loadFlags); + } + + /** + * Allows overriding user agent with the given string. + * + * Redirected to Emulation.setUserAgentOverride. + */ + setUserAgentOverride(options = {}) { + const { id } = this.session; + this.session.execute(id, "Emulation", "setUserAgentOverride", options); + } + + _onRequest(eventName, httpChannel, data) { + const wrappedChannel = ChannelWrapper.get(httpChannel); + const urlFragment = httpChannel.URI.hasRef + ? "#" + httpChannel.URI.ref + : undefined; + + const request = { + url: httpChannel.URI.specIgnoringRef, + urlFragment, + method: httpChannel.requestMethod, + headers: headersAsObject(data.headers), + postData: undefined, + hasPostData: false, + mixedContentType: undefined, + initialPriority: undefined, + referrerPolicy: undefined, + isLinkPreload: false, + }; + this.emit("Network.requestWillBeSent", { + requestId: data.requestId, + loaderId: data.loaderId, + documentURL: + wrappedChannel.documentURL || httpChannel.URI.specIgnoringRef, + request, + timestamp: Date.now() / 1000, + wallTime: undefined, + initiator: undefined, + redirectResponse: undefined, + type: LOAD_CAUSE_STRINGS[data.cause] || "unknown", + frameId: data.frameId.toString(), + hasUserGesture: undefined, + }); + } + + _onResponse(eventName, httpChannel, data) { + const wrappedChannel = ChannelWrapper.get(httpChannel); + const headers = headersAsObject(data.headers); + + this.emit("Network.responseReceived", { + requestId: data.requestId, + loaderId: data.loaderId, + timestamp: Date.now() / 1000, + type: LOAD_CAUSE_STRINGS[data.cause] || "unknown", + response: { + url: httpChannel.URI.spec, + status: data.status, + statusText: data.statusText, + headers, + mimeType: wrappedChannel.contentType, + requestHeaders: headersAsObject(data.requestHeaders), + connectionReused: undefined, + connectionId: undefined, + remoteIPAddress: data.remoteIPAddress, + remotePort: data.remotePort, + fromDiskCache: data.fromCache, + encodedDataLength: undefined, + protocol: httpChannel.protocolVersion, + securityDetails: data.securityDetails, + // unknown, neutral, insecure, secure, info, insecure-broken + securityState: "unknown", + }, + frameId: data.frameId.toString(), + }); + } + + /** + * Creates an array of all Urls in the page context + * + * @returns {Array<string>=} + */ + _getDefaultUrls() { + const urls = this.session.target.browsingContext + .getAllBrowsingContextsInSubtree() + .map(context => context.currentURI.spec); + + return urls; + } +} + +/** + * Creates a CDP Network.Cookie from our internal cookie values + * + * @param {nsICookie} cookie + * + * @returns {Network.Cookie} + * A CDP Cookie + */ +function _buildCookie(cookie) { + const data = { + name: cookie.name, + value: cookie.value, + domain: cookie.host, + path: cookie.path, + expires: cookie.isSession ? -1 : cookie.expiry, + // The size is the combined length of both the cookie name and value + size: cookie.name.length + cookie.value.length, + httpOnly: cookie.isHttpOnly, + secure: cookie.isSecure, + session: cookie.isSession, + }; + + if (cookie.sameSite) { + const sameSiteMap = new Map([ + [Ci.nsICookie.SAMESITE_LAX, "Lax"], + [Ci.nsICookie.SAMESITE_STRICT, "Strict"], + ]); + + data.sameSite = sameSiteMap.get(cookie.sameSite); + } + + return data; +} + +/** + * Given a array of possibly repeating header names, merge the values for + * duplicate headers into a comma-separated list, or in some cases a + * newline-separated list. + * + * e.g. { "Cache-Control": "no-cache,no-store" } + * + * Based on + * https://hg.mozilla.org/mozilla-central/file/56c09d42f411246e407fe30418c27e67a6a44d29/netwerk/protocol/http/nsHttpHeaderArray.h + * + * @param {Array} headers + * Array of {name, value} + * @returns {object} + * Object where each key is a header name. + */ +function headersAsObject(headers) { + const rv = {}; + headers.forEach(({ name, value }) => { + name = name.toLowerCase(); + if (rv[name]) { + const separator = [ + "set-cookie", + "www-authenticate", + "proxy-authenticate", + ].includes(name) + ? "\n" + : ","; + rv[name] += `${separator}${value}`; + } else { + rv[name] = value; + } + }); + return rv; +} diff --git a/remote/cdp/domains/parent/Page.sys.mjs b/remote/cdp/domains/parent/Page.sys.mjs new file mode 100644 index 0000000000..d050277aff --- /dev/null +++ b/remote/cdp/domains/parent/Page.sys.mjs @@ -0,0 +1,756 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + + DialogHandler: + "chrome://remote/content/cdp/domains/parent/page/DialogHandler.sys.mjs", + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + PollPromise: "chrome://remote/content/shared/Sync.sys.mjs", + print: "chrome://remote/content/shared/PDF.sys.mjs", + streamRegistry: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs", + Stream: "chrome://remote/content/cdp/StreamRegistry.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs", + windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", +}); + +const MAX_CANVAS_DIMENSION = 32767; +const MAX_CANVAS_AREA = 472907776; + +const PRINT_MAX_SCALE_VALUE = 2.0; +const PRINT_MIN_SCALE_VALUE = 0.1; + +const PDF_TRANSFER_MODES = { + base64: "ReturnAsBase64", + stream: "ReturnAsStream", +}; + +const TIMEOUT_SET_HISTORY_INDEX = 1000; + +export class Page extends Domain { + constructor(session) { + super(session); + + this._onDialogLoaded = this._onDialogLoaded.bind(this); + this._onRequest = this._onRequest.bind(this); + + this.enabled = false; + + this.session.networkObserver.startTrackingBrowserNetwork( + this.session.target.browser + ); + this.session.networkObserver.on("request", this._onRequest); + } + + destructor() { + // Flip a flag to avoid to disable the content domain from this.disable() + this._isDestroyed = false; + this.disable(); + + this.session.networkObserver.off("request", this._onRequest); + this.session.networkObserver.stopTrackingBrowserNetwork( + this.session.target.browser + ); + super.destructor(); + } + + // commands + + /** + * Navigates current page to given URL. + * + * @param {object} options + * @param {string} options.url + * destination URL + * @param {string=} options.frameId + * frame id to navigate (not supported), + * if not specified navigate top frame + * @param {string=} options.referrer + * referred URL (optional) + * @param {string=} options.transitionType + * intended transition type + * @returns {object} + * - frameId {string} frame id that has navigated (or failed to) + * - errorText {string=} error message if navigation has failed + * - loaderId {string} (not supported) + */ + async navigate(options = {}) { + const { url, frameId, referrer, transitionType } = options; + if (typeof url != "string") { + throw new TypeError("url: string value expected"); + } + let validURL; + try { + validURL = Services.io.newURI(url); + } catch (e) { + throw new Error("Error: Cannot navigate to invalid URL"); + } + const topFrameId = this.session.browsingContext.id.toString(); + if (frameId && frameId != topFrameId) { + throw new lazy.UnsupportedError("frameId not supported"); + } + + const hitsNetwork = ["https", "http"].includes(validURL.scheme); + let networkLessLoaderId; + if (!hitsNetwork) { + // This navigation will not hit the network, use a randomly generated id. + networkLessLoaderId = lazy.generateUUID(); + + // Update the content process map of loader ids. + await this.executeInChild("_updateLoaderId", { + frameId: this.session.browsingContext.id, + loaderId: networkLessLoaderId, + }); + } + + const currentURI = this.session.browsingContext.currentURI; + + const isSameDocumentNavigation = + // The "host", "query" and "ref" getters can throw if the URLs are not + // http/https, so verify first that both currentURI and validURL are + // using http/https. + hitsNetwork && + ["https", "http"].includes(currentURI.scheme) && + currentURI.host === validURL.host && + currentURI.query === validURL.query && + !!validURL.ref; + + const requestDone = new Promise(resolve => { + if (isSameDocumentNavigation) { + // Per CDP documentation, same-document navigations should not emit any + // loader id (https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate) + resolve({}); + return; + } + + if (!hitsNetwork) { + // This navigation will not hit the network, use a randomly generated id. + resolve({ navigationRequestId: networkLessLoaderId }); + return; + } + let navigationRequestId, redirectedRequestId; + const _onNavigationRequest = function (_type, _ch, data) { + const { + url: requestURL, + requestId, + redirectedFrom = null, + isNavigationRequest, + } = data; + if (!isNavigationRequest) { + return; + } + if (validURL.spec === requestURL) { + navigationRequestId = redirectedRequestId = requestId; + } else if (redirectedFrom === redirectedRequestId) { + redirectedRequestId = requestId; + } + }; + + const _onRequestFinished = function (_type, _ch, data) { + const { requestId, errorCode } = data; + if ( + redirectedRequestId !== requestId || + errorCode == "NS_BINDING_REDIRECTED" + ) { + // handle next request in redirection chain + return; + } + this.session.networkObserver.off("request", _onNavigationRequest); + this.session.networkObserver.off("requestfinished", _onRequestFinished); + resolve({ errorCode, navigationRequestId }); + }.bind(this); + + this.session.networkObserver.on("request", _onNavigationRequest); + this.session.networkObserver.on("requestfinished", _onRequestFinished); + }); + + const opts = { + loadFlags: transitionToLoadFlag(transitionType), + referrerURI: referrer, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + this.session.browsingContext.loadURI(validURL, opts); + // clients expect loaderId == requestId for a document navigation request + const { navigationRequestId: loaderId, errorCode } = await requestDone; + const result = { + frameId: topFrameId, + loaderId, + }; + if (errorCode) { + result.errorText = errorCode; + } + return result; + } + + /** + * Capture page screenshot. + * + * @param {object} options + * @param {Viewport=} options.clip + * Capture the screenshot of a given region only. + * @param {string=} options.format + * Image compression format. Defaults to "png". + * @param {number=} options.quality + * Compression quality from range [0..100] (jpeg only). Defaults to 80. + * + * @returns {string} + * Base64-encoded image data. + */ + async captureScreenshot(options = {}) { + const { clip, format = "png", quality = 80 } = options; + + if (options.fromSurface) { + throw new lazy.UnsupportedError("fromSurface not supported"); + } + + let rect; + let scale = await this.executeInChild("_devicePixelRatio"); + + if (clip) { + for (const prop of ["x", "y", "width", "height", "scale"]) { + if (clip[prop] == undefined) { + throw new TypeError(`clip.${prop}: double value expected`); + } + } + + const contentRect = await this.executeInChild("_contentRect"); + + // For invalid scale values default to full page + if (clip.scale <= 0) { + Object.assign(clip, { + x: 0, + y: 0, + width: contentRect.width, + height: contentRect.height, + scale: 1, + }); + } else { + if (clip.x < 0 || clip.x > contentRect.width - 1) { + clip.x = 0; + } + if (clip.y < 0 || clip.y > contentRect.height - 1) { + clip.y = 0; + } + if (clip.width <= 0) { + clip.width = contentRect.width; + } + if (clip.height <= 0) { + clip.height = contentRect.height; + } + } + + rect = new DOMRect(clip.x, clip.y, clip.width, clip.height); + scale *= clip.scale; + } else { + // If no specific clipping region has been specified, + // fallback to the layout (fixed) viewport, and the + // default pixel ratio. + const { pageX, pageY, clientWidth, clientHeight } = + await this.executeInChild("_layoutViewport"); + + rect = new DOMRect(pageX, pageY, clientWidth, clientHeight); + } + + let canvasWidth = rect.width * scale; + let canvasHeight = rect.height * scale; + + // Cap the screenshot size based on maximum allowed canvas sizes. + // Using higher dimensions would trigger exceptions in Gecko. + // + // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size + if (canvasWidth > MAX_CANVAS_DIMENSION) { + rect.width = Math.floor(MAX_CANVAS_DIMENSION / scale); + canvasWidth = rect.width * scale; + } + if (canvasHeight > MAX_CANVAS_DIMENSION) { + rect.height = Math.floor(MAX_CANVAS_DIMENSION / scale); + canvasHeight = rect.height * scale; + } + // If the area is larger, reduce the height to keep the full width. + if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) { + rect.height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale)); + canvasHeight = rect.height * scale; + } + + const { browsingContext, window } = this.session.target; + const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( + rect, + scale, + "rgb(255,255,255)" + ); + + const canvas = window.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(snapshot, 0, 0); + + // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies + // of the bitmap will exist in memory. Force the removal of the snapshot + // because it is no longer needed. + snapshot.close(); + + const url = canvas.toDataURL(`image/${format}`, quality / 100); + if (!url.startsWith(`data:image/${format}`)) { + throw new lazy.UnsupportedError(`Unsupported MIME type: image/${format}`); + } + + // only return the base64 encoded data without the data URL prefix + const data = url.substring(url.indexOf(",") + 1); + + return { data }; + } + + async enable() { + if (this.enabled) { + return; + } + + this.enabled = true; + + const { browser } = this.session.target; + this._dialogHandler = new lazy.DialogHandler(browser); + this._dialogHandler.on("dialog-loaded", this._onDialogLoaded); + await this.executeInChild("enable"); + } + + async disable() { + if (!this.enabled) { + return; + } + + this._dialogHandler.destructor(); + this._dialogHandler = null; + this.enabled = false; + + if (!this._isDestroyed) { + // Only call disable in the content domain if we are not destroying the domain. + // If we are destroying the domain, the content domains will be destroyed + // independently after firing the remote:destroy event. + await this.executeInChild("disable"); + } + } + + async bringToFront() { + const { tab, window } = this.session.target; + + // Focus the window, and select the corresponding tab + await lazy.windowManager.focusWindow(window); + await lazy.TabManager.selectTab(tab); + } + + /** + * Return metrics relating to the layouting of the page. + * + * The returned object contains the following entries: + * + * layoutViewport: + * {number} pageX + * Horizontal offset relative to the document (CSS pixels) + * {number} pageY + * Vertical offset relative to the document (CSS pixels) + * {number} clientWidth + * Width (CSS pixels), excludes scrollbar if present + * {number} clientHeight + * Height (CSS pixels), excludes scrollbar if present + * + * visualViewport: + * {number} offsetX + * Horizontal offset relative to the layout viewport (CSS pixels) + * {number} offsetY + * Vertical offset relative to the layout viewport (CSS pixels) + * {number} pageX + * Horizontal offset relative to the document (CSS pixels) + * {number} pageY + * Vertical offset relative to the document (CSS pixels) + * {number} clientWidth + * Width (CSS pixels), excludes scrollbar if present + * {number} clientHeight + * Height (CSS pixels), excludes scrollbar if present + * {number} scale + * Scale relative to the ideal viewport (size at width=device-width) + * {number} zoom + * Page zoom factor (CSS to device independent pixels ratio) + * + * contentSize: + * {number} x + * X coordinate + * {number} y + * Y coordinate + * {number} width + * Width of scrollable area + * {number} height + * Height of scrollable area + * + * @returns {Promise<object>} + * Promise which resolves with an object with the following properties + * layoutViewport and contentSize + */ + async getLayoutMetrics() { + return { + layoutViewport: await this.executeInChild("_layoutViewport"), + contentSize: await this.executeInChild("_contentRect"), + }; + } + + /** + * Returns navigation history for the current page. + * + * @returns {Promise<object>} + * Promise which resolves with an object with the following properties + * currentIndex (number) and entries (Array<NavigationEntry>). + */ + async getNavigationHistory() { + const { window } = this.session.target; + + return new Promise(resolve => { + function updateSessionHistory(sessionHistory) { + const entries = sessionHistory.entries.map(entry => { + return { + id: entry.ID, + url: entry.url, + userTypedURL: entry.originalURI || entry.url, + title: entry.title, + // TODO: Bug 1609514 + transitionType: null, + }; + }); + + resolve({ + currentIndex: sessionHistory.index, + entries, + }); + } + + lazy.SessionStore.getSessionHistory( + window.gBrowser.selectedTab, + updateSessionHistory + ); + }); + } + + /** + * Interact with the currently opened JavaScript dialog (alert, confirm, + * prompt) for this page. This will always close the dialog, either accepting + * or rejecting it, with the optional prompt filled. + * + * @param {object} options + * @param {boolean=} options.accept + * for "confirm", "prompt", "beforeunload" dialogs true will accept + * the dialog, false will cancel it. For "alert" dialogs, true or + * false closes the dialog in the same way. + * @param {string=} options.promptText + * for "prompt" dialogs, used to fill the prompt input. + */ + async handleJavaScriptDialog(options = {}) { + const { accept, promptText } = options; + + if (!this.enabled) { + throw new Error("Page domain is not enabled"); + } + await this._dialogHandler.handleJavaScriptDialog({ accept, promptText }); + } + + /** + * Navigates current page to the given history entry. + * + * @param {object} options + * @param {number} options.entryId + * Unique id of the entry to navigate to. + */ + async navigateToHistoryEntry(options = {}) { + const { entryId } = options; + + const index = await this._getIndexForHistoryEntryId(entryId); + + if (index == null) { + throw new Error("No entry with passed id"); + } + + const { window } = this.session.target; + window.gBrowser.gotoIndex(index); + + // On some platforms the requested index isn't set immediately. + await lazy.PollPromise( + async (resolve, reject) => { + const currentIndex = await this._getCurrentHistoryIndex(); + if (currentIndex == index) { + resolve(); + } else { + reject(); + } + }, + { timeout: TIMEOUT_SET_HISTORY_INDEX } + ); + } + + /** + * Print page as PDF. + * + * @param {object} options + * @param {boolean=} options.displayHeaderFooter + * Display header and footer. Defaults to false. + * @param {string=} options.footerTemplate (not supported) + * HTML template for the print footer. + * @param {string=} options.headerTemplate (not supported) + * HTML template for the print header. Should use the same format + * as the footerTemplate. + * @param {boolean=} options.ignoreInvalidPageRanges + * Whether to silently ignore invalid but successfully parsed page ranges, + * such as '3-2'. Defaults to false. + * @param {boolean=} options.landscape + * Paper orientation. Defaults to false. + * @param {number=} options.marginBottom + * Bottom margin in inches. Defaults to 1cm (~0.4 inches). + * @param {number=} options.marginLeft + * Left margin in inches. Defaults to 1cm (~0.4 inches). + * @param {number=} options.marginRight + * Right margin in inches. Defaults to 1cm (~0.4 inches). + * @param {number=} options.marginTop + * Top margin in inches. Defaults to 1cm (~0.4 inches). + * @param {string=} options.pageRanges (not supported) + * Paper ranges to print, e.g., '1-5, 8, 11-13'. + * Defaults to the empty string, which means print all pages. + * @param {number=} options.paperHeight + * Paper height in inches. Defaults to 11 inches. + * @param {number=} options.paperWidth + * Paper width in inches. Defaults to 8.5 inches. + * @param {boolean=} options.preferCSSPageSize + * Whether or not to prefer page size as defined by CSS. + * Defaults to false, in which case the content will be scaled + * to fit the paper size. + * @param {boolean=} options.printBackground + * Print background graphics. Defaults to false. + * @param {number=} options.scale + * Scale of the webpage rendering. Defaults to 1. + * @param {string=} options.transferMode + * Return as base64-encoded string (ReturnAsBase64), + * or stream (ReturnAsStream). Defaults to ReturnAsBase64. + * + * @returns {Promise<{data:string, stream:Stream}>} + * Based on the transferMode setting data is a base64-encoded string, + * or stream is a Stream. + */ + async printToPDF(options = {}) { + const { + displayHeaderFooter = false, + // Bug 1601570 - Implement templates for header and footer + // headerTemplate = "", + // footerTemplate = "", + landscape = false, + marginBottom = 0.39, + marginLeft = 0.39, + marginRight = 0.39, + marginTop = 0.39, + // Bug 1601571 - Implement handling of page ranges + // TODO: pageRanges = "", + // TODO: ignoreInvalidPageRanges = false, + paperHeight = 11.0, + paperWidth = 8.5, + preferCSSPageSize = false, + printBackground = false, + scale = 1.0, + transferMode = PDF_TRANSFER_MODES.base64, + } = options; + + if (marginBottom < 0) { + throw new TypeError("marginBottom is negative"); + } + if (marginLeft < 0) { + throw new TypeError("marginLeft is negative"); + } + if (marginRight < 0) { + throw new TypeError("marginRight is negative"); + } + if (marginTop < 0) { + throw new TypeError("marginTop is negative"); + } + if (scale < PRINT_MIN_SCALE_VALUE || scale > PRINT_MAX_SCALE_VALUE) { + throw new TypeError("scale is outside [0.1 - 2] range"); + } + if (paperHeight <= 0) { + throw new TypeError("paperHeight is zero or negative"); + } + if (paperWidth <= 0) { + throw new TypeError("paperWidth is zero or negative"); + } + + const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + + const printSettings = psService.createNewPrintSettings(); + printSettings.isInitializedFromPrinter = true; + printSettings.isInitializedFromPrefs = true; + printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + printSettings.printerName = ""; + printSettings.printSilent = true; + + printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches; + printSettings.paperWidth = paperWidth; + printSettings.paperHeight = paperHeight; + + // Override any os-specific unwriteable margins + printSettings.unwriteableMarginTop = 0; + printSettings.unwriteableMarginLeft = 0; + printSettings.unwriteableMarginBottom = 0; + printSettings.unwriteableMarginRight = 0; + + printSettings.marginBottom = marginBottom; + printSettings.marginLeft = marginLeft; + printSettings.marginRight = marginRight; + printSettings.marginTop = marginTop; + + printSettings.printBGColors = printBackground; + printSettings.printBGImages = printBackground; + printSettings.scaling = scale; + printSettings.shrinkToFit = preferCSSPageSize; + + if (!displayHeaderFooter) { + printSettings.headerStrCenter = ""; + printSettings.headerStrLeft = ""; + printSettings.headerStrRight = ""; + printSettings.footerStrCenter = ""; + printSettings.footerStrLeft = ""; + printSettings.footerStrRight = ""; + } + + if (landscape) { + printSettings.orientation = Ci.nsIPrintSettings.kLandscapeOrientation; + } + + const retval = { data: null, stream: null }; + const { linkedBrowser } = this.session.target.tab; + + if (transferMode === PDF_TRANSFER_MODES.stream) { + // If we are returning a stream, we write the PDF to disk so that we don't + // keep (potentially very large) PDFs in memory. We can then stream them + // to the client via the returned Stream. + // + // NOTE: This is a potentially premature optimization -- it might be fine + // to keep these PDFs in memory, but we don't have specifics on how CDP is + // used in the field so it is possible that leaving the PDFs in memory + // could cause a regression. + const path = await IOUtils.createUniqueFile( + PathUtils.tempDir, + "remote-agent.pdf" + ); + + printSettings.outputDestination = + Ci.nsIPrintSettings.kOutputDestinationFile; + printSettings.toFileName = path; + + await linkedBrowser.browsingContext.print(printSettings); + + retval.stream = lazy.streamRegistry.add(new lazy.Stream(path)); + } else { + const binaryString = await lazy.print.printToBinaryString( + linkedBrowser.browsingContext, + printSettings + ); + + retval.data = btoa(binaryString); + } + + return retval; + } + + /** + * Intercept file chooser requests and transfer control to protocol clients. + * + * When file chooser interception is enabled, + * the native file chooser dialog is not shown. + * Instead, a protocol event Page.fileChooserOpened is emitted. + * + * @param {object} options + * @param {boolean=} options.enabled + * Enabled state of file chooser interception. + */ + setInterceptFileChooserDialog(options = {}) {} + + _getCurrentHistoryIndex() { + const { window } = this.session.target; + + return new Promise(resolve => { + lazy.SessionStore.getSessionHistory( + window.gBrowser.selectedTab, + history => { + resolve(history.index); + } + ); + }); + } + + _getIndexForHistoryEntryId(id) { + const { window } = this.session.target; + + return new Promise(resolve => { + function updateSessionHistory(sessionHistory) { + sessionHistory.entries.forEach((entry, index) => { + if (entry.ID == id) { + resolve(index); + } + }); + + resolve(null); + } + + lazy.SessionStore.getSessionHistory( + window.gBrowser.selectedTab, + updateSessionHistory + ); + }); + } + + /** + * Emit the proper CDP event javascriptDialogOpening when a javascript dialog + * opens for the current target. + */ + _onDialogLoaded(e, data) { + const { message, type } = data; + // XXX: We rely on the common-dialog-loaded event (see DialogHandler.jsm) + // which is inconsistent with the name "javascriptDialogOpening". + // For correctness we should rely on an event fired _before_ the prompt is + // visible, such as DOMWillOpenModalDialog. However the payload of this + // event does not contain enough data to populate javascriptDialogOpening. + // + // Since the event is fired asynchronously, this should not have an impact + // on the actual tests relying on this API. + this.emit("Page.javascriptDialogOpening", { message, type }); + } + + /** + * Handles HTTP request to propagate loaderId to events emitted from + * content process + */ + _onRequest(_type, _ch, data) { + if (!data.loaderId) { + return; + } + this.executeInChild("_updateLoaderId", { + loaderId: data.loaderId, + frameId: data.frameId, + }); + } +} + +function transitionToLoadFlag(transitionType) { + switch (transitionType) { + case "reload": + return Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH; + case "link": + default: + return Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK; + } +} diff --git a/remote/cdp/domains/parent/Security.sys.mjs b/remote/cdp/domains/parent/Security.sys.mjs new file mode 100644 index 0000000000..77e00acc8c --- /dev/null +++ b/remote/cdp/domains/parent/Security.sys.mjs @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetters(lazy, { + sss: ["@mozilla.org/ssservice;1", "nsISiteSecurityService"], + certOverrideService: [ + "@mozilla.org/security/certoverride;1", + "nsICertOverrideService", + ], +}); + +const CERT_PINNING_ENFORCEMENT_PREF = "security.cert_pinning.enforcement_level"; +const HSTS_PRELOAD_LIST_PREF = "network.stricttransportsecurity.preloadlist"; + +export class Security extends Domain { + destructor() { + this.setIgnoreCertificateErrors({ ignore: false }); + } + + /** + * Enable/disable whether all certificate errors should be ignored + * + * @param {object} options + * @param {boolean=} options.ignore + * if true, all certificate errors will be ignored. + */ + setIgnoreCertificateErrors(options = {}) { + const { ignore } = options; + + if (ignore) { + // make it possible to register certificate overrides for domains + // that use HSTS or HPKP + Services.prefs.setBoolPref(HSTS_PRELOAD_LIST_PREF, false); + Services.prefs.setIntPref(CERT_PINNING_ENFORCEMENT_PREF, 0); + } else { + Services.prefs.clearUserPref(HSTS_PRELOAD_LIST_PREF); + Services.prefs.clearUserPref(CERT_PINNING_ENFORCEMENT_PREF); + + // clear collected HSTS and HPKP state + lazy.sss.clearAll(); + } + + lazy.certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + ignore + ); + } +} diff --git a/remote/cdp/domains/parent/SystemInfo.sys.mjs b/remote/cdp/domains/parent/SystemInfo.sys.mjs new file mode 100644 index 0000000000..8b5e4a27c2 --- /dev/null +++ b/remote/cdp/domains/parent/SystemInfo.sys.mjs @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +export class SystemInfo extends Domain { + async getProcessInfo() { + const procInfo = await ChromeUtils.requestProcInfo(); + + // Add child processes + const processInfo = procInfo.children.map(proc => ({ + type: this.#getProcessType(proc.type), + id: proc.pid, + cpuTime: this.#getCpuTime(proc.cpuTime), + })); + + // Add parent process + processInfo.unshift({ + type: "browser", + id: procInfo.pid, + cpuTime: this.#getCpuTime(procInfo.cpuTime), + }); + + return processInfo; + } + + #getProcessType(type) { + // Map internal types to CDP types if applicable + switch (type) { + case "gpu": + return "GPU"; + + case "web": + case "webIsolated": + case "privilegedabout": + return "renderer"; + + default: + return type; + } + } + + #getCpuTime(cpuTime) { + // cpuTime is tracked internally as nanoseconds, CDP is in seconds + return cpuTime / 1000 / 1000 / 1000; + } +} diff --git a/remote/cdp/domains/parent/Target.sys.mjs b/remote/cdp/domains/parent/Target.sys.mjs new file mode 100644 index 0000000000..3f4588038b --- /dev/null +++ b/remote/cdp/domains/parent/Target.sys.mjs @@ -0,0 +1,290 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContextualIdentityService: + "resource://gre/modules/ContextualIdentityService.sys.mjs", + + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TabSession: "chrome://remote/content/cdp/sessions/TabSession.sys.mjs", + UserContextManager: + "chrome://remote/content/shared/UserContextManager.sys.mjs", + windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", +}); + +let browserContextIds = 1; + +// Default filter from CDP specification +const defaultFilter = [ + { type: "browser", exclude: true }, + { type: "tab", exclude: true }, + {}, +]; + +export class Target extends Domain { + #browserContextIds; + #discoverTargetFilter; + + constructor(session) { + super(session); + + this.#browserContextIds = new Set(); + + this._onTargetCreated = this._onTargetCreated.bind(this); + this._onTargetDestroyed = this._onTargetDestroyed.bind(this); + } + + getBrowserContexts() { + const browserContextIds = + lazy.ContextualIdentityService.getPublicUserContextIds().filter(id => + this.#browserContextIds.has(id) + ); + + return { browserContextIds }; + } + + createBrowserContext() { + const identity = lazy.ContextualIdentityService.create( + "remote-agent-" + browserContextIds++ + ); + + this.#browserContextIds.add(identity.userContextId); + return { browserContextId: identity.userContextId }; + } + + disposeBrowserContext(options = {}) { + const { browserContextId } = options; + + lazy.ContextualIdentityService.remove(browserContextId); + lazy.ContextualIdentityService.closeContainerTabs(browserContextId); + + this.#browserContextIds.delete(browserContextId); + } + + getTargets(options = {}) { + const { filter = defaultFilter } = options; + const { targetList } = this.session.target; + + this._validateTargetFilter(filter); + + const targetInfos = [...targetList] + .filter(target => this._filterIncludesTarget(target, filter)) + .map(target => this._getTargetInfo(target)); + + return { + targetInfos, + }; + } + + setDiscoverTargets(options = {}) { + const { discover, filter } = options; + const { targetList } = this.session.target; + + if (typeof discover !== "boolean") { + throw new TypeError("discover: boolean value expected"); + } + + if (discover === false && filter !== undefined) { + throw new Error("filter: should not be present when discover is false"); + } + + // null filter should not be defaulted + const targetFilter = filter === undefined ? defaultFilter : filter; + this._validateTargetFilter(targetFilter); + + // Store active filter for filtering in event listeners (targetCreated, targetDestroyed, targetInfoChanged) + this.#discoverTargetFilter = targetFilter; + + if (discover) { + targetList.on("target-created", this._onTargetCreated); + targetList.on("target-destroyed", this._onTargetDestroyed); + + for (const target of targetList) { + this._onTargetCreated("target-created", target); + } + } else { + targetList.off("target-created", this._onTargetCreated); + targetList.off("target-destroyed", this._onTargetDestroyed); + } + } + + async createTarget(options = {}) { + const { browserContextId, url } = options; + + if (typeof url !== "string") { + throw new TypeError("url: string value expected"); + } + + let validURL; + try { + validURL = Services.io.newURI(url); + } catch (e) { + // If we failed to parse given URL, use about:blank instead + validURL = Services.io.newURI("about:blank"); + } + + const { targetList, window } = this.session.target; + const onTarget = targetList.once("target-created"); + const tab = await lazy.TabManager.addTab({ + focus: true, + userContextId: + // Bug 1878649: Use UserContextManager ids consistently in CDP. + lazy.UserContextManager.getIdByInternalId(browserContextId), + window, + }); + + const target = await onTarget; + if (tab.linkedBrowser != target.browser) { + throw new Error( + "Unexpected tab opened: " + tab.linkedBrowser.currentURI.spec + ); + } + + target.browsingContext.loadURI(validURL, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + return { targetId: target.id }; + } + + async closeTarget(options = {}) { + const { targetId } = options; + const { targetList } = this.session.target; + const target = targetList.getById(targetId); + + if (!target) { + throw new Error(`Unable to find target with id '${targetId}'`); + } + + await lazy.TabManager.removeTab(target.tab); + } + + async activateTarget(options = {}) { + const { targetId } = options; + const { targetList, window } = this.session.target; + const target = targetList.getById(targetId); + + if (!target) { + throw new Error(`Unable to find target with id '${targetId}'`); + } + + // Focus the window, and select the corresponding tab + await lazy.windowManager.focusWindow(window); + await lazy.TabManager.selectTab(target.tab); + } + + attachToTarget(options = {}) { + const { targetId } = options; + const { targetList } = this.session.target; + const target = targetList.getById(targetId); + + if (!target) { + throw new Error(`Unable to find target with id '${targetId}'`); + } + + const tabSession = new lazy.TabSession( + this.session.connection, + target, + lazy.generateUUID() + ); + this.session.connection.registerSession(tabSession); + + this._emitAttachedToTarget(target, tabSession); + + return { + sessionId: tabSession.id, + }; + } + + setAutoAttach() {} + + sendMessageToTarget(options = {}) { + const { sessionId, message } = options; + const { connection } = this.session; + connection.sendMessageToTarget(sessionId, message); + } + + /** + * Internal methods: the following methods are not part of CDP; + * note the _ prefix. + */ + + _emitAttachedToTarget(target, tabSession) { + const targetInfo = this._getTargetInfo(target); + this.emit("Target.attachedToTarget", { + targetInfo, + sessionId: tabSession.id, + waitingForDebugger: false, + }); + } + + _getTargetInfo(target) { + const attached = [...this.session.connection.sessions.values()].some( + session => session.target.id === target.id + ); + + return { + targetId: target.id, + type: target.type, + title: target.title, + url: target.url, + attached, + browserContextId: target.browserContextId, + }; + } + + _filterIncludesTarget(target, filter) { + for (const entry of filter) { + if ([undefined, target.type].includes(entry.type)) { + return !entry.exclude; + } + } + + return false; + } + + _validateTargetFilter(filter) { + if (!Array.isArray(filter)) { + throw new TypeError("filter: array value expected"); + } + + for (const entry of filter) { + if (entry === null || Array.isArray(entry) || typeof entry !== "object") { + throw new TypeError("filter: object values expected in array"); + } + + if (!["undefined", "string"].includes(typeof entry.type)) { + throw new TypeError("filter: type: string value expected"); + } + + if (!["undefined", "boolean"].includes(typeof entry.exclude)) { + throw new TypeError("filter: exclude: boolean value expected"); + } + } + } + + _onTargetCreated(eventName, target) { + if (!this._filterIncludesTarget(target, this.#discoverTargetFilter)) { + return; + } + + const targetInfo = this._getTargetInfo(target); + this.emit("Target.targetCreated", { targetInfo }); + } + + _onTargetDestroyed(eventName, target) { + if (!this._filterIncludesTarget(target, this.#discoverTargetFilter)) { + return; + } + + this.emit("Target.targetDestroyed", { + targetId: target.id, + }); + } +} diff --git a/remote/cdp/domains/parent/page/DialogHandler.sys.mjs b/remote/cdp/domains/parent/page/DialogHandler.sys.mjs new file mode 100644 index 0000000000..c5c70cb17f --- /dev/null +++ b/remote/cdp/domains/parent/page/DialogHandler.sys.mjs @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", +}); + +const DIALOG_TYPES = { + ALERT: "alert", + BEFOREUNLOAD: "beforeunload", + CONFIRM: "confirm", + PROMPT: "prompt", +}; + +/** + * Helper dedicated to detect and interact with browser dialogs such as `alert`, + * `confirm` etc. The current implementation only supports tabmodal dialogs, + * not full window dialogs. + * + * Emits "dialog-loaded" when a javascript dialog is opened for the current + * browser. + * + * @param {BrowserElement} browser + */ +export class DialogHandler { + constructor(browser) { + lazy.EventEmitter.decorate(this); + this._dialog = null; + this._browser = browser; + + this._onCommonDialogLoaded = this._onCommonDialogLoaded.bind(this); + + Services.obs.addObserver( + this._onCommonDialogLoaded, + "common-dialog-loaded" + ); + } + + destructor() { + this._dialog = null; + this._pageTarget = null; + + Services.obs.removeObserver( + this._onCommonDialogLoaded, + "common-dialog-loaded" + ); + } + + async handleJavaScriptDialog({ accept, promptText }) { + if (!this._dialog) { + throw new Error("No dialog available for handleJavaScriptDialog"); + } + + const type = this._getDialogType(); + if (promptText && type === "prompt") { + this._dialog.ui.loginTextbox.value = promptText; + } + + const onDialogClosed = new Promise(r => { + this._browser.addEventListener("DOMModalDialogClosed", r, { + once: true, + }); + }); + + // 0 corresponds to the OK callback, 1 to the CANCEL callback. + if (accept) { + this._dialog.ui.button0.click(); + } else { + this._dialog.ui.button1.click(); + } + + await onDialogClosed; + + // Resetting dialog to null here might be racy and lead to errors if the + // content page is triggering several prompts in a row. + // See Bug 1569578. + this._dialog = null; + } + + _getDialogType() { + const { inPermitUnload, promptType } = this._dialog.args; + + if (inPermitUnload) { + return DIALOG_TYPES.BEFOREUNLOAD; + } + + switch (promptType) { + case "alert": + return DIALOG_TYPES.ALERT; + case "confirm": + return DIALOG_TYPES.CONFIRM; + case "prompt": + return DIALOG_TYPES.PROMPT; + default: + throw new Error("Unsupported dialog type: " + promptType); + } + } + + _onCommonDialogLoaded(dialogWindow) { + const dialogs = + this._browser.tabDialogBox.getContentDialogManager().dialogs; + const dialog = dialogs.find(d => d.frameContentWindow === dialogWindow); + + if (!dialog) { + // The dialog is not for the current tab. + return; + } + + this._dialog = dialogWindow.Dialog; + const message = this._dialog.args.text; + const type = this._getDialogType(); + + this.emit("dialog-loaded", { message, type }); + } +} diff --git a/remote/cdp/jar.mn b/remote/cdp/jar.mn new file mode 100644 index 0000000000..2b3780ba48 --- /dev/null +++ b/remote/cdp/jar.mn @@ -0,0 +1,55 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +remote.jar: +% content remote %content/ + content/cdp/CDP.sys.mjs (CDP.sys.mjs) + content/cdp/CDPConnection.sys.mjs (CDPConnection.sys.mjs) + content/cdp/Error.sys.mjs (Error.sys.mjs) + content/cdp/JSONHandler.sys.mjs (JSONHandler.sys.mjs) + content/cdp/Protocol.sys.mjs (Protocol.sys.mjs) + content/cdp/StreamRegistry.sys.mjs (StreamRegistry.sys.mjs) + # domains + content/cdp/domains/ContentProcessDomain.sys.mjs (domains/ContentProcessDomain.sys.mjs) + content/cdp/domains/ContentProcessDomains.sys.mjs (domains/ContentProcessDomains.sys.mjs) + content/cdp/domains/Domain.sys.mjs (domains/Domain.sys.mjs) + content/cdp/domains/DomainCache.sys.mjs (domains/DomainCache.sys.mjs) + content/cdp/domains/ParentProcessDomains.sys.mjs (domains/ParentProcessDomains.sys.mjs) + content/cdp/domains/content/DOM.sys.mjs (domains/content/DOM.sys.mjs) + content/cdp/domains/content/Emulation.sys.mjs (domains/content/Emulation.sys.mjs) + content/cdp/domains/content/Input.sys.mjs (domains/content/Input.sys.mjs) + content/cdp/domains/content/Log.sys.mjs (domains/content/Log.sys.mjs) + content/cdp/domains/content/Network.sys.mjs (domains/content/Network.sys.mjs) + content/cdp/domains/content/Page.sys.mjs (domains/content/Page.sys.mjs) + content/cdp/domains/content/Performance.sys.mjs (domains/content/Performance.sys.mjs) + content/cdp/domains/content/Runtime.sys.mjs (domains/content/Runtime.sys.mjs) + content/cdp/domains/content/runtime/ExecutionContext.sys.mjs (domains/content/runtime/ExecutionContext.sys.mjs) + content/cdp/domains/content/Security.sys.mjs (domains/content/Security.sys.mjs) + content/cdp/domains/parent/Browser.sys.mjs (domains/parent/Browser.sys.mjs) + content/cdp/domains/parent/Emulation.sys.mjs (domains/parent/Emulation.sys.mjs) + content/cdp/domains/parent/Fetch.sys.mjs (domains/parent/Fetch.sys.mjs) + content/cdp/domains/parent/Input.sys.mjs (domains/parent/Input.sys.mjs) + content/cdp/domains/parent/IO.sys.mjs (domains/parent/IO.sys.mjs) + content/cdp/domains/parent/Network.sys.mjs (domains/parent/Network.sys.mjs) + content/cdp/domains/parent/Page.sys.mjs (domains/parent/Page.sys.mjs) + content/cdp/domains/parent/page/DialogHandler.sys.mjs (domains/parent/page/DialogHandler.sys.mjs) + content/cdp/domains/parent/Security.sys.mjs (domains/parent/Security.sys.mjs) + content/cdp/domains/parent/SystemInfo.sys.mjs (domains/parent/SystemInfo.sys.mjs) + content/cdp/domains/parent/Target.sys.mjs (domains/parent/Target.sys.mjs) + # observers + content/cdp/observers/ChannelEventSink.sys.mjs (observers/ChannelEventSink.sys.mjs) + content/cdp/observers/ContextObserver.sys.mjs (observers/ContextObserver.sys.mjs) + content/cdp/observers/NetworkObserver.sys.mjs (observers/NetworkObserver.sys.mjs) + content/cdp/observers/TargetObserver.sys.mjs (observers/TargetObserver.sys.mjs) + # sessions + content/cdp/sessions/frame-script.js (sessions/frame-script.js) + content/cdp/sessions/ContentProcessSession.sys.mjs (sessions/ContentProcessSession.sys.mjs) + content/cdp/sessions/MainProcessSession.sys.mjs (sessions/MainProcessSession.sys.mjs) + content/cdp/sessions/Session.sys.mjs (sessions/Session.sys.mjs) + content/cdp/sessions/TabSession.sys.mjs (sessions/TabSession.sys.mjs) + # targets + content/cdp/targets/MainProcessTarget.sys.mjs (targets/MainProcessTarget.sys.mjs) + content/cdp/targets/TabTarget.sys.mjs (targets/TabTarget.sys.mjs) + content/cdp/targets/Target.sys.mjs (targets/Target.sys.mjs) + content/cdp/targets/TargetList.sys.mjs (targets/TargetList.sys.mjs) diff --git a/remote/cdp/moz.build b/remote/cdp/moz.build new file mode 100644 index 0000000000..f1966f0101 --- /dev/null +++ b/remote/cdp/moz.build @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Remote Protocol", "CDP") + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", + "test/browser/browser_cdp_only.toml", + "test/browser/dom/browser.toml", + "test/browser/emulation/browser.toml", + "test/browser/fetch/browser.toml", + "test/browser/input/browser.toml", + "test/browser/io/browser.toml", + "test/browser/log/browser.toml", + "test/browser/network/browser.toml", + "test/browser/page/browser.toml", + "test/browser/runtime/browser.toml", + "test/browser/runtime/browser_with_default_prefs.toml", + "test/browser/security/browser.toml", + "test/browser/systemInfo/browser.toml", + "test/browser/target/browser.toml", +] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] diff --git a/remote/cdp/observers/ChannelEventSink.sys.mjs b/remote/cdp/observers/ChannelEventSink.sys.mjs new file mode 100644 index 0000000000..48e7c6ee64 --- /dev/null +++ b/remote/cdp/observers/ChannelEventSink.sys.mjs @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ComponentUtils } from "resource://gre/modules/ComponentUtils.sys.mjs"; + +const Cm = Components.manager; + +/** + * This is a nsIChannelEventSink implementation that monitors channel redirects. + * This has been forked from: + * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/channel-event-sink.js + * The rest of this module is also more or less forking: + * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/network-observer.js + * TODO(try to re-unify /remote/ with /devtools code) + */ +const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink"; +const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}"); +const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; +const SINK_CATEGORY_NAME = "net-channel-event-sinks"; + +function ChannelEventSink() { + this.wrappedJSObject = this; + this.collectors = new Set(); +} + +ChannelEventSink.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]), + + registerCollector(collector) { + this.collectors.add(collector); + }, + + unregisterCollector(collector) { + this.collectors.delete(collector); + + if (this.collectors.size == 0) { + ChannelEventSinkFactory.unregister(); + } + }, + + // eslint-disable-next-line no-shadow + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + for (const collector of this.collectors) { + try { + collector._onChannelRedirect(oldChannel, newChannel, flags); + } catch (ex) { + console.error( + "StackTraceCollector.onChannelRedirect threw an exception", + ex + ); + } + } + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, +}; + +export const ChannelEventSinkFactory = + ComponentUtils.generateSingletonFactory(ChannelEventSink); + +ChannelEventSinkFactory.register = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (registrar.isCIDRegistered(SINK_CLASS_ID)) { + return; + } + + registrar.registerFactory( + SINK_CLASS_ID, + SINK_CLASS_DESCRIPTION, + SINK_CONTRACT_ID, + ChannelEventSinkFactory + ); + + Services.catMan.addCategoryEntry( + SINK_CATEGORY_NAME, + SINK_CONTRACT_ID, + SINK_CONTRACT_ID, + false, + true + ); +}; + +ChannelEventSinkFactory.unregister = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory); + + Services.catMan.deleteCategoryEntry( + SINK_CATEGORY_NAME, + SINK_CONTRACT_ID, + false + ); +}; + +ChannelEventSinkFactory.getService = function () { + // Make sure the ChannelEventSink service is registered before accessing it + ChannelEventSinkFactory.register(); + + return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink) + .wrappedJSObject; +}; diff --git a/remote/cdp/observers/ContextObserver.sys.mjs b/remote/cdp/observers/ContextObserver.sys.mjs new file mode 100644 index 0000000000..d0d6fd1f93 --- /dev/null +++ b/remote/cdp/observers/ContextObserver.sys.mjs @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Helper class to coordinate Runtime and Page events. + * Events have to be sent in the following order: + * - Runtime.executionContextDestroyed + * - Page.frameNavigated + * - Runtime.executionContextCreated + * + * This class also handles the special case of Pages going from/to the BF cache. + * When you navigate to a new URL, the previous document may be stored in the BF Cache. + * All its asynchronous operations are frozen (XHR, timeouts, ...) and a `pagehide` event + * is fired for this document. We then navigate to the new URL. + * If the user navigates back to the previous page, the page is resurected from the + * cache. A `pageshow` event is fired and its asynchronous operations are resumed. + * + * When a page is in the BF Cache, we should consider it as frozen and shouldn't try + * to execute any javascript. So that the ExecutionContext should be considered as + * being destroyed and the document navigated. + */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + executeSoon: "chrome://remote/content/shared/Sync.sys.mjs", +}); + +export class ContextObserver { + constructor(chromeEventHandler) { + this.chromeEventHandler = chromeEventHandler; + lazy.EventEmitter.decorate(this); + + this._fissionEnabled = Services.appinfo.fissionAutostart; + + this.chromeEventHandler.addEventListener("DOMWindowCreated", this, { + mozSystemGroup: true, + }); + + // Listen for pageshow and pagehide to track pages going in/out to/from the BF Cache + this.chromeEventHandler.addEventListener("pageshow", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.addEventListener("pagehide", this, { + mozSystemGroup: true, + }); + + Services.obs.addObserver(this, "document-element-inserted"); + Services.obs.addObserver(this, "inner-window-destroyed"); + + // With Fission disabled the `DOMWindowCreated` event is fired too late. + // Use the `webnavigation-create` notification instead. + if (!this._fissionEnabled) { + Services.obs.addObserver(this, "webnavigation-create"); + } + Services.obs.addObserver(this, "webnavigation-destroy"); + } + + destructor() { + this.chromeEventHandler.removeEventListener("DOMWindowCreated", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.removeEventListener("pageshow", this, { + mozSystemGroup: true, + }); + this.chromeEventHandler.removeEventListener("pagehide", this, { + mozSystemGroup: true, + }); + + Services.obs.removeObserver(this, "document-element-inserted"); + Services.obs.removeObserver(this, "inner-window-destroyed"); + + if (!this._fissionEnabled) { + Services.obs.removeObserver(this, "webnavigation-create"); + } + Services.obs.removeObserver(this, "webnavigation-destroy"); + } + + handleEvent({ type, target, persisted }) { + const window = target.defaultView; + const frameId = window.browsingContext.id; + const id = window.windowGlobalChild.innerWindowId; + + switch (type) { + case "DOMWindowCreated": + // Do not pass `id` here as that's the new document ID instead of the old one + // that is destroyed. Instead, pass the frameId and let the listener figure out + // what ExecutionContext(s) to destroy. + this.emit("context-destroyed", { frameId }); + + // With Fission enabled the frame is attached early enough so that + // expected network requests and responses are handles afterward. + // Otherwise send the event when `webnavigation-create` is received. + if (this._fissionEnabled) { + this.emit("frame-attached", { frameId, window }); + } + + break; + + case "pageshow": + // `persisted` is true when this is about a page being resurected from BF Cache + if (!persisted) { + return; + } + // XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache + // scenario in Page domain events + this.emit("context-created", { windowId: id, window }); + this.emit("script-loaded", { windowId: id, window }); + break; + + case "pagehide": + // `persisted` is true when this is about a page being frozen into BF Cache + if (!persisted) { + return; + } + this.emit("context-destroyed", { windowId: id }); + break; + } + } + + observe(subject, topic, data) { + switch (topic) { + case "document-element-inserted": + const window = subject.defaultView; + + // Ignore events without a window and those from other tabs + if ( + !window || + window.docShell.chromeEventHandler !== this.chromeEventHandler + ) { + return; + } + + // Send when the document gets attached to the window, and its location + // is available. + this.emit("frame-navigated", { + frameId: window.browsingContext.id, + window, + }); + + const id = window.windowGlobalChild.innerWindowId; + this.emit("context-created", { windowId: id, window }); + // Delay script-loaded to allow context cleanup to happen first + lazy.executeSoon(() => { + this.emit("script-loaded", { windowId: id, window }); + }); + break; + case "inner-window-destroyed": + const windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + this.emit("context-destroyed", { windowId }); + break; + case "webnavigation-create": + subject.QueryInterface(Ci.nsIDocShell); + this.onDocShellCreated(subject); + break; + case "webnavigation-destroy": + subject.QueryInterface(Ci.nsIDocShell); + this.onDocShellDestroyed(subject); + break; + } + } + + onDocShellCreated(docShell) { + this.emit("frame-attached", { + frameId: docShell.browsingContext.id, + window: docShell.browsingContext.window, + }); + } + + onDocShellDestroyed(docShell) { + this.emit("frame-detached", { + frameId: docShell.browsingContext.id, + }); + } +} diff --git a/remote/cdp/observers/NetworkObserver.sys.mjs b/remote/cdp/observers/NetworkObserver.sys.mjs new file mode 100644 index 0000000000..ffd8028ba7 --- /dev/null +++ b/remote/cdp/observers/NetworkObserver.sys.mjs @@ -0,0 +1,630 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CommonUtils: "resource://services-common/utils.sys.mjs", + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + + ChannelEventSinkFactory: + "chrome://remote/content/cdp/observers/ChannelEventSink.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor" +); + +const CC = Components.Constructor; + +ChromeUtils.defineLazyGetter(lazy, "BinaryInputStream", () => { + return CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" + ); +}); + +ChromeUtils.defineLazyGetter(lazy, "BinaryOutputStream", () => { + return CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" + ); +}); + +ChromeUtils.defineLazyGetter(lazy, "StorageStream", () => { + return CC("@mozilla.org/storagestream;1", "nsIStorageStream", "init"); +}); + +// Cap response storage with 100Mb per tracked tab. +const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024; + +export class NetworkObserver { + constructor() { + lazy.EventEmitter.decorate(this); + this._browserSessionCount = new Map(); + lazy.gActivityDistributor.addObserver(this); + lazy.ChannelEventSinkFactory.getService().registerCollector(this); + + this._redirectMap = new Map(); + + // Request interception state. + this._browserSuspendedChannels = new Map(); + this._extraHTTPHeaders = new Map(); + this._browserResponseStorages = new Map(); + + this._onRequest = this._onRequest.bind(this); + this._onExamineResponse = this._onResponse.bind( + this, + false /* fromCache */ + ); + this._onCachedResponse = this._onResponse.bind(this, true /* fromCache */); + } + + dispose() { + lazy.gActivityDistributor.removeObserver(this); + lazy.ChannelEventSinkFactory.getService().unregisterCollector(this); + + Services.obs.removeObserver(this._onRequest, "http-on-modify-request"); + Services.obs.removeObserver( + this._onExamineResponse, + "http-on-examine-response" + ); + Services.obs.removeObserver( + this._onCachedResponse, + "http-on-examine-cached-response" + ); + Services.obs.removeObserver( + this._onCachedResponse, + "http-on-examine-merged-response" + ); + } + + setExtraHTTPHeaders(browser, headers) { + if (!headers) { + this._extraHTTPHeaders.delete(browser); + } else { + this._extraHTTPHeaders.set(browser, headers); + } + } + + enableRequestInterception(browser) { + if (!this._browserSuspendedChannels.has(browser)) { + this._browserSuspendedChannels.set(browser, new Map()); + } + } + + disableRequestInterception(browser) { + const suspendedChannels = this._browserSuspendedChannels.get(browser); + if (!suspendedChannels) { + return; + } + this._browserSuspendedChannels.delete(browser); + for (const channel of suspendedChannels.values()) { + channel.resume(); + } + } + + resumeSuspendedRequest(browser, requestId, headers) { + const suspendedChannels = this._browserSuspendedChannels.get(browser); + if (!suspendedChannels) { + throw new Error(`Request interception is not enabled`); + } + const httpChannel = suspendedChannels.get(requestId); + if (!httpChannel) { + throw new Error(`Cannot find request "${requestId}"`); + } + if (headers) { + // 1. Clear all previous headers. + for (const header of requestHeaders(httpChannel)) { + httpChannel.setRequestHeader(header.name, "", false /* merge */); + } + // 2. Set new headers. + for (const header of headers) { + httpChannel.setRequestHeader( + header.name, + header.value, + false /* merge */ + ); + } + } + suspendedChannels.delete(requestId); + httpChannel.resume(); + } + + getResponseBody(browser, requestId) { + const responseStorage = this._browserResponseStorages.get(browser); + if (!responseStorage) { + throw new Error("Responses are not tracked for the given browser"); + } + return responseStorage.getBase64EncodedResponse(requestId); + } + + abortSuspendedRequest(browser, aRequestId) { + const suspendedChannels = this._browserSuspendedChannels.get(browser); + if (!suspendedChannels) { + throw new Error(`Request interception is not enabled`); + } + const httpChannel = suspendedChannels.get(aRequestId); + if (!httpChannel) { + throw new Error(`Cannot find request "${aRequestId}"`); + } + suspendedChannels.delete(aRequestId); + httpChannel.cancel(Cr.NS_ERROR_FAILURE); + httpChannel.resume(); + this.emit("requestfailed", httpChannel, { + requestId: requestId(httpChannel), + errorCode: getNetworkErrorStatusText(httpChannel.status), + }); + } + + _onChannelRedirect(oldChannel, newChannel) { + // We can be called with any nsIChannel, but are interested only in HTTP channels + try { + oldChannel.QueryInterface(Ci.nsIHttpChannel); + newChannel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + return; + } + + const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); + const loadContext = getLoadContext(httpChannel); + if ( + !loadContext || + !this._browserSessionCount.has(loadContext.topFrameElement) + ) { + return; + } + this._redirectMap.set(newChannel, oldChannel); + } + + _onRequest(channel, topic) { + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const loadContext = getLoadContext(httpChannel); + const browser = loadContext?.topFrameElement; + if (!loadContext || !this._browserSessionCount.has(browser)) { + return; + } + + const extraHeaders = this._extraHTTPHeaders.get(browser); + if (extraHeaders) { + for (const header of extraHeaders) { + httpChannel.setRequestHeader( + header.name, + header.value, + false /* merge */ + ); + } + } + const causeType = httpChannel.loadInfo + ? httpChannel.loadInfo.externalContentPolicyType + : Ci.nsIContentPolicy.TYPE_OTHER; + + const suspendedChannels = this._browserSuspendedChannels.get(browser); + if (suspendedChannels) { + httpChannel.suspend(); + suspendedChannels.set(requestId(httpChannel), httpChannel); + } + + const oldChannel = this._redirectMap.get(httpChannel); + this._redirectMap.delete(httpChannel); + + // Install response body hooks. + new ResponseBodyListener(this, browser, httpChannel); + + this.emit("request", httpChannel, { + url: httpChannel.URI.spec, + suspended: suspendedChannels ? true : undefined, + requestId: requestId(httpChannel), + redirectedFrom: oldChannel ? requestId(oldChannel) : undefined, + postData: readRequestPostData(httpChannel), + headers: requestHeaders(httpChannel), + method: httpChannel.requestMethod, + isNavigationRequest: httpChannel.isMainDocumentChannel, + cause: causeType, + causeString: causeTypeToString(causeType), + frameId: this.frameId(httpChannel), + // clients expect loaderId == requestId for document navigation + loaderId: [ + Ci.nsIContentPolicy.TYPE_DOCUMENT, + Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, + ].includes(causeType) + ? requestId(httpChannel) + : undefined, + }); + } + + _onResponse(fromCache, httpChannel, topic) { + const loadContext = getLoadContext(httpChannel); + if ( + !loadContext || + !this._browserSessionCount.has(loadContext.topFrameElement) + ) { + return; + } + httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); + const causeType = httpChannel.loadInfo + ? httpChannel.loadInfo.externalContentPolicyType + : Ci.nsIContentPolicy.TYPE_OTHER; + let remoteIPAddress; + let remotePort; + try { + remoteIPAddress = httpChannel.remoteAddress; + remotePort = httpChannel.remotePort; + } catch (e) { + // remoteAddress is not defined for cached requests. + } + + this.emit("response", httpChannel, { + requestId: requestId(httpChannel), + securityDetails: getSecurityDetails(httpChannel), + fromCache, + headers: responseHeaders(httpChannel), + requestHeaders: requestHeaders(httpChannel), + remoteIPAddress, + remotePort, + status: httpChannel.responseStatus, + statusText: httpChannel.responseStatusText, + cause: causeType, + causeString: causeTypeToString(causeType), + frameId: this.frameId(httpChannel), + // clients expect loaderId == requestId for document navigation + loaderId: [ + Ci.nsIContentPolicy.TYPE_DOCUMENT, + Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, + ].includes(causeType) + ? requestId(httpChannel) + : undefined, + }); + } + + _onResponseFinished(browser, httpChannel, body) { + const responseStorage = this._browserResponseStorages.get(browser); + if (!responseStorage) { + return; + } + responseStorage.addResponseBody(httpChannel, body); + this.emit("requestfinished", httpChannel, { + requestId: requestId(httpChannel), + errorCode: getNetworkErrorStatusText(httpChannel.status), + }); + } + + isActive(browser) { + return !!this._browserSessionCount.get(browser); + } + + startTrackingBrowserNetwork(browser) { + const value = this._browserSessionCount.get(browser) || 0; + this._browserSessionCount.set(browser, value + 1); + if (value === 0) { + Services.obs.addObserver(this._onRequest, "http-on-modify-request"); + Services.obs.addObserver( + this._onExamineResponse, + "http-on-examine-response" + ); + Services.obs.addObserver( + this._onCachedResponse, + "http-on-examine-cached-response" + ); + Services.obs.addObserver( + this._onCachedResponse, + "http-on-examine-merged-response" + ); + this._browserResponseStorages.set( + browser, + new ResponseStorage( + MAX_RESPONSE_STORAGE_SIZE, + MAX_RESPONSE_STORAGE_SIZE / 10 + ) + ); + } + return () => this.stopTrackingBrowserNetwork(browser); + } + + stopTrackingBrowserNetwork(browser) { + const value = this._browserSessionCount.get(browser); + if (value) { + this._browserSessionCount.set(browser, value - 1); + } else { + this._browserSessionCount.delete(browser); + this._browserResponseStorages.delete(browser); + this.dispose(); + } + } + + /** + * Returns the frameId of the current httpChannel. + */ + frameId(httpChannel) { + const loadInfo = httpChannel.loadInfo; + return loadInfo.frameBrowsingContext?.id || loadInfo.browsingContext.id; + } +} + +const protocolVersionNames = { + [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: "TLS 1", + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: "TLS 1.1", + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: "TLS 1.2", + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: "TLS 1.3", +}; + +function getSecurityDetails(httpChannel) { + const securityInfo = httpChannel.securityInfo; + if (!securityInfo) { + return null; + } + if (!securityInfo.serverCert) { + return null; + } + return { + protocol: protocolVersionNames[securityInfo.protocolVersion] || "<unknown>", + subjectName: securityInfo.serverCert.commonName, + issuer: securityInfo.serverCert.issuerCommonName, + // Convert to seconds. + validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000, + validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000, + }; +} + +function readRequestPostData(httpChannel) { + if (!(httpChannel instanceof Ci.nsIUploadChannel)) { + return undefined; + } + const iStream = httpChannel.uploadStream; + if (!iStream) { + return undefined; + } + const isSeekableStream = iStream instanceof Ci.nsISeekableStream; + + let prevOffset; + if (isSeekableStream) { + prevOffset = iStream.tell(); + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + + // Read data from the stream. + let text; + try { + text = lazy.NetUtil.readInputStreamToString(iStream, iStream.available()); + const converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + text = converter.ConvertToUnicode(text); + } catch (err) { + text = undefined; + } + + // Seek locks the file, so seek to the beginning only if necko hasn"t + // read it yet, since necko doesn"t seek to 0 before reading (at lest + // not till 459384 is fixed). + if (isSeekableStream && prevOffset == 0) { + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + return text; +} + +function getLoadContext(httpChannel) { + let loadContext = null; + try { + if (httpChannel.notificationCallbacks) { + loadContext = httpChannel.notificationCallbacks.getInterface( + Ci.nsILoadContext + ); + } + } catch (e) {} + try { + if (!loadContext && httpChannel.loadGroup) { + loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface( + Ci.nsILoadContext + ); + } + } catch (e) {} + return loadContext; +} + +function requestId(httpChannel) { + return String(httpChannel.channelId); +} + +function requestHeaders(httpChannel) { + const headers = []; + httpChannel.visitRequestHeaders({ + visitHeader: (name, value) => headers.push({ name, value }), + }); + return headers; +} + +function responseHeaders(httpChannel) { + const headers = []; + httpChannel.visitResponseHeaders({ + visitHeader: (name, value) => headers.push({ name, value }), + }); + return headers; +} + +function causeTypeToString(causeType) { + for (let key in Ci.nsIContentPolicy) { + if (Ci.nsIContentPolicy[key] === causeType) { + return key; + } + } + return "TYPE_OTHER"; +} + +class ResponseStorage { + constructor(maxTotalSize, maxResponseSize) { + this._totalSize = 0; + this._maxResponseSize = maxResponseSize; + this._maxTotalSize = maxTotalSize; + this._responses = new Map(); + } + + addResponseBody(httpChannel, body) { + if (body.length > this._maxResponseSize) { + this._responses.set(requestId, { + evicted: true, + body: "", + }); + return; + } + let encodings = []; + if ( + httpChannel instanceof Ci.nsIEncodedChannel && + httpChannel.contentEncodings && + !httpChannel.applyConversion + ) { + const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); + encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + } + this._responses.set(requestId(httpChannel), { body, encodings }); + this._totalSize += body.length; + if (this._totalSize > this._maxTotalSize) { + for (let [, response] of this._responses) { + this._totalSize -= response.body.length; + response.body = ""; + response.evicted = true; + if (this._totalSize < this._maxTotalSize) { + break; + } + } + } + } + + getBase64EncodedResponse(requestId) { + const response = this._responses.get(requestId); + if (!response) { + throw new Error(`Request "${requestId}" is not found`); + } + if (response.evicted) { + return { base64body: "", evicted: true }; + } + let result = response.body; + if (response.encodings && response.encodings.length) { + for (const encoding of response.encodings) { + result = lazy.CommonUtils.convertString( + result, + encoding, + "uncompressed" + ); + } + } + return { base64body: btoa(result) }; + } +} + +class ResponseBodyListener { + constructor(networkObserver, browser, httpChannel) { + this._networkObserver = networkObserver; + this._browser = browser; + this._httpChannel = httpChannel; + this._chunks = []; + this.QueryInterface = ChromeUtils.generateQI(["nsIStreamListener"]); + httpChannel.QueryInterface(Ci.nsITraceableChannel); + this.originalListener = httpChannel.setNewListener(this); + } + + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + const iStream = new lazy.BinaryInputStream(aInputStream); + const sStream = new lazy.StorageStream(8192, aCount, null); + const oStream = new lazy.BinaryOutputStream(sStream.getOutputStream(0)); + + // Copy received data as they come. + const data = iStream.readBytes(aCount); + this._chunks.push(data); + + oStream.writeBytes(data, aCount); + this.originalListener.onDataAvailable( + aRequest, + sStream.newInputStream(0), + aOffset, + aCount + ); + } + + onStartRequest(aRequest) { + this.originalListener.onStartRequest(aRequest); + } + + onStopRequest(aRequest, aStatusCode) { + this.originalListener.onStopRequest(aRequest, aStatusCode); + const body = this._chunks.join(""); + delete this._chunks; + this._networkObserver._onResponseFinished( + this._browser, + this._httpChannel, + body + ); + } +} + +function getNetworkErrorStatusText(status) { + if (!status) { + return null; + } + for (const key of Object.keys(Cr)) { + if (Cr[key] === status) { + return key; + } + } + // Security module. The following is taken from + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL + if ((status & 0xff0000) === 0x5a0000) { + // NSS_SEC errors (happen below the base value because of negative vals) + if ( + (status & 0xffff) < + Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) + ) { + // The bases are actually negative, so in our positive numeric space, we + // need to subtract the base off our value. + const nssErr = + Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); + switch (nssErr) { + case 11: + return "SEC_ERROR_EXPIRED_CERTIFICATE"; + case 12: + return "SEC_ERROR_REVOKED_CERTIFICATE"; + case 13: + return "SEC_ERROR_UNKNOWN_ISSUER"; + case 20: + return "SEC_ERROR_UNTRUSTED_ISSUER"; + case 21: + return "SEC_ERROR_UNTRUSTED_CERT"; + case 36: + return "SEC_ERROR_CA_CERT_INVALID"; + case 90: + return "SEC_ERROR_INADEQUATE_KEY_USAGE"; + case 176: + return "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED"; + default: + return "SEC_ERROR_UNKNOWN"; + } + } + const sslErr = + Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); + switch (sslErr) { + case 3: + return "SSL_ERROR_NO_CERTIFICATE"; + case 4: + return "SSL_ERROR_BAD_CERTIFICATE"; + case 8: + return "SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE"; + case 9: + return "SSL_ERROR_UNSUPPORTED_VERSION"; + case 12: + return "SSL_ERROR_BAD_CERT_DOMAIN"; + default: + return "SSL_ERROR_UNKNOWN"; + } + } + return "<unknown error>"; +} diff --git a/remote/cdp/observers/TargetObserver.sys.mjs b/remote/cdp/observers/TargetObserver.sys.mjs new file mode 100644 index 0000000000..dfd9e2d9dc --- /dev/null +++ b/remote/cdp/observers/TargetObserver.sys.mjs @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + EventPromise: "chrome://remote/content/shared/Sync.sys.mjs", +}); + +// TODO(ato): +// +// The DOM team is working on pulling browsing context related behaviour, +// such as window and tab handling, out of product code and into the platform. +// This will have implication for the remote agent, +// and as the platform gains support for product-independent events +// we can likely get rid of this entire module. + +/** + * Observe Firefox tabs as they open and close. + * + * "open" fires when a tab opens. + * "close" fires when a tab closes. + */ +export class TabObserver { + /** + * @param {boolean?} [false] registerExisting + * Events will be fired for ChromeWIndows and their respective tabs + * at the time when the observer is started. + */ + constructor({ registerExisting = false } = {}) { + lazy.EventEmitter.decorate(this); + + this.registerExisting = registerExisting; + + this.onTabOpen = this.onTabOpen.bind(this); + this.onTabClose = this.onTabClose.bind(this); + } + + async start() { + Services.wm.addListener(this); + + if (this.registerExisting) { + // Start listening for events on already open windows + for (const win of Services.wm.getEnumerator("navigator:browser")) { + this._registerDOMWindow(win); + } + } + } + + stop() { + Services.wm.removeListener(this); + + // Stop listening for events on still opened windows + for (const win of Services.wm.getEnumerator("navigator:browser")) { + this._unregisterDOMWindow(win); + } + } + + // Event emitters + + onTabOpen({ target }) { + this.emit("open", target); + } + + onTabClose({ target }) { + this.emit("close", target); + } + + // Internal methods + + _registerDOMWindow(win) { + for (const tab of win.gBrowser.tabs) { + // a missing linkedBrowser means the tab is still initialising, + // and a TabOpen event will fire once it is ready + if (!tab.linkedBrowser) { + continue; + } + + this.onTabOpen({ target: tab }); + } + + win.gBrowser.tabContainer.addEventListener("TabOpen", this.onTabOpen); + win.gBrowser.tabContainer.addEventListener("TabClose", this.onTabClose); + } + + _unregisterDOMWindow(win) { + for (const tab of win.gBrowser.tabs) { + // a missing linkedBrowser means the tab is still initialising + if (!tab.linkedBrowser) { + continue; + } + + // Emulate custom "TabClose" events because that event is not + // fired for each of the tabs when the window closes. + this.onTabClose({ target: tab }); + } + + win.gBrowser.tabContainer.removeEventListener("TabOpen", this.onTabOpen); + win.gBrowser.tabContainer.removeEventListener("TabClose", this.onTabClose); + } + + // nsIWindowMediatorListener + + async onOpenWindow(xulWindow) { + const win = xulWindow.docShell.domWindow; + + await new lazy.EventPromise(win, "load"); + + // Return early if it's not a browser window + if ( + win.document.documentElement.getAttribute("windowtype") != + "navigator:browser" + ) { + return; + } + + this._registerDOMWindow(win); + } + + onCloseWindow(xulWindow) { + const win = xulWindow.docShell.domWindow; + + // Return early if it's not a browser window + if ( + win.document.documentElement.getAttribute("windowtype") != + "navigator:browser" + ) { + return; + } + + this._unregisterDOMWindow(win); + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIWindowMediatorListener"]); + } +} diff --git a/remote/cdp/sessions/ContentProcessSession.sys.mjs b/remote/cdp/sessions/ContentProcessSession.sys.mjs new file mode 100644 index 0000000000..d7aa3de57b --- /dev/null +++ b/remote/cdp/sessions/ContentProcessSession.sys.mjs @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContentProcessDomains: + "chrome://remote/content/cdp/domains/ContentProcessDomains.sys.mjs", + ContextObserver: + "chrome://remote/content/cdp/observers/ContextObserver.sys.mjs", + DomainCache: "chrome://remote/content/cdp/domains/DomainCache.sys.mjs", +}); + +export class ContentProcessSession { + constructor(messageManager, browsingContext, content, docShell) { + this.messageManager = messageManager; + this.browsingContext = browsingContext; + this.content = content; + this.docShell = docShell; + // Most children or sibling classes are going to assume that docShell + // implements the following interface. So do the QI only once from here. + this.docShell.QueryInterface(Ci.nsIWebNavigation); + + this.domains = new lazy.DomainCache(this, lazy.ContentProcessDomains); + this.messageManager.addMessageListener("remote:request", this); + this.messageManager.addMessageListener("remote:destroy", this); + } + + destructor() { + this._contextObserver?.destructor(); + + this.messageManager.removeMessageListener("remote:request", this); + this.messageManager.removeMessageListener("remote:destroy", this); + this.domains.clear(); + } + + get contextObserver() { + if (!this._contextObserver) { + this._contextObserver = new lazy.ContextObserver( + this.docShell.chromeEventHandler + ); + } + return this._contextObserver; + } + + // Domain event listener + + onEvent(eventName, params) { + this.messageManager.sendAsyncMessage("remote:event", { + browsingContextId: this.browsingContext.id, + event: { + eventName, + params, + }, + }); + } + + // nsIMessageListener + + async receiveMessage({ name, data }) { + const { browsingContextId } = data; + + // We may have more than one tab loaded in the same process, + // and debug the two at the same time. We want to ensure not + // mixing up the requests made against two such tabs. + // Each tab is going to have its own frame script instance + // and two communication channels are going to be set up via + // the two message managers. + if (browsingContextId != this.browsingContext.id) { + return; + } + + switch (name) { + case "remote:request": + try { + const { id, domain, command, params } = data.request; + + const result = await this.domains.execute(domain, command, params); + + this.messageManager.sendAsyncMessage("remote:result", { + browsingContextId, + id, + result, + }); + } catch (e) { + this.messageManager.sendAsyncMessage("remote:error", { + browsingContextId, + id: data.request.id, + error: { + name: e.name || "exception", + message: e.message || String(e), + stack: e.stack, + }, + }); + } + break; + + case "remote:destroy": + this.destructor(); + break; + } + } +} diff --git a/remote/cdp/sessions/MainProcessSession.sys.mjs b/remote/cdp/sessions/MainProcessSession.sys.mjs new file mode 100644 index 0000000000..259e1312ac --- /dev/null +++ b/remote/cdp/sessions/MainProcessSession.sys.mjs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Session } from "chrome://remote/content/cdp/sessions/Session.sys.mjs"; + +/** + * A session, dedicated to the main process target. + * For some reason, it doesn't need any specific code and can share the base Session class + * aside TabSession. + */ +export class MainProcessSession extends Session {} diff --git a/remote/cdp/sessions/Session.sys.mjs b/remote/cdp/sessions/Session.sys.mjs new file mode 100644 index 0000000000..cbc82fb097 --- /dev/null +++ b/remote/cdp/sessions/Session.sys.mjs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DomainCache: "chrome://remote/content/cdp/domains/DomainCache.sys.mjs", + NetworkObserver: + "chrome://remote/content/cdp/observers/NetworkObserver.sys.mjs", + ParentProcessDomains: + "chrome://remote/content/cdp/domains/ParentProcessDomains.sys.mjs", +}); + +/** + * A session represents exactly one client WebSocket connection. + * + * Every new WebSocket connections is associated with one session that + * deals with dispatching incoming command requests to the right + * target, sending back responses, and propagating events originating + * from domains. + * Then, some subsequent Sessions may be created over a single WebSocket + * connection. In this case, the subsequent session will have an `id` + * being passed to their constructor and each packet of these sessions + * will have a `sessionId` attribute in order to filter the packets + * by session both on client and server side. + */ +export class Session { + /** + * @param {Connection} connection + * The connection used to communicate with the server. + * @param {Target} target + * The target to which this session communicates with. + * @param {number=} id + * If this session isn't the default one used for the HTTP endpoint we + * connected to, the session requires an id to distinguish it from the default + * one. This id is used to filter our request, responses and events between + * all active sessions. For now, this is only passed by `Target.attachToTarget()`. + */ + constructor(connection, target, id) { + this.connection = connection; + this.target = target; + this.id = id; + + this.domains = new lazy.DomainCache(this, lazy.ParentProcessDomains); + } + + destructor() { + if ( + this.networkObserver && + this.networkObserver.isActive(this.target.browser) + ) { + this.networkObserver.dispose(); + } + this.domains.clear(); + } + + execute(id, domain, command, params) { + return this.domains.execute(domain, command, params); + } + + get networkObserver() { + if (!this._networkObserver) { + this._networkObserver = new lazy.NetworkObserver(); + } + return this._networkObserver; + } + + /** + * Domains event listener. Called when an event is fired + * by any Domain and has to be sent to the client. + */ + onEvent(eventName, params) { + this.connection.sendEvent(eventName, params, this.id); + } + + toString() { + return `[object ${this.constructor.name} ${this.connection.id}]`; + } +} diff --git a/remote/cdp/sessions/TabSession.sys.mjs b/remote/cdp/sessions/TabSession.sys.mjs new file mode 100644 index 0000000000..8c8b5f585e --- /dev/null +++ b/remote/cdp/sessions/TabSession.sys.mjs @@ -0,0 +1,148 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Session } from "chrome://remote/content/cdp/sessions/Session.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); + +/** + * A session to communicate with a given tab + */ +export class TabSession extends Session { + /** + * @param {Connection} connection + * The connection used to communicate with the server. + * @param {TabTarget} target + * The tab target to which this session communicates with. + * @param {number=} id + * If this session isn't the default one used for the HTTP endpoint we + * connected to, the session requires an id to distinguish it from the default + * one. This id is used to filter our request, responses and events between + * all active sessions. + * For now, this is only passed by `Target.attachToTarget()`. + * Otherwise it will be undefined when you are connecting directly to + * a given Tab. i.e. connect directly to the WebSocket URL provided by + * /json/list HTTP endpoint. + */ + constructor(connection, target, id) { + super(connection, target, id); + + // Request id => { resolve, reject } + this.requestPromises = new Map(); + + this.registerFramescript(this.mm); + + this.target.browser.addEventListener("XULFrameLoaderCreated", this); + } + + destructor() { + super.destructor(); + + this.requestPromises.clear(); + + this.target.browser.removeEventListener("XULFrameLoaderCreated", this); + + // this.mm might be null if the browser of the TabTarget was already closed. + // See Bug 1747301. + this.mm?.sendAsyncMessage("remote:destroy", { + browsingContextId: this.browsingContext.id, + }); + + this.mm?.removeMessageListener("remote:event", this); + this.mm?.removeMessageListener("remote:result", this); + this.mm?.removeMessageListener("remote:error", this); + } + + execute(id, domain, command, params) { + // Check if the domain and command is implemented in the parent + // and execute it there. Otherwise forward the command to the content process + // in order to try to execute it in the content process. + if (this.domains.domainSupportsMethod(domain, command)) { + return super.execute(id, domain, command, params); + } + return this.executeInChild(id, domain, command, params); + } + + executeInChild(id, domain, command, params) { + return new Promise((resolve, reject) => { + // Save the promise's resolution and rejection handler in order to later + // resolve this promise once we receive the reply back from the content process. + this.requestPromises.set(id, { resolve, reject }); + + this.mm.sendAsyncMessage("remote:request", { + browsingContextId: this.browsingContext.id, + request: { id, domain, command, params }, + }); + }); + } + + get mm() { + return this.target.mm; + } + + get browsingContext() { + return this.target.browsingContext; + } + + /** + * Register the framescript and listeners for the given message manager. + * + * @param {MessageManager} messageManager + * The message manager to use. + */ + registerFramescript(messageManager) { + messageManager.loadFrameScript( + "chrome://remote/content/cdp/sessions/frame-script.js", + false + ); + + messageManager.addMessageListener("remote:event", this); + messageManager.addMessageListener("remote:result", this); + messageManager.addMessageListener("remote:error", this); + } + + // Event handler + handleEvent = function ({ target, type }) { + switch (type) { + case "XULFrameLoaderCreated": + if (target === this.target.browser) { + lazy.logger.trace("Remoteness change detected"); + this.registerFramescript(this.mm); + } + break; + } + }; + + // nsIMessageListener + + receiveMessage({ name, data }) { + const { id, result, event, error } = data; + + switch (name) { + case "remote:result": + const { resolve } = this.requestPromises.get(id); + resolve(result); + this.requestPromises.delete(id); + break; + + case "remote:event": + this.connection.sendEvent(event.eventName, event.params, this.id); + break; + + case "remote:error": + const { reject } = this.requestPromises.get(id); + reject(error); + this.requestPromises.delete(id); + break; + } + } +} diff --git a/remote/cdp/sessions/frame-script.js b/remote/cdp/sessions/frame-script.js new file mode 100644 index 0000000000..88071086cb --- /dev/null +++ b/remote/cdp/sessions/frame-script.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { ContentProcessSession } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/sessions/ContentProcessSession.sys.mjs" +); + +new ContentProcessSession(this, docShell.browsingContext, content, docShell); diff --git a/remote/cdp/targets/MainProcessTarget.sys.mjs b/remote/cdp/targets/MainProcessTarget.sys.mjs new file mode 100644 index 0000000000..1f32b5bde5 --- /dev/null +++ b/remote/cdp/targets/MainProcessTarget.sys.mjs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Target } from "chrome://remote/content/cdp/targets/Target.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + MainProcessSession: + "chrome://remote/content/cdp/sessions/MainProcessSession.sys.mjs", + RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", +}); + +/** + * The main process Target. + * + * Matches BrowserDevToolsAgentHost from chromium, and only support a couple of Domains: + * https://cs.chromium.org/chromium/src/content/browser/devtools/browser_devtools_agent_host.cc?dr=CSs&g=0&l=80-91 + */ +export class MainProcessTarget extends Target { + /* + * @param TargetList targetList + */ + constructor(targetList) { + super(targetList, lazy.MainProcessSession); + + this.type = "browser"; + + // Define the HTTP path to query this target + this.path = `/devtools/browser/${this.id}`; + } + + get wsDebuggerURL() { + const { host, port } = lazy.RemoteAgent; + return `ws://${host}:${port}${this.path}`; + } + + toString() { + return `[object MainProcessTarget]`; + } + + toJSON() { + return { + description: "Main process target", + devtoolsFrontendUrl: "", + faviconUrl: "", + id: this.id, + title: "Main process target", + type: this.type, + url: "", + webSocketDebuggerUrl: this.wsDebuggerURL, + }; + } +} diff --git a/remote/cdp/targets/TabTarget.sys.mjs b/remote/cdp/targets/TabTarget.sys.mjs new file mode 100644 index 0000000000..a35b0c3f07 --- /dev/null +++ b/remote/cdp/targets/TabTarget.sys.mjs @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { Target } from "chrome://remote/content/cdp/targets/Target.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TabSession: "chrome://remote/content/cdp/sessions/TabSession.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "Favicons", + "@mozilla.org/browser/favicon-service;1", + "nsIFaviconService" +); + +/** + * Target for a local tab or a remoted frame. + */ +export class TabTarget extends Target { + /** + * @param {TargetList} targetList + * @param {BrowserElement} browser + */ + constructor(targetList, browser) { + super(targetList, lazy.TabSession); + + this.browser = browser; + + // The tab target uses a unique id as shared with WebDriver to reference + // a specific tab. + this.id = lazy.TabManager.getIdForBrowser(browser); + + // Define the HTTP path to query this target + this.path = `/devtools/page/${this.id}`; + + Services.obs.addObserver(this, "message-manager-disconnect"); + } + + destructor() { + Services.obs.removeObserver(this, "message-manager-disconnect"); + super.destructor(); + } + + get browserContextId() { + return parseInt(this.browser.getAttribute("usercontextid")); + } + + get browsingContext() { + return this.browser.browsingContext; + } + + get mm() { + return this.browser.messageManager; + } + + get window() { + return this.browser.ownerGlobal; + } + + get tab() { + return this.window.gBrowser.getTabForBrowser(this.browser); + } + + /** + * Determines if the content browser remains attached + * to its parent chrome window. + * + * We determine this by checking if the <browser> element + * is still attached to the DOM. + * + * @returns {boolean} + * True if target's browser is still attached, + * false if it has been disconnected. + */ + get closed() { + return !this.browser || !this.browser.isConnected; + } + + get description() { + return ""; + } + + get frontendURL() { + return null; + } + + /** @returns {Promise<string|null>} */ + get faviconUrl() { + return new Promise((resolve, reject) => { + lazy.Favicons.getFaviconURLForPage(this.browser.currentURI, url => { + if (url) { + resolve(url.spec); + } else { + resolve(null); + } + }); + }); + } + + get title() { + return this.browsingContext.currentWindowGlobal.documentTitle; + } + + get type() { + return "page"; + } + + get url() { + return this.browser.currentURI.spec; + } + + get wsDebuggerURL() { + const { host, port } = lazy.RemoteAgent; + return `ws://${host}:${port}${this.path}`; + } + + toString() { + return `[object Target ${this.id}]`; + } + + toJSON() { + return { + description: this.description, + devtoolsFrontendUrl: this.frontendURL, + // TODO(ato): toJSON cannot be marked async )-: + faviconUrl: "", + id: this.id, + // Bug 1680817: Fails to encode some UTF-8 characters + // title: this.title, + type: this.type, + url: this.url, + webSocketDebuggerUrl: this.wsDebuggerURL, + }; + } + + // nsIObserver + + observe(subject, topic, data) { + if (subject === this.mm && subject == "message-manager-disconnect") { + // disconnect debugging target if <browser> is disconnected, + // otherwise this is just a host process change + if (this.closed) { + this.disconnect(); + } + } + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIHttpRequestHandler", "nsIObserver"]); + } +} diff --git a/remote/cdp/targets/Target.sys.mjs b/remote/cdp/targets/Target.sys.mjs new file mode 100644 index 0000000000..9264110c37 --- /dev/null +++ b/remote/cdp/targets/Target.sys.mjs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CDPConnection: "chrome://remote/content/cdp/CDPConnection.sys.mjs", + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + WebSocketHandshake: + "chrome://remote/content/server/WebSocketHandshake.sys.mjs", +}); + +/** + * Base class for all the targets. + */ +export class Target { + /** + * @param {TargetList} targetList + * @param {Class} sessionClass + */ + constructor(targetList, sessionClass) { + // Save a reference to TargetList instance in order to expose it to: + // domains/parent/Target.jsm + this.targetList = targetList; + + // When a new connection is made to this target, + // we will instantiate a new session based on this given class. + // The session class is specific to each target's kind and is passed + // by the inheriting class. + this.sessionClass = sessionClass; + + // There can be more than one connection if multiple clients connect to the remote agent. + this.connections = new Set(); + this.id = lazy.generateUUID(); + } + + /** + * Close all active connections made to this target. + */ + destructor() { + for (const conn of this.connections) { + conn.close(); + } + } + + // nsIHttpRequestHandler + + async handle(request, response) { + const webSocket = await lazy.WebSocketHandshake.upgrade(request, response); + const conn = new lazy.CDPConnection(webSocket, response._connection); + const session = new this.sessionClass(conn, this); + conn.registerSession(session); + this.connections.add(conn); + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIHttpRequestHandler"]); + } +} diff --git a/remote/cdp/targets/TargetList.sys.mjs b/remote/cdp/targets/TargetList.sys.mjs new file mode 100644 index 0000000000..a68b36763f --- /dev/null +++ b/remote/cdp/targets/TargetList.sys.mjs @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + MainProcessTarget: + "chrome://remote/content/cdp/targets/MainProcessTarget.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TabObserver: "chrome://remote/content/cdp/observers/TargetObserver.sys.mjs", + TabTarget: "chrome://remote/content/cdp/targets/TabTarget.sys.mjs", +}); + +export class TargetList { + constructor() { + // Target ID -> Target + this._targets = new Map(); + + lazy.EventEmitter.decorate(this); + } + + /** + * Start listing and listening for all the debuggable targets + */ + async watchForTargets() { + await this.watchForTabs(); + } + + unwatchForTargets() { + this.unwatchForTabs(); + } + + /** + * Watch for all existing and new tabs being opened. + * So that we can create the related TabTarget instance for + * each of them. + */ + async watchForTabs() { + if (this.tabObserver) { + throw new Error("Targets is already watching for new tabs"); + } + + this.tabObserver = new lazy.TabObserver({ registerExisting: true }); + + // Handle creation of tab targets for opened tabs. + this.tabObserver.on("open", async (eventName, tab) => { + const target = new lazy.TabTarget(this, tab.linkedBrowser); + this.registerTarget(target); + }); + + // Handle removal of tab targets when tabs are closed. + this.tabObserver.on("close", (eventName, tab) => { + const browser = tab.linkedBrowser; + + // Ignore unloaded tabs. + if (browser.browserId === 0) { + return; + } + + const id = lazy.TabManager.getIdForBrowser(browser); + const target = Array.from(this._targets.values()).find( + target => target.id == id + ); + if (target) { + this.destroyTarget(target); + } + }); + await this.tabObserver.start(); + } + + unwatchForTabs() { + if (this.tabObserver) { + this.tabObserver.stop(); + this.tabObserver = null; + } + } + + /** + * To be called right after instantiating a new Target instance. + * This will hold the new instance in the list and notify about + * its creation. + */ + registerTarget(target) { + this._targets.set(target.id, target); + this.emit("target-created", target); + } + + /** + * To be called when the debuggable target has been destroy. + * So that we can notify it no longer exists and disconnect + * all connecting made to debug it. + */ + destroyTarget(target) { + target.destructor(); + this._targets.delete(target.id); + this.emit("target-destroyed", target); + } + + /** + * Destroy all the registered target of all kinds. + * This will end up dropping all connections made to debug any of them. + */ + destructor() { + for (const target of this) { + this.destroyTarget(target); + } + this._targets.clear(); + if (this.mainProcessTarget) { + this.mainProcessTarget = null; + } + + this.unwatchForTargets(); + } + + get size() { + return this._targets.size; + } + + /** + * Get Target instance by target id + * + * @param {string} id + * Target id + * + * @returns {Target} + */ + getById(id) { + return this._targets.get(id); + } + + /** + * Get the Target instance for the main process. + * This target is a singleton and only exposes a subset of domains. + */ + getMainProcessTarget() { + if (!this.mainProcessTarget) { + this.mainProcessTarget = new lazy.MainProcessTarget(this); + this.registerTarget(this.mainProcessTarget); + } + return this.mainProcessTarget; + } + + *[Symbol.iterator]() { + for (const target of this._targets.values()) { + yield target; + } + } + + toJSON() { + return [...this]; + } + + toString() { + return `[object TargetList ${this.size}]`; + } +} diff --git a/remote/cdp/test/browser/README.md b/remote/cdp/test/browser/README.md new file mode 100644 index 0000000000..65ed2b862d --- /dev/null +++ b/remote/cdp/test/browser/README.md @@ -0,0 +1,11 @@ +Update chrome-remote-interface.js +================================= + +Upstream instructions on +https://github.com/cyrus-and/chrome-remote-interface#using-vanilla-javascript: + + % git clone https://github.com/cyrus-and/chrome-remote-interface.git + % cd chrome-remote-interface + % npm install + % TARGET=var DEBUG=true npm run webpack + % cp chrome-remote-interface.js ../ diff --git a/remote/cdp/test/browser/browser.toml b/remote/cdp/test/browser/browser.toml new file mode 100644 index 0000000000..83c1fcd607 --- /dev/null +++ b/remote/cdp/test/browser/browser.toml @@ -0,0 +1,30 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "chrome-remote-interface.js", + "head.js", +] + +["browser_agent.js"] + +["browser_cdp.js"] + +["browser_httpd.js"] + +["browser_main_target.js"] + +["browser_session.js"] + +["browser_tabs.js"] diff --git a/remote/cdp/test/browser/browser_agent.js b/remote/cdp/test/browser/browser_agent.js new file mode 100644 index 0000000000..faa4ce7fbc --- /dev/null +++ b/remote/cdp/test/browser/browser_agent.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// To fully test the Remote Agent's capabilities an instance of the interface +// also needs to be used. +const remoteAgentInstance = Cc["@mozilla.org/remote/agent;1"].createInstance( + Ci.nsIRemoteAgent +); + +add_task(async function running() { + is(remoteAgentInstance.running, true, "Agent is running"); +}); diff --git a/remote/cdp/test/browser/browser_cdp.js b/remote/cdp/test/browser/browser_cdp.js new file mode 100644 index 0000000000..bbc593c39e --- /dev/null +++ b/remote/cdp/test/browser/browser_cdp.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. +add_task(async function testCDP({ client }) { + const { Browser, Log, Page } = client; + + ok("Browser" in client, "Browser domain is available"); + ok("Log" in client, "Log domain is available"); + ok("Page" in client, "Page domain is available"); + + const version = await Browser.getVersion(); + const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService( + Ci.nsIGfxInfo + ); + is( + version.product, + (isHeadless ? "Headless" : "") + + `${Services.appinfo.name}/${Services.appinfo.version}`, + "Browser.getVersion works and depends on headless mode" + ); + is( + version.userAgent, + window.navigator.userAgent, + "Browser.getVersion().userAgent is correct" + ); + + is( + version.revision, + Services.appinfo.sourceURL.split("/").pop(), + "Browser.getVersion().revision is correct" + ); + + is( + version.jsVersion, + Services.appinfo.version, + "Browser.getVersion().jsVersion is correct" + ); + + // receive console.log messages and print them + let result = await Log.enable(); + info("Log domain has been enabled"); + Assert.deepEqual(result, {}, "Got expected result value"); + + Log.entryAdded(({ entry }) => { + const { timestamp, level, text, args } = entry; + const msg = text || args.join(" "); + console.log(`${new Date(timestamp)}\t${level.toUpperCase()}\t${msg}`); + }); + + // turn on navigation related events, such as DOMContentLoaded et al. + result = await Page.enable(); + info("Page domain has been enabled"); + Assert.deepEqual(result, {}, "Got expected result value"); + + const frameStoppedLoading = Page.frameStoppedLoading(); + const frameNavigated = Page.frameNavigated(); + const loadEventFired = Page.loadEventFired(); + await Page.navigate({ + url: toDataURL(`<script>console.log("foo")</script>`), + }); + info("A new page has been requested"); + + await loadEventFired; + info("`Page.loadEventFired` fired"); + + await frameStoppedLoading; + info("`Page.frameStoppedLoading` fired"); + + await frameNavigated; + info("`Page.frameNavigated` fired"); +}); diff --git a/remote/cdp/test/browser/browser_cdp_only.toml b/remote/cdp/test/browser/browser_cdp_only.toml new file mode 100644 index 0000000000..e4f6145922 --- /dev/null +++ b/remote/cdp/test/browser/browser_cdp_only.toml @@ -0,0 +1,22 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] + +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", + "remote.active-protocols=2", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "chrome-remote-interface.js", + "head.js", +] + +["browser_interface.js"] diff --git a/remote/cdp/test/browser/browser_httpd.js b/remote/cdp/test/browser/browser_httpd.js new file mode 100644 index 0000000000..fb74ae8b70 --- /dev/null +++ b/remote/cdp/test/browser/browser_httpd.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { JSONHandler } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/JSONHandler.sys.mjs" +); + +// Get list of supported routes from JSONHandler +const routes = new JSONHandler().routes; + +add_task(async function json_version() { + const { userAgent } = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + + const json = await requestJSON("/json/version"); + is( + json.Browser, + `${Services.appinfo.name}/${Services.appinfo.version}`, + "Browser name and version found" + ); + is(json["Protocol-Version"], "1.3", "Protocol version found"); + is(json["User-Agent"], userAgent, "User agent found"); + is(json["V8-Version"], "1.0", "V8 version found"); + is(json["WebKit-Version"], "1.0", "Webkit version found"); + is( + json.webSocketDebuggerUrl, + RemoteAgent.cdp.targetList.getMainProcessTarget().wsDebuggerURL, + "Websocket URL for main process target found" + ); +}); + +add_task(async function check_routes() { + for (const route in routes) { + const { parameter, method } = routes[route]; + // Skip routes expecting parameter + if (parameter) { + continue; + } + + // Check request succeeded (200) and responded with valid JSON + info(`Checking ${route}`); + await requestJSON(route, { method }); + + // Check with trailing slash + info(`Checking ${route + "/"}`); + await requestJSON(route + "/", { method }); + + // Test routes expecting a certain method + if (method) { + const responseText = await requestJSON(route, { + method: "DELETE", + status: 405, + json: false, + }); + is( + responseText, + `Using unsafe HTTP verb DELETE to invoke ${route}. This action supports only ${method} verb.`, + "/json/new fails with 405 when using GET" + ); + } + } +}); + +add_task(async function json_list({ client }) { + const { Target } = client; + const { targetInfos } = await Target.getTargets(); + + const json = await requestJSON("/json/list"); + const jsonAlias = await requestJSON("/json"); + + Assert.deepEqual(json, jsonAlias, "/json/list and /json return the same"); + + ok(Array.isArray(json), "Target list is an array"); + + is( + json.length, + targetInfos.length, + "Targets as listed on /json/list are equal to Target.getTargets" + ); + + for (let i = 0; i < json.length; i++) { + const jsonTarget = json[i]; + const wsTarget = targetInfos[i]; + + is( + jsonTarget.id, + wsTarget.targetId, + "Target id matches between HTTP and Target.getTargets" + ); + is( + jsonTarget.type, + wsTarget.type, + "Target type matches between HTTP and Target.getTargets" + ); + is( + jsonTarget.url, + wsTarget.url, + "Target url matches between HTTP and Target.getTargets" + ); + + // Ensure expected values specifically for JSON endpoint + // and that type is always "page" as main process target should not be included + is( + jsonTarget.type, + "page", + `Target (${jsonTarget.id}) from list has expected type (page)` + ); + is( + jsonTarget.webSocketDebuggerUrl, + `ws://${RemoteAgent.debuggerAddress}/devtools/page/${wsTarget.targetId}`, + `Target (${jsonTarget.id}) from list has expected webSocketDebuggerUrl value` + ); + } +}); + +add_task(async function json_new_target({ client }) { + const newUrl = "https://example.com"; + + let getError = await requestJSON("/json/new?" + newUrl, { + status: 405, + json: false, + }); + is( + getError, + "Using unsafe HTTP verb GET to invoke /json/new. This action supports only PUT verb.", + "/json/new fails with 405 when using GET" + ); + + const newTarget = await requestJSON("/json/new?" + newUrl, { method: "PUT" }); + + is(newTarget.type, "page", "Returned target type is 'page'"); + is(newTarget.url, newUrl, "Returned target URL matches"); + ok(!!newTarget.id, "Returned target has id"); + ok( + !!newTarget.webSocketDebuggerUrl, + "Returned target has webSocketDebuggerUrl" + ); + + const { Target } = client; + const targets = await getDiscoveredTargets(Target); + const foundTarget = targets.find(target => target.targetId === newTarget.id); + + ok(!!foundTarget, "Returned target id was found"); +}); + +add_task(async function json_activate_target({ client, tab }) { + const { Target, target } = client; + + const currentTargetId = target.id; + const targets = await getDiscoveredTargets(Target); + const initialTarget = targets.find( + target => target.targetId === currentTargetId + ); + ok(!!initialTarget, "The current target has been found"); + + // open some more tabs in the initial window + await openTab(Target); + await openTab(Target); + + const lastTabFirstWindow = await openTab(Target); + is( + gBrowser.selectedTab, + lastTabFirstWindow.newTab, + "Selected tab has changed to a new tab" + ); + + const activateResponse = await requestJSON( + "/json/activate/" + initialTarget.targetId, + { json: false } + ); + + is( + activateResponse, + "Target activated", + "Activate endpoint returned expected string" + ); + + is(gBrowser.selectedTab, tab, "Selected tab is the initial tab again"); + + const invalidResponse = await requestJSON("/json/activate/does-not-exist", { + status: 404, + json: false, + }); + + is(invalidResponse, "No such target id: does-not-exist"); +}); + +add_task(async function json_close_target({ CDP, client, tab }) { + const { Target } = client; + + const { targetInfo, newTab } = await openTab(Target); + + const targetListBefore = await CDP.List(); + const beforeTarget = targetListBefore.find( + target => target.id === targetInfo.targetId + ); + + ok(!!beforeTarget, "New target has been found"); + + const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose"); + const targetDestroyed = Target.targetDestroyed(); + + const activateResponse = await requestJSON( + "/json/close/" + targetInfo.targetId, + { json: false } + ); + is( + activateResponse, + "Target is closing", + "Close endpoint returned expected string" + ); + + await tabClosed; + info("Tab was closed"); + + await targetDestroyed; + info("Received the Target.targetDestroyed event"); + + const targetListAfter = await CDP.List(); + const afterTarget = targetListAfter.find( + target => target.id === targetInfo.targetId + ); + + ok(afterTarget == null, "New target is gone"); + + const invalidResponse = await requestJSON("/json/close/does-not-exist", { + status: 404, + json: false, + }); + + is(invalidResponse, "No such target id: does-not-exist"); +}); + +add_task(async function json_prevent_load_in_iframe({ client }) { + const { Page } = client; + + const PAGE = `https://example.com/document-builder.sjs?html=${encodeURIComponent( + '<iframe src="http://localhost:9222/json/version"></iframe>`' + )}`; + + await Page.enable(); + + const NAVIGATED = "Page.frameNavigated"; + + const history = new RecordEvents(2); + history.addRecorder({ + event: Page.frameNavigated, + eventName: NAVIGATED, + messageFn: payload => { + return `Received ${NAVIGATED} for frame id ${payload.frame.id}`; + }, + }); + + await loadURL(PAGE); + + const frameNavigatedEvents = await history.record(); + + const frames = frameNavigatedEvents + .map(({ payload }) => payload.frame) + .filter(frame => frame.parentId !== undefined); + + const windowGlobal = BrowsingContext.get(frames[0].id).currentWindowGlobal; + ok( + windowGlobal.documentURI.spec.startsWith("about:neterror?e=cspBlocked"), + "Expected page not be loaded within an iframe" + ); +}); + +async function requestJSON(path, options = {}) { + const { method = "GET", status = 200, json = true } = options; + + info(`${method} http://${RemoteAgent.debuggerAddress}${path}`); + + const response = await fetch(`http://${RemoteAgent.debuggerAddress}${path}`, { + method, + }); + + is(response.status, status, `JSON response is ${status}`); + + if (json) { + return response.json(); + } + + return response.text(); +} diff --git a/remote/cdp/test/browser/browser_interface.js b/remote/cdp/test/browser/browser_interface.js new file mode 100644 index 0000000000..d2e42362e1 --- /dev/null +++ b/remote/cdp/test/browser/browser_interface.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function navigator_webdriver({ client }) { + const { Runtime } = client; + + const url = toDataURL("default-test-page"); + await loadURL(url); + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "navigator.webdriver", + }); + + is(result.type, "boolean", "The returned type is correct"); + is(result.value, true, "navigator.webdriver is enabled"); +}); diff --git a/remote/cdp/test/browser/browser_main_target.js b/remote/cdp/test/browser/browser_main_target.js new file mode 100644 index 0000000000..b3b18b4c0c --- /dev/null +++ b/remote/cdp/test/browser/browser_main_target.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. + +add_task(async function ({ CDP }) { + const { mainProcessTarget } = RemoteAgent.cdp.targetList; + ok( + mainProcessTarget, + "The main process target is instantiated after the call to `listen`" + ); + + const targetURL = mainProcessTarget.wsDebuggerURL; + + const client = await CDP({ target: targetURL }); + info("CDP client has been instantiated"); + + try { + const { Browser, Target } = client; + ok(Browser, "The main process target exposes Browser domain"); + ok(Target, "The main process target exposes Target domain"); + + const version = await Browser.getVersion(); + + const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService( + Ci.nsIGfxInfo + ); + const expectedProduct = + (isHeadless ? "Headless" : "") + + `${Services.appinfo.name}/${Services.appinfo.version}`; + is(version.product, expectedProduct, "Browser.getVersion works"); + + is( + version.revision, + Services.appinfo.sourceURL.split("/").pop(), + "Browser.getVersion().revision is correct" + ); + + is( + version.jsVersion, + Services.appinfo.version, + "Browser.getVersion().jsVersion is correct" + ); + + const { webSocketDebuggerUrl } = await CDP.Version(); + is( + webSocketDebuggerUrl, + targetURL, + "Version endpoint refers to the same Main process target" + ); + } finally { + await client.close(); + } +}); diff --git a/remote/cdp/test/browser/browser_session.js b/remote/cdp/test/browser/browser_session.js new file mode 100644 index 0000000000..2afff10330 --- /dev/null +++ b/remote/cdp/test/browser/browser_session.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function ({ CDP }) { + const { webSocketDebuggerUrl } = await CDP.Version(); + const client = await CDP({ target: webSocketDebuggerUrl }); + + await Assert.rejects( + client.send("Hoobaflooba"), + /Invalid method format/, + `Fails with invalid method format` + ); + + await Assert.rejects( + client.send("Hooba.flooba"), + /UnknownMethodError/, + `Fails with UnknownMethodError` + ); + + await client.close(); +}); diff --git a/remote/cdp/test/browser/browser_tabs.js b/remote/cdp/test/browser/browser_tabs.js new file mode 100644 index 0000000000..9b0cd6ea96 --- /dev/null +++ b/remote/cdp/test/browser/browser_tabs.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. + +const TEST_URL = toDataURL("default-test-page"); + +add_task(async function ({ CDP }) { + // Use gBrowser.addTab instead of BrowserTestUtils as it creates the tab differently. + // It demonstrates a race around tab.linkedBrowser.browsingContext being undefined + // when accessing this property early. + const tab = gBrowser.addTab(TEST_URL, { + skipAnimation: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let targets = await getTargets(CDP); + ok( + targets.some(target => target.url == TEST_URL), + "Found the tab in target list" + ); + + BrowserTestUtils.removeTab(tab); + + targets = await getTargets(CDP); + ok( + !targets.some(target => target.url == TEST_URL), + "Tab has been removed from the target list" + ); +}); diff --git a/remote/cdp/test/browser/chrome-remote-interface.js b/remote/cdp/test/browser/chrome-remote-interface.js new file mode 100644 index 0000000000..2df9c701fe --- /dev/null +++ b/remote/cdp/test/browser/chrome-remote-interface.js @@ -0,0 +1,8 @@ +var CDP=function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=152)}([function(e,t,n){var r=n(2),i=n(19),o=n(12),a=n(13),s=n(20),c=function(e,t,n){var p,d,u,l,m=e&c.F,f=e&c.G,h=e&c.S,g=e&c.P,y=e&c.B,b=f?r:h?r[t]||(r[t]={}):(r[t]||{}).prototype,v=f?i:i[t]||(i[t]={}),w=v.prototype||(v.prototype={});for(p in f&&(n=t),n)u=((d=!m&&b&&void 0!==b[p])?b:n)[p],l=y&&d?s(u,r):g&&"function"==typeof u?s(Function.call,u):u,b&&a(b,p,u,e&c.U),v[p]!=u&&o(v,p,l),g&&w[p]!=u&&(w[p]=u)};r.core=i,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,t,n){var r=n(4);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(51)("wks"),i=n(36),o=n(2).Symbol,a="function"==typeof o;(e.exports=function(e){return r[e]||(r[e]=a&&o[e]||(a?o:i)("Symbol."+e))}).store=r},function(e,t,n){var r=n(22),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},function(e,t,n){e.exports=!n(3)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(1),i=n(103),o=n(24),a=Object.defineProperty;t.f=n(7)?Object.defineProperty:function(e,t,n){if(r(e),t=o(t,!0),r(n),i)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){var r=n(25);e.exports=function(e){return Object(r(e))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){var r=n(8),i=n(35);e.exports=n(7)?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){var r=n(2),i=n(12),o=n(15),a=n(36)("src"),s=n(156),c=(""+s).split("toString");n(19).inspectSource=function(e){return s.call(e)},(e.exports=function(e,t,n,s){var p="function"==typeof n;p&&(o(n,"name")||i(n,"name",t)),e[t]!==n&&(p&&(o(n,a)||i(n,a,e[t]?""+e[t]:c.join(String(t)))),e===r?e[t]=n:s?e[t]?e[t]=n:i(e,t,n):(delete e[t],i(e,t,n)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[a]||s.call(this)})},function(e,t,n){var r=n(0),i=n(3),o=n(25),a=/"/g,s=function(e,t,n,r){var i=String(o(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+i+"</"+t+">"};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*i(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(52),i=n(25);e.exports=function(e){return r(i(e))}},function(e,t,n){var r=n(53),i=n(35),o=n(16),a=n(24),s=n(15),c=n(103),p=Object.getOwnPropertyDescriptor;t.f=n(7)?p:function(e,t){if(e=o(e),t=a(t,!0),c)try{return p(e,t)}catch(e){}if(s(e,t))return i(!r.f.call(e,t),e[t])}},function(e,t,n){var r=n(15),i=n(9),o=n(78)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),r(e,o)?e[o]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},function(e,t){var n=e.exports={version:"2.6.9"};"number"==typeof __e&&(__e=n)},function(e,t,n){var r=n(10);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t,n){"use strict";var r=n(3);e.exports=function(e,t){return!!e&&r(function(){t?e.call(null,function(){},1):e.call(null)})}},function(e,t,n){var r=n(4);e.exports=function(e,t){if(!r(e))return e;var n,i;if(t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;if("function"==typeof(n=e.valueOf)&&!r(i=n.call(e)))return i;if(!t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(0),i=n(19),o=n(3);e.exports=function(e,t){var n=(i.Object||{})[e]||Object[e],a={};a[e]=t(n),r(r.S+r.F*o(function(){n(1)}),"Object",a)}},function(e,t,n){var r=n(20),i=n(52),o=n(9),a=n(6),s=n(94);e.exports=function(e,t){var n=1==e,c=2==e,p=3==e,d=4==e,u=6==e,l=5==e||u,m=t||s;return function(t,s,f){for(var h,g,y=o(t),b=i(y),v=r(s,f,3),w=a(b.length),S=0,x=n?m(t,w):c?m(t,0):void 0;w>S;S++)if((l||S in b)&&(g=v(h=b[S],S,y),e))if(n)x[S]=g;else if(g)switch(e){case 3:return!0;case 5:return h;case 6:return S;case 2:x.push(h)}else if(d)return!1;return u?-1:p||d?d:x}}},function(e,t,n){"use strict";if(n(7)){var r=n(31),i=n(2),o=n(3),a=n(0),s=n(69),c=n(102),p=n(20),d=n(42),u=n(35),l=n(12),m=n(44),f=n(22),h=n(6),g=n(131),y=n(38),b=n(24),v=n(15),w=n(47),S=n(4),x=n(9),I=n(91),T=n(39),R=n(18),k=n(40).f,C=n(93),O=n(36),$=n(5),E=n(27),D=n(59),j=n(55),P=n(96),A=n(49),M=n(64),N=n(41),_=n(95),L=n(120),q=n(8),F=n(17),U=q.f,B=F.f,W=i.RangeError,H=i.TypeError,z=i.Uint8Array,V=Array.prototype,G=c.ArrayBuffer,J=c.DataView,X=E(0),Y=E(2),K=E(3),Q=E(4),Z=E(5),ee=E(6),te=D(!0),ne=D(!1),re=P.values,ie=P.keys,oe=P.entries,ae=V.lastIndexOf,se=V.reduce,ce=V.reduceRight,pe=V.join,de=V.sort,ue=V.slice,le=V.toString,me=V.toLocaleString,fe=$("iterator"),he=$("toStringTag"),ge=O("typed_constructor"),ye=O("def_constructor"),be=s.CONSTR,ve=s.TYPED,we=s.VIEW,Se=E(1,function(e,t){return ke(j(e,e[ye]),t)}),xe=o(function(){return 1===new z(new Uint16Array([1]).buffer)[0]}),Ie=!!z&&!!z.prototype.set&&o(function(){new z(1).set({})}),Te=function(e,t){var n=f(e);if(n<0||n%t)throw W("Wrong offset!");return n},Re=function(e){if(S(e)&&ve in e)return e;throw H(e+" is not a typed array!")},ke=function(e,t){if(!(S(e)&&ge in e))throw H("It is not a typed array constructor!");return new e(t)},Ce=function(e,t){return Oe(j(e,e[ye]),t)},Oe=function(e,t){for(var n=0,r=t.length,i=ke(e,r);r>n;)i[n]=t[n++];return i},$e=function(e,t,n){U(e,t,{get:function(){return this._d[n]}})},Ee=function(e){var t,n,r,i,o,a,s=x(e),c=arguments.length,d=c>1?arguments[1]:void 0,u=void 0!==d,l=C(s);if(null!=l&&!I(l)){for(a=l.call(s),r=[],t=0;!(o=a.next()).done;t++)r.push(o.value);s=r}for(u&&c>2&&(d=p(d,arguments[2],2)),t=0,n=h(s.length),i=ke(this,n);n>t;t++)i[t]=u?d(s[t],t):s[t];return i},De=function(){for(var e=0,t=arguments.length,n=ke(this,t);t>e;)n[e]=arguments[e++];return n},je=!!z&&o(function(){me.call(new z(1))}),Pe=function(){return me.apply(je?ue.call(Re(this)):Re(this),arguments)},Ae={copyWithin:function(e,t){return L.call(Re(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return Q(Re(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return _.apply(Re(this),arguments)},filter:function(e){return Ce(this,Y(Re(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return Z(Re(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ee(Re(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){X(Re(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return ne(Re(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return te(Re(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return pe.apply(Re(this),arguments)},lastIndexOf:function(e){return ae.apply(Re(this),arguments)},map:function(e){return Se(Re(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return se.apply(Re(this),arguments)},reduceRight:function(e){return ce.apply(Re(this),arguments)},reverse:function(){for(var e,t=Re(this).length,n=Math.floor(t/2),r=0;r<n;)e=this[r],this[r++]=this[--t],this[t]=e;return this},some:function(e){return K(Re(this),e,arguments.length>1?arguments[1]:void 0)},sort:function(e){return de.call(Re(this),e)},subarray:function(e,t){var n=Re(this),r=n.length,i=y(e,r);return new(j(n,n[ye]))(n.buffer,n.byteOffset+i*n.BYTES_PER_ELEMENT,h((void 0===t?r:y(t,r))-i))}},Me=function(e,t){return Ce(this,ue.call(Re(this),e,t))},Ne=function(e){Re(this);var t=Te(arguments[1],1),n=this.length,r=x(e),i=h(r.length),o=0;if(i+t>n)throw W("Wrong length!");for(;o<i;)this[t+o]=r[o++]},_e={entries:function(){return oe.call(Re(this))},keys:function(){return ie.call(Re(this))},values:function(){return re.call(Re(this))}},Le=function(e,t){return S(e)&&e[ve]&&"symbol"!=typeof t&&t in e&&String(+t)==String(t)},qe=function(e,t){return Le(e,t=b(t,!0))?u(2,e[t]):B(e,t)},Fe=function(e,t,n){return!(Le(e,t=b(t,!0))&&S(n)&&v(n,"value"))||v(n,"get")||v(n,"set")||n.configurable||v(n,"writable")&&!n.writable||v(n,"enumerable")&&!n.enumerable?U(e,t,n):(e[t]=n.value,e)};be||(F.f=qe,q.f=Fe),a(a.S+a.F*!be,"Object",{getOwnPropertyDescriptor:qe,defineProperty:Fe}),o(function(){le.call({})})&&(le=me=function(){return pe.call(this)});var Ue=m({},Ae);m(Ue,_e),l(Ue,fe,_e.values),m(Ue,{slice:Me,set:Ne,constructor:function(){},toString:le,toLocaleString:Pe}),$e(Ue,"buffer","b"),$e(Ue,"byteOffset","o"),$e(Ue,"byteLength","l"),$e(Ue,"length","e"),U(Ue,he,{get:function(){return this[ve]}}),e.exports=function(e,t,n,c){var p=e+((c=!!c)?"Clamped":"")+"Array",u="get"+e,m="set"+e,f=i[p],y=f||{},b=f&&R(f),v=!f||!s.ABV,x={},I=f&&f.prototype,C=function(e,n){U(e,n,{get:function(){return function(e,n){var r=e._d;return r.v[u](n*t+r.o,xe)}(this,n)},set:function(e){return function(e,n,r){var i=e._d;c&&(r=(r=Math.round(r))<0?0:r>255?255:255&r),i.v[m](n*t+i.o,r,xe)}(this,n,e)},enumerable:!0})};v?(f=n(function(e,n,r,i){d(e,f,p,"_d");var o,a,s,c,u=0,m=0;if(S(n)){if(!(n instanceof G||"ArrayBuffer"==(c=w(n))||"SharedArrayBuffer"==c))return ve in n?Oe(f,n):Ee.call(f,n);o=n,m=Te(r,t);var y=n.byteLength;if(void 0===i){if(y%t)throw W("Wrong length!");if((a=y-m)<0)throw W("Wrong length!")}else if((a=h(i)*t)+m>y)throw W("Wrong length!");s=a/t}else s=g(n),o=new G(a=s*t);for(l(e,"_d",{b:o,o:m,l:a,e:s,v:new J(o)});u<s;)C(e,u++)}),I=f.prototype=T(Ue),l(I,"constructor",f)):o(function(){f(1)})&&o(function(){new f(-1)})&&M(function(e){new f,new f(null),new f(1.5),new f(e)},!0)||(f=n(function(e,n,r,i){var o;return d(e,f,p),S(n)?n instanceof G||"ArrayBuffer"==(o=w(n))||"SharedArrayBuffer"==o?void 0!==i?new y(n,Te(r,t),i):void 0!==r?new y(n,Te(r,t)):new y(n):ve in n?Oe(f,n):Ee.call(f,n):new y(g(n))}),X(b!==Function.prototype?k(y).concat(k(b)):k(y),function(e){e in f||l(f,e,y[e])}),f.prototype=I,r||(I.constructor=f));var O=I[fe],$=!!O&&("values"==O.name||null==O.name),E=_e.values;l(f,ge,!0),l(I,ve,p),l(I,we,!0),l(I,ye,f),(c?new f(1)[he]==p:he in I)||U(I,he,{get:function(){return p}}),x[p]=f,a(a.G+a.W+a.F*(f!=y),x),a(a.S,p,{BYTES_PER_ELEMENT:t}),a(a.S+a.F*o(function(){y.of.call(f,1)}),p,{from:Ee,of:De}),"BYTES_PER_ELEMENT"in I||l(I,"BYTES_PER_ELEMENT",t),a(a.P,p,Ae),N(p),a(a.P+a.F*Ie,p,{set:Ne}),a(a.P+a.F*!$,p,_e),r||I.toString==le||(I.toString=le),a(a.P+a.F*o(function(){new f(1).slice()}),p,{slice:Me}),a(a.P+a.F*(o(function(){return[1,2].toLocaleString()!=new f([1,2]).toLocaleString()})||!o(function(){I.toLocaleString.call([1,2])})),p,{toLocaleString:Pe}),A[p]=$?O:E,r||$||l(I,fe,E)}}else e.exports=function(){}},function(e,t,n){var r=n(126),i=n(0),o=n(51)("metadata"),a=o.store||(o.store=new(n(129))),s=function(e,t,n){var i=a.get(e);if(!i){if(!n)return;a.set(e,i=new r)}var o=i.get(t);if(!o){if(!n)return;i.set(t,o=new r)}return o};e.exports={store:a,map:s,has:function(e,t,n){var r=s(t,n,!1);return void 0!==r&&r.has(e)},get:function(e,t,n){var r=s(t,n,!1);return void 0===r?void 0:r.get(e)},set:function(e,t,n,r){s(n,r,!0).set(e,t)},keys:function(e,t){var n=s(e,t,!1),r=[];return n&&n.forEach(function(e,t){r.push(t)}),r},key:function(e){return void 0===e||"symbol"==typeof e?e:String(e)},exp:function(e){i(i.S,"Reflect",e)}}},function(e,t){var n,r,i=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===o||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:o}catch(e){n=o}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var c,p=[],d=!1,u=-1;function l(){d&&c&&(d=!1,c.length?p=c.concat(p):u=-1,p.length&&m())}function m(){if(!d){var e=s(l);d=!0;for(var t=p.length;t;){for(c=p,p=[];++u<t;)c&&c[u].run();u=-1,t=p.length}c=null,d=!1,function(e){if(r===clearTimeout)return clearTimeout(e);if((r===a||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(e);try{r(e)}catch(t){try{return r.call(null,e)}catch(t){return r.call(this,e)}}}(e)}}function f(e,t){this.fun=e,this.array=t}function h(){}i.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];p.push(new f(e,t)),1!==p.length||d||s(m)},f.prototype.run=function(){this.fun.apply(null,this.array)},i.title="browser",i.browser=!0,i.env={},i.argv=[],i.version="",i.versions={},i.on=h,i.addListener=h,i.once=h,i.off=h,i.removeListener=h,i.removeAllListeners=h,i.emit=h,i.prependListener=h,i.prependOnceListener=h,i.listeners=function(e){return[]},i.binding=function(e){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(e){throw new Error("process.chdir is not supported")},i.umask=function(){return 0}},function(e,t){e.exports=!1},function(e,t,n){var r=n(36)("meta"),i=n(4),o=n(15),a=n(8).f,s=0,c=Object.isExtensible||function(){return!0},p=!n(3)(function(){return c(Object.preventExtensions({}))}),d=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},u=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,r)){if(!c(e))return"F";if(!t)return"E";d(e)}return e[r].i},getWeak:function(e,t){if(!o(e,r)){if(!c(e))return!0;if(!t)return!1;d(e)}return e[r].w},onFreeze:function(e){return p&&u.NEED&&c(e)&&!o(e,r)&&d(e),e}}},function(e,t,n){var r=n(5)("unscopables"),i=Array.prototype;null==i[r]&&n(12)(i,r,{}),e.exports=function(e){i[r][e]=!0}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(105),i=n(79);e.exports=Object.keys||function(e){return r(e,i)}},function(e,t,n){var r=n(22),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=r(e))<0?i(e+t,0):o(e,t)}},function(e,t,n){var r=n(1),i=n(106),o=n(79),a=n(78)("IE_PROTO"),s=function(){},c=function(){var e,t=n(76)("iframe"),r=o.length;for(t.style.display="none",n(80).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("<script>document.F=Object<\/script>"),e.close(),c=e.F;r--;)delete c.prototype[o[r]];return c()};e.exports=Object.create||function(e,t){var n;return null!==e?(s.prototype=r(e),n=new s,s.prototype=null,n[a]=e):n=c(),void 0===t?n:i(n,t)}},function(e,t,n){var r=n(105),i=n(79).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},function(e,t,n){"use strict";var r=n(2),i=n(8),o=n(7),a=n(5)("species");e.exports=function(e){var t=r[e];o&&t&&!t[a]&&i.f(t,a,{configurable:!0,get:function(){return this}})}},function(e,t){e.exports=function(e,t,n,r){if(!(e instanceof t)||void 0!==r&&r in e)throw TypeError(n+": incorrect invocation!");return e}},function(e,t,n){var r=n(20),i=n(118),o=n(91),a=n(1),s=n(6),c=n(93),p={},d={};(t=e.exports=function(e,t,n,u,l){var m,f,h,g,y=l?function(){return e}:c(e),b=r(n,u,t?2:1),v=0;if("function"!=typeof y)throw TypeError(e+" is not iterable!");if(o(y)){for(m=s(e.length);m>v;v++)if((g=t?b(a(f=e[v])[0],f[1]):b(e[v]))===p||g===d)return g}else for(h=y.call(e);!(f=h.next()).done;)if((g=i(h,b,f.value,t))===p||g===d)return g}).BREAK=p,t.RETURN=d},function(e,t,n){var r=n(13);e.exports=function(e,t,n){for(var i in t)r(e,i,t[i],n);return e}},function(e,t,n){var r=n(4);e.exports=function(e,t){if(!r(e)||e._t!==t)throw TypeError("Incompatible receiver, "+t+" required!");return e}},function(e,t,n){var r=n(8).f,i=n(15),o=n(5)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,o)&&r(e,o,{configurable:!0,value:t})}},function(e,t,n){var r=n(21),i=n(5)("toStringTag"),o="Arguments"==r(function(){return arguments}());e.exports=function(e){var t,n,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?n:o?r(t):"Object"==(a=r(t))&&"function"==typeof t.callee?"Arguments":a}},function(e,t,n){var r=n(0),i=n(25),o=n(3),a=n(82),s="["+a+"]",c=RegExp("^"+s+s+"*"),p=RegExp(s+s+"*$"),d=function(e,t,n){var i={},s=o(function(){return!!a[e]()||"
"!="
"[e]()}),c=i[e]=s?t(u):a[e];n&&(i[n]=c),r(r.P+r.F*s,"String",i)},u=d.trim=function(e,t){return e=String(i(e)),1&t&&(e=e.replace(c,"")),2&t&&(e=e.replace(p,"")),e};e.exports=d},function(e,t){e.exports={}},function(e,t,n){"use strict";var r=n(73),i=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=u;var o=n(58);o.inherits=n(34);var a=n(145),s=n(148);o.inherits(u,a);for(var c=i(s.prototype),p=0;p<c.length;p++){var d=c[p];u.prototype[d]||(u.prototype[d]=s.prototype[d])}function u(e){if(!(this instanceof u))return new u(e);a.call(this,e),s.call(this,e),e&&!1===e.readable&&(this.readable=!1),e&&!1===e.writable&&(this.writable=!1),this.allowHalfOpen=!0,e&&!1===e.allowHalfOpen&&(this.allowHalfOpen=!1),this.once("end",l)}function l(){this.allowHalfOpen||this._writableState.ended||r.nextTick(m,this)}function m(e){e.end()}Object.defineProperty(u.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),Object.defineProperty(u.prototype,"destroyed",{get:function(){return void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed&&this._writableState.destroyed)},set:function(e){void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed=e,this._writableState.destroyed=e)}}),u.prototype._destroy=function(e,t){this.push(null),this.end(),r.nextTick(t,e)}},function(e,t,n){var r=n(19),i=n(2),o=i["__core-js_shared__"]||(i["__core-js_shared__"]={});(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:r.version,mode:n(31)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(e,t,n){var r=n(21);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,n){"use strict";var r=n(1);e.exports=function(){var e=r(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},function(e,t,n){var r=n(1),i=n(10),o=n(5)("species");e.exports=function(e,t){var n,a=r(e).constructor;return void 0===a||null==(n=r(a)[o])?t:i(n)}},function(e,t,n){"use strict";var r,i="object"==typeof Reflect?Reflect:null,o=i&&"function"==typeof i.apply?i.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};r=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var a=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var c=10;function p(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function d(e,t,n,r){var i,o,a,s;if("function"!=typeof n)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof n);if(void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),o=e._events),a=o[t]),void 0===a)a=o[t]=n,++e._eventsCount;else if("function"==typeof a?a=o[t]=r?[n,a]:[a,n]:r?a.unshift(n):a.push(n),(i=p(e))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=a.length,s=c,console&&console.warn&&console.warn(s)}return e}function u(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=function(){for(var e=[],t=0;t<arguments.length;t++)e.push(arguments[t]);this.fired||(this.target.removeListener(this.type,this.wrapFn),this.fired=!0,o(this.listener,this.target,e))}.bind(r);return i.listener=n,r.wrapFn=i,i}function l(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n<t.length;++n)t[n]=e[n].listener||e[n];return t}(i):f(i,i.length)}function m(e){var t=this._events;if(void 0!==t){var n=t[e];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function f(e,t){for(var n=new Array(t),r=0;r<t;++r)n[r]=e[r];return n}Object.defineProperty(s,"defaultMaxListeners",{enumerable:!0,get:function(){return c},set:function(e){if("number"!=typeof e||e<0||a(e))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+e+".");c=e}}),s.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},s.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||a(e))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+e+".");return this._maxListeners=e,this},s.prototype.getMaxListeners=function(){return p(this)},s.prototype.emit=function(e){for(var t=[],n=1;n<arguments.length;n++)t.push(arguments[n]);var r="error"===e,i=this._events;if(void 0!==i)r=r&&void 0===i.error;else if(!r)return!1;if(r){var a;if(t.length>0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var c=i[e];if(void 0===c)return!1;if("function"==typeof c)o(c,this,t);else{var p=c.length,d=f(c,p);for(n=0;n<p;++n)o(d[n],this,t)}return!0},s.prototype.addListener=function(e,t){return d(this,e,t,!1)},s.prototype.on=s.prototype.addListener,s.prototype.prependListener=function(e,t){return d(this,e,t,!0)},s.prototype.once=function(e,t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);return this.on(e,u(this,e,t)),this},s.prototype.prependOnceListener=function(e,t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);return this.prependListener(e,u(this,e,t)),this},s.prototype.removeListener=function(e,t){var n,r,i,o,a;if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);if(void 0===(r=this._events))return this;if(void 0===(n=r[e]))return this;if(n===t||n.listener===t)0==--this._eventsCount?this._events=Object.create(null):(delete r[e],r.removeListener&&this.emit("removeListener",e,n.listener||t));else if("function"!=typeof n){for(i=-1,o=n.length-1;o>=0;o--)if(n[o]===t||n[o].listener===t){a=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1<e.length;t++)e[t]=e[t+1];e.pop()}(n,i),1===n.length&&(r[e]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",e,a||t)}return this},s.prototype.off=s.prototype.removeListener,s.prototype.removeAllListeners=function(e){var t,n,r;if(void 0===(n=this._events))return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[e]),this;if(0===arguments.length){var i,o=Object.keys(n);for(r=0;r<o.length;++r)"removeListener"!==(i=o[r])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(t=n[e]))this.removeListener(e,t);else if(void 0!==t)for(r=t.length-1;r>=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return l(this,e,!0)},s.prototype.rawListeners=function(e){return l(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},s.prototype.listenerCount=m,s.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){"use strict";(function(e){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org> + * @license MIT + */ +var r=n(357),i=n(358),o=n(141);function a(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()<t)throw new RangeError("Invalid typed array length");return c.TYPED_ARRAY_SUPPORT?(e=new Uint8Array(t)).__proto__=c.prototype:(null===e&&(e=new c(t)),e.length=t),e}function c(e,t,n){if(!(c.TYPED_ARRAY_SUPPORT||this instanceof c))return new c(e,t,n);if("number"==typeof e){if("string"==typeof t)throw new Error("If encoding is specified then the first argument must be a string");return u(this,e)}return p(this,e,t,n)}function p(e,t,n,r){if("number"==typeof t)throw new TypeError('"value" argument must not be a number');return"undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer?function(e,t,n,r){if(t.byteLength,n<0||t.byteLength<n)throw new RangeError("'offset' is out of bounds");if(t.byteLength<n+(r||0))throw new RangeError("'length' is out of bounds");t=void 0===n&&void 0===r?new Uint8Array(t):void 0===r?new Uint8Array(t,n):new Uint8Array(t,n,r);c.TYPED_ARRAY_SUPPORT?(e=t).__proto__=c.prototype:e=l(e,t);return e}(e,t,n,r):"string"==typeof t?function(e,t,n){"string"==typeof n&&""!==n||(n="utf8");if(!c.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|f(t,n),i=(e=s(e,r)).write(t,n);i!==r&&(e=e.slice(0,i));return e}(e,t,n):function(e,t){if(c.isBuffer(t)){var n=0|m(t.length);return 0===(e=s(e,n)).length?e:(t.copy(e,0,0,n),e)}if(t){if("undefined"!=typeof ArrayBuffer&&t.buffer instanceof ArrayBuffer||"length"in t)return"number"!=typeof t.length||(r=t.length)!=r?s(e,0):l(e,t);if("Buffer"===t.type&&o(t.data))return l(e,t.data)}var r;throw new TypeError("First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.")}(e,t)}function d(e){if("number"!=typeof e)throw new TypeError('"size" argument must be a number');if(e<0)throw new RangeError('"size" argument must not be negative')}function u(e,t){if(d(t),e=s(e,t<0?0:0|m(t)),!c.TYPED_ARRAY_SUPPORT)for(var n=0;n<t;++n)e[n]=0;return e}function l(e,t){var n=t.length<0?0:0|m(t.length);e=s(e,n);for(var r=0;r<n;r+=1)e[r]=255&t[r];return e}function m(e){if(e>=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function f(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return U(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return B(e).length;default:if(r)return U(e).length;t=(""+t).toLowerCase(),r=!0}}function h(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return E(this,t,n);case"utf8":case"utf-8":return k(this,t,n);case"ascii":return O(this,t,n);case"latin1":case"binary":return $(this,t,n);case"base64":return R(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return D(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,i){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:b(e,t,n,r,i);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):b(e,[t],n,r,i);throw new TypeError("val must be string, number or Buffer")}function b(e,t,n,r,i){var o,a=1,s=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,c/=2,n/=2}function p(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(i){var d=-1;for(o=n;o<s;o++)if(p(e,o)===p(t,-1===d?0:o-d)){if(-1===d&&(d=o),o-d+1===c)return d*a}else-1!==d&&(o-=o-d),d=-1}else for(n+c>s&&(n=s-c),o=n;o>=0;o--){for(var u=!0,l=0;l<c;l++)if(p(e,o+l)!==p(t,l)){u=!1;break}if(u)return o}return-1}function v(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r))>i&&(r=i):r=i;var o=t.length;if(o%2!=0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;a<r;++a){var s=parseInt(t.substr(2*a,2),16);if(isNaN(s))return a;e[n+a]=s}return a}function w(e,t,n,r){return W(U(t,e.length-n),e,n,r)}function S(e,t,n,r){return W(function(e){for(var t=[],n=0;n<e.length;++n)t.push(255&e.charCodeAt(n));return t}(t),e,n,r)}function x(e,t,n,r){return S(e,t,n,r)}function I(e,t,n,r){return W(B(t),e,n,r)}function T(e,t,n,r){return W(function(e,t){for(var n,r,i,o=[],a=0;a<e.length&&!((t-=2)<0);++a)n=e.charCodeAt(a),r=n>>8,i=n%256,o.push(i),o.push(r);return o}(t,e.length-n),e,n,r)}function R(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function k(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;i<n;){var o,a,s,c,p=e[i],d=null,u=p>239?4:p>223?3:p>191?2:1;if(i+u<=n)switch(u){case 1:p<128&&(d=p);break;case 2:128==(192&(o=e[i+1]))&&(c=(31&p)<<6|63&o)>127&&(d=c);break;case 3:o=e[i+1],a=e[i+2],128==(192&o)&&128==(192&a)&&(c=(15&p)<<12|(63&o)<<6|63&a)>2047&&(c<55296||c>57343)&&(d=c);break;case 4:o=e[i+1],a=e[i+2],s=e[i+3],128==(192&o)&&128==(192&a)&&128==(192&s)&&(c=(15&p)<<18|(63&o)<<12|(63&a)<<6|63&s)>65535&&c<1114112&&(d=c)}null===d?(d=65533,u=1):d>65535&&(d-=65536,r.push(d>>>10&1023|55296),d=56320|1023&d),r.push(d),i+=u}return function(e){var t=e.length;if(t<=C)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r<t;)n+=String.fromCharCode.apply(String,e.slice(r,r+=C));return n}(r)}t.Buffer=c,t.SlowBuffer=function(e){+e!=e&&(e=0);return c.alloc(+e)},t.INSPECT_MAX_BYTES=50,c.TYPED_ARRAY_SUPPORT=void 0!==e.TYPED_ARRAY_SUPPORT?e.TYPED_ARRAY_SUPPORT:function(){try{var e=new Uint8Array(1);return e.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(e){return!1}}(),t.kMaxLength=a(),c.poolSize=8192,c._augment=function(e){return e.__proto__=c.prototype,e},c.from=function(e,t,n){return p(null,e,t,n)},c.TYPED_ARRAY_SUPPORT&&(c.prototype.__proto__=Uint8Array.prototype,c.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&c[Symbol.species]===c&&Object.defineProperty(c,Symbol.species,{value:null,configurable:!0})),c.alloc=function(e,t,n){return function(e,t,n,r){return d(t),t<=0?s(e,t):void 0!==n?"string"==typeof r?s(e,t).fill(n,r):s(e,t).fill(n):s(e,t)}(null,e,t,n)},c.allocUnsafe=function(e){return u(null,e)},c.allocUnsafeSlow=function(e){return u(null,e)},c.isBuffer=function(e){return!(null==e||!e._isBuffer)},c.compare=function(e,t){if(!c.isBuffer(e)||!c.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);i<o;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return n<r?-1:r<n?1:0},c.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},c.concat=function(e,t){if(!o(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return c.alloc(0);var n;if(void 0===t)for(t=0,n=0;n<e.length;++n)t+=e[n].length;var r=c.allocUnsafe(t),i=0;for(n=0;n<e.length;++n){var a=e[n];if(!c.isBuffer(a))throw new TypeError('"list" argument must be an Array of Buffers');a.copy(r,i),i+=a.length}return r},c.byteLength=f,c.prototype._isBuffer=!0,c.prototype.swap16=function(){var e=this.length;if(e%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(var t=0;t<e;t+=2)g(this,t,t+1);return this},c.prototype.swap32=function(){var e=this.length;if(e%4!=0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;t<e;t+=4)g(this,t,t+3),g(this,t+1,t+2);return this},c.prototype.swap64=function(){var e=this.length;if(e%8!=0)throw new RangeError("Buffer size must be a multiple of 64-bits");for(var t=0;t<e;t+=8)g(this,t,t+7),g(this,t+1,t+6),g(this,t+2,t+5),g(this,t+3,t+4);return this},c.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?k(this,0,e):h.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),"<Buffer "+e+">"},c.prototype.compare=function(e,t,n,r,i){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),t<0||n>e.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(this===e)return 0;for(var o=(i>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(o,a),p=this.slice(r,i),d=e.slice(t,n),u=0;u<s;++u)if(p[u]!==d[u]){o=p[u],a=d[u];break}return o<a?-1:a<o?1:0},c.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},c.prototype.indexOf=function(e,t,n){return y(this,e,t,n,!0)},c.prototype.lastIndexOf=function(e,t,n){return y(this,e,t,n,!1)},c.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t|=0,isFinite(n)?(n|=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return v(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":return S(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return I(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var C=4096;function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;i<n;++i)r+=String.fromCharCode(127&e[i]);return r}function $(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;i<n;++i)r+=String.fromCharCode(e[i]);return r}function E(e,t,n){var r=e.length;(!t||t<0)&&(t=0),(!n||n<0||n>r)&&(n=r);for(var i="",o=t;o<n;++o)i+=F(e[o]);return i}function D(e,t,n){for(var r=e.slice(t,n),i="",o=0;o<r.length;o+=2)i+=String.fromCharCode(r[o]+256*r[o+1]);return i}function j(e,t,n){if(e%1!=0||e<0)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function P(e,t,n,r,i,o){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||t<o)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function A(e,t,n,r){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);i<o;++i)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function M(e,t,n,r){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);i<o;++i)e[n+i]=t>>>8*(r?i:3-i)&255}function N(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function _(e,t,n,r,o){return o||N(e,0,n,4),i.write(e,t,n,r,23,4),n+4}function L(e,t,n,r,o){return o||N(e,0,n,8),i.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t<e&&(t=e),c.TYPED_ARRAY_SUPPORT)(n=this.subarray(e,t)).__proto__=c.prototype;else{var i=t-e;n=new c(i,void 0);for(var o=0;o<i;++o)n[o]=this[o+e]}return n},c.prototype.readUIntLE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return r},c.prototype.readUIntBE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e+--t],i=1;t>0&&(i*=256);)r+=this[e+--t]*i;return r},c.prototype.readUInt8=function(e,t){return t||j(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||j(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||j(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||j(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||j(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return r>=(i*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*t)),o},c.prototype.readInt8=function(e,t){return t||j(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||j(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||j(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||j(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||j(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||j(e,4,this.length),i.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||j(e,4,this.length),i.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||j(e,8,this.length),i.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||j(e,8,this.length),i.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||P(this,e,t,n,Math.pow(2,8*n)-1,0);var i=1,o=0;for(this[t]=255&e;++o<n&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUIntBE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||P(this,e,t,n,Math.pow(2,8*n)-1,0);var i=n-1,o=1;for(this[t+i]=255&e;--i>=0&&(o*=256);)this[t+i]=e/o&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):A(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):A(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):M(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=0,a=1,s=0;for(this[t]=255&e;++o<n&&(a*=256);)e<0&&0===s&&0!==this[t+o-1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=n-1,a=1,s=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)e<0&&0===s&&0!==this[t+o+1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):A(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):A(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):M(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return L(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return L(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r<n&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(t<0)throw new RangeError("targetStart out of bounds");if(n<0||n>=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t<r-n&&(r=e.length-t+n);var i,o=r-n;if(this===e&&n<t&&t<r)for(i=o-1;i>=0;--i)e[i+t]=this[i+n];else if(o<1e3||!c.TYPED_ARRAY_SUPPORT)for(i=0;i<o;++i)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},c.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);i<256&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!c.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e&=255);if(t<0||this.length<t||this.length<n)throw new RangeError("Out of range index");if(n<=t)return this;var o;if(t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(o=t;o<n;++o)this[o]=e;else{var a=c.isBuffer(e)?e:U(new c(e,r).toString()),s=a.length;for(o=0;o<n-t;++o)this[o+t]=a[o%s]}return this};var q=/[^+\/0-9A-Za-z-_]/g;function F(e){return e<16?"0"+e.toString(16):e.toString(16)}function U(e,t){var n;t=t||1/0;for(var r=e.length,i=null,o=[],a=0;a<r;++a){if((n=e.charCodeAt(a))>55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=65536+(i-55296<<10|n-56320)}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;o.push(n)}else if(n<2048){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function B(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(q,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function W(e,t,n,r){for(var i=0;i<r&&!(i+n>=t.length||i>=e.length);++i)t[i+n]=e[i];return i}}).call(this,n(11))},function(e,t,n){(function(e){function n(e){return Object.prototype.toString.call(e)}t.isArray=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===n(e)},t.isBoolean=function(e){return"boolean"==typeof e},t.isNull=function(e){return null===e},t.isNullOrUndefined=function(e){return null==e},t.isNumber=function(e){return"number"==typeof e},t.isString=function(e){return"string"==typeof e},t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=function(e){return void 0===e},t.isRegExp=function(e){return"[object RegExp]"===n(e)},t.isObject=function(e){return"object"==typeof e&&null!==e},t.isDate=function(e){return"[object Date]"===n(e)},t.isError=function(e){return"[object Error]"===n(e)||e instanceof Error},t.isFunction=function(e){return"function"==typeof e},t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=e.isBuffer}).call(this,n(57).Buffer)},function(e,t,n){var r=n(16),i=n(6),o=n(38);e.exports=function(e){return function(t,n,a){var s,c=r(t),p=i(c.length),d=o(a,p);if(e&&n!=n){for(;p>d;)if((s=c[d++])!=s)return!0}else for(;p>d;d++)if((e||d in c)&&c[d]===n)return e||d||0;return!e&&-1}}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var r=n(21);e.exports=Array.isArray||function(e){return"Array"==r(e)}},function(e,t,n){var r=n(22),i=n(25);e.exports=function(e){return function(t,n){var o,a,s=String(i(t)),c=r(n),p=s.length;return c<0||c>=p?e?"":void 0:(o=s.charCodeAt(c))<55296||o>56319||c+1===p||(a=s.charCodeAt(c+1))<56320||a>57343?e?s.charAt(c):o:e?s.slice(c,c+2):a-56320+(o-55296<<10)+65536}}},function(e,t,n){var r=n(4),i=n(21),o=n(5)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},function(e,t,n){var r=n(5)("iterator"),i=!1;try{var o=[7][r]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var o=[7],a=o[r]();a.next=function(){return{done:n=!0}},o[r]=function(){return a},e(o)}catch(e){}return n}},function(e,t,n){"use strict";var r=n(47),i=RegExp.prototype.exec;e.exports=function(e,t){var n=e.exec;if("function"==typeof n){var o=n.call(e,t);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==r(e))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(e,t)}},function(e,t,n){"use strict";n(122);var r=n(13),i=n(12),o=n(3),a=n(25),s=n(5),c=n(97),p=s("species"),d=!o(function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$<a>")}),u=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();e.exports=function(e,t,n){var l=s(e),m=!o(function(){var t={};return t[l]=function(){return 7},7!=""[e](t)}),f=m?!o(function(){var t=!1,n=/a/;return n.exec=function(){return t=!0,null},"split"===e&&(n.constructor={},n.constructor[p]=function(){return n}),n[l](""),!t}):void 0;if(!m||!f||"replace"===e&&!d||"split"===e&&!u){var h=/./[l],g=n(a,l,""[e],function(e,t,n,r,i){return t.exec===c?m&&!i?{done:!0,value:h.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),y=g[0],b=g[1];r(String.prototype,e,y),i(RegExp.prototype,l,2==t?function(e,t){return b.call(e,this,t)}:function(e){return b.call(e,this)})}}},function(e,t,n){var r=n(2).navigator;e.exports=r&&r.userAgent||""},function(e,t,n){"use strict";var r=n(2),i=n(0),o=n(13),a=n(44),s=n(32),c=n(43),p=n(42),d=n(4),u=n(3),l=n(64),m=n(46),f=n(83);e.exports=function(e,t,n,h,g,y){var b=r[e],v=b,w=g?"set":"add",S=v&&v.prototype,x={},I=function(e){var t=S[e];o(S,e,"delete"==e?function(e){return!(y&&!d(e))&&t.call(this,0===e?0:e)}:"has"==e?function(e){return!(y&&!d(e))&&t.call(this,0===e?0:e)}:"get"==e?function(e){return y&&!d(e)?void 0:t.call(this,0===e?0:e)}:"add"==e?function(e){return t.call(this,0===e?0:e),this}:function(e,n){return t.call(this,0===e?0:e,n),this})};if("function"==typeof v&&(y||S.forEach&&!u(function(){(new v).entries().next()}))){var T=new v,R=T[w](y?{}:-0,1)!=T,k=u(function(){T.has(1)}),C=l(function(e){new v(e)}),O=!y&&u(function(){for(var e=new v,t=5;t--;)e[w](t,t);return!e.has(-0)});C||((v=t(function(t,n){p(t,v,e);var r=f(new b,t,v);return null!=n&&c(n,g,r[w],r),r})).prototype=S,S.constructor=v),(k||O)&&(I("delete"),I("has"),g&&I("get")),(O||R)&&I(w),y&&S.clear&&delete S.clear}else v=h.getConstructor(t,e,g,w),a(v.prototype,n),s.NEED=!0;return m(v,e),x[e]=v,i(i.G+i.W+i.F*(v!=b),x),y||h.setStrong(v,e,g),v}},function(e,t,n){for(var r,i=n(2),o=n(12),a=n(36),s=a("typed_array"),c=a("view"),p=!(!i.ArrayBuffer||!i.DataView),d=p,u=0,l="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");u<9;)(r=i[l[u++]])?(o(r.prototype,s,!0),o(r.prototype,c,!0)):d=!1;e.exports={ABV:p,CONSTR:d,TYPED:s,VIEW:c}},function(e,t,n){"use strict";e.exports=n(31)||!n(3)(function(){var e=Math.random();__defineSetter__.call(null,e,function(){}),delete n(2)[e]})},function(e,t,n){"use strict";var r=n(0);e.exports=function(e){r(r.S,e,{of:function(){for(var e=arguments.length,t=new Array(e);e--;)t[e]=arguments[e];return new this(t)}})}},function(e,t,n){"use strict";var r=n(0),i=n(10),o=n(20),a=n(43);e.exports=function(e){r(r.S,e,{from:function(e){var t,n,r,s,c=arguments[1];return i(this),(t=void 0!==c)&&i(c),null==e?new this:(n=[],t?(r=0,s=o(c,arguments[2],2),a(e,!1,function(e){n.push(s(e,r++))})):a(e,!1,n.push,n),new this(n))}})}},function(e,t,n){"use strict";(function(t){void 0===t||!t.version||0===t.version.indexOf("v0.")||0===t.version.indexOf("v1.")&&0!==t.version.indexOf("v1.8.")?e.exports={nextTick:function(e,n,r,i){if("function"!=typeof e)throw new TypeError('"callback" argument must be a function');var o,a,s=arguments.length;switch(s){case 0:case 1:return t.nextTick(e);case 2:return t.nextTick(function(){e.call(null,n)});case 3:return t.nextTick(function(){e.call(null,n,r)});case 4:return t.nextTick(function(){e.call(null,n,r,i)});default:for(o=new Array(s-1),a=0;a<o.length;)o[a++]=arguments[a];return t.nextTick(function(){e.apply(null,o)})}}}:e.exports=t}).call(this,n(30))},function(e,t,n){var r=n(57),i=r.Buffer;function o(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return i(e,t,n)}i.from&&i.alloc&&i.allocUnsafe&&i.allocUnsafeSlow?e.exports=r:(o(r,t),t.Buffer=a),o(i,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return i(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=i(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return i(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){"use strict";var r=n(369),i=n(371);function o(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}t.parse=v,t.resolve=function(e,t){return v(e,!1,!0).resolve(t)},t.resolveObject=function(e,t){return e?v(e,!1,!0).resolveObject(t):t},t.format=function(e){i.isString(e)&&(e=v(e));return e instanceof o?e.format():o.prototype.format.call(e)},t.Url=o;var a=/^([a-z0-9.+-]+:)/i,s=/:[0-9]*$/,c=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,p=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),d=["'"].concat(p),u=["%","/","?",";","#"].concat(d),l=["/","?","#"],m=/^[+a-z0-9A-Z_-]{0,63}$/,f=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,h={javascript:!0,"javascript:":!0},g={javascript:!0,"javascript:":!0},y={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},b=n(372);function v(e,t,n){if(e&&i.isObject(e)&&e instanceof o)return e;var r=new o;return r.parse(e,t,n),r}o.prototype.parse=function(e,t,n){if(!i.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),s=-1!==o&&o<e.indexOf("#")?"?":"#",p=e.split(s);p[0]=p[0].replace(/\\/g,"/");var v=e=p.join(s);if(v=v.trim(),!n&&1===e.split("#").length){var w=c.exec(v);if(w)return this.path=v,this.href=v,this.pathname=w[1],w[2]?(this.search=w[2],this.query=t?b.parse(this.search.substr(1)):this.search.substr(1)):t&&(this.search="",this.query={}),this}var S=a.exec(v);if(S){var x=(S=S[0]).toLowerCase();this.protocol=x,v=v.substr(S.length)}if(n||S||v.match(/^\/\/[^@\/]+@[^@\/]+/)){var I="//"===v.substr(0,2);!I||S&&g[S]||(v=v.substr(2),this.slashes=!0)}if(!g[S]&&(I||S&&!y[S])){for(var T,R,k=-1,C=0;C<l.length;C++){-1!==(O=v.indexOf(l[C]))&&(-1===k||O<k)&&(k=O)}-1!==(R=-1===k?v.lastIndexOf("@"):v.lastIndexOf("@",k))&&(T=v.slice(0,R),v=v.slice(R+1),this.auth=decodeURIComponent(T)),k=-1;for(C=0;C<u.length;C++){var O;-1!==(O=v.indexOf(u[C]))&&(-1===k||O<k)&&(k=O)}-1===k&&(k=v.length),this.host=v.slice(0,k),v=v.slice(k),this.parseHost(),this.hostname=this.hostname||"";var $="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!$)for(var E=this.hostname.split(/\./),D=(C=0,E.length);C<D;C++){var j=E[C];if(j&&!j.match(m)){for(var P="",A=0,M=j.length;A<M;A++)j.charCodeAt(A)>127?P+="x":P+=j[A];if(!P.match(m)){var N=E.slice(0,C),_=E.slice(C+1),L=j.match(f);L&&(N.push(L[1]),_.unshift(L[2])),_.length&&(v="/"+_.join(".")+v),this.hostname=N.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),$||(this.hostname=r.toASCII(this.hostname));var q=this.port?":"+this.port:"",F=this.hostname||"";this.host=F+q,this.href+=this.host,$&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==v[0]&&(v="/"+v))}if(!h[x])for(C=0,D=d.length;C<D;C++){var U=d[C];if(-1!==v.indexOf(U)){var B=encodeURIComponent(U);B===U&&(B=escape(U)),v=v.split(U).join(B)}}var W=v.indexOf("#");-1!==W&&(this.hash=v.substr(W),v=v.slice(0,W));var H=v.indexOf("?");if(-1!==H?(this.search=v.substr(H),this.query=v.substr(H+1),t&&(this.query=b.parse(this.query)),v=v.slice(0,H)):t&&(this.search="",this.query={}),v&&(this.pathname=v),y[x]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){q=this.pathname||"";var z=this.search||"";this.path=q+z}return this.href=this.format(),this},o.prototype.format=function(){var e=this.auth||"";e&&(e=(e=encodeURIComponent(e)).replace(/%3A/i,":"),e+="@");var t=this.protocol||"",n=this.pathname||"",r=this.hash||"",o=!1,a="";this.host?o=e+this.host:this.hostname&&(o=e+(-1===this.hostname.indexOf(":")?this.hostname:"["+this.hostname+"]"),this.port&&(o+=":"+this.port)),this.query&&i.isObject(this.query)&&Object.keys(this.query).length&&(a=b.stringify(this.query));var s=this.search||a&&"?"+a||"";return t&&":"!==t.substr(-1)&&(t+=":"),this.slashes||(!t||y[t])&&!1!==o?(o="//"+(o||""),n&&"/"!==n.charAt(0)&&(n="/"+n)):o||(o=""),r&&"#"!==r.charAt(0)&&(r="#"+r),s&&"?"!==s.charAt(0)&&(s="?"+s),t+o+(n=n.replace(/[?#]/g,function(e){return encodeURIComponent(e)}))+(s=s.replace("#","%23"))+r},o.prototype.resolve=function(e){return this.resolveObject(v(e,!1,!0)).format()},o.prototype.resolveObject=function(e){if(i.isString(e)){var t=new o;t.parse(e,!1,!0),e=t}for(var n=new o,r=Object.keys(this),a=0;a<r.length;a++){var s=r[a];n[s]=this[s]}if(n.hash=e.hash,""===e.href)return n.href=n.format(),n;if(e.slashes&&!e.protocol){for(var c=Object.keys(e),p=0;p<c.length;p++){var d=c[p];"protocol"!==d&&(n[d]=e[d])}return y[n.protocol]&&n.hostname&&!n.pathname&&(n.path=n.pathname="/"),n.href=n.format(),n}if(e.protocol&&e.protocol!==n.protocol){if(!y[e.protocol]){for(var u=Object.keys(e),l=0;l<u.length;l++){var m=u[l];n[m]=e[m]}return n.href=n.format(),n}if(n.protocol=e.protocol,e.host||g[e.protocol])n.pathname=e.pathname;else{for(var f=(e.pathname||"").split("/");f.length&&!(e.host=f.shift()););e.host||(e.host=""),e.hostname||(e.hostname=""),""!==f[0]&&f.unshift(""),f.length<2&&f.unshift(""),n.pathname=f.join("/")}if(n.search=e.search,n.query=e.query,n.host=e.host||"",n.auth=e.auth,n.hostname=e.hostname||e.host,n.port=e.port,n.pathname||n.search){var h=n.pathname||"",b=n.search||"";n.path=h+b}return n.slashes=n.slashes||e.slashes,n.href=n.format(),n}var v=n.pathname&&"/"===n.pathname.charAt(0),w=e.host||e.pathname&&"/"===e.pathname.charAt(0),S=w||v||n.host&&e.pathname,x=S,I=n.pathname&&n.pathname.split("/")||[],T=(f=e.pathname&&e.pathname.split("/")||[],n.protocol&&!y[n.protocol]);if(T&&(n.hostname="",n.port=null,n.host&&(""===I[0]?I[0]=n.host:I.unshift(n.host)),n.host="",e.protocol&&(e.hostname=null,e.port=null,e.host&&(""===f[0]?f[0]=e.host:f.unshift(e.host)),e.host=null),S=S&&(""===f[0]||""===I[0])),w)n.host=e.host||""===e.host?e.host:n.host,n.hostname=e.hostname||""===e.hostname?e.hostname:n.hostname,n.search=e.search,n.query=e.query,I=f;else if(f.length)I||(I=[]),I.pop(),I=I.concat(f),n.search=e.search,n.query=e.query;else if(!i.isNullOrUndefined(e.search)){if(T)n.hostname=n.host=I.shift(),($=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=$.shift(),n.host=n.hostname=$.shift());return n.search=e.search,n.query=e.query,i.isNull(n.pathname)&&i.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!I.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var R=I.slice(-1)[0],k=(n.host||e.host||I.length>1)&&("."===R||".."===R)||""===R,C=0,O=I.length;O>=0;O--)"."===(R=I[O])?I.splice(O,1):".."===R?(I.splice(O,1),C++):C&&(I.splice(O,1),C--);if(!S&&!x)for(;C--;C)I.unshift("..");!S||""===I[0]||I[0]&&"/"===I[0].charAt(0)||I.unshift(""),k&&"/"!==I.join("/").substr(-1)&&I.push("");var $,E=""===I[0]||I[0]&&"/"===I[0].charAt(0);T&&(n.hostname=n.host=E?"":I.length?I.shift():"",($=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=$.shift(),n.host=n.hostname=$.shift()));return(S=S||n.host&&I.length)&&!E&&I.unshift(""),I.length?n.pathname=I.join("/"):(n.pathname=null,n.path=null),i.isNull(n.pathname)&&i.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=s.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},function(e,t,n){var r=n(4),i=n(2).document,o=r(i)&&r(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},function(e,t,n){var r=n(2),i=n(19),o=n(31),a=n(104),s=n(8).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:r.Symbol||{});"_"==e.charAt(0)||e in t||s(t,e,{value:a.f(e)})}},function(e,t,n){var r=n(51)("keys"),i=n(36);e.exports=function(e){return r[e]||(r[e]=i(e))}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,n){var r=n(2).document;e.exports=r&&r.documentElement},function(e,t,n){var r=n(4),i=n(1),o=function(e,t){if(i(e),!r(t)&&null!==t)throw TypeError(t+": can't set as prototype!")};e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,t,r){try{(r=n(20)(Function.call,n(17).f(Object.prototype,"__proto__").set,2))(e,[]),t=!(e instanceof Array)}catch(e){t=!0}return function(e,n){return o(e,n),t?e.__proto__=n:r(e,n),e}}({},!1):void 0),check:o}},function(e,t){e.exports="\t\n\v\f\r \u2028\u2029\ufeff"},function(e,t,n){var r=n(4),i=n(81).set;e.exports=function(e,t,n){var o,a=t.constructor;return a!==n&&"function"==typeof a&&(o=a.prototype)!==n.prototype&&r(o)&&i&&i(e,o),e}},function(e,t,n){"use strict";var r=n(22),i=n(25);e.exports=function(e){var t=String(i(this)),n="",o=r(e);if(o<0||o==1/0)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(t+=t))1&o&&(n+=t);return n}},function(e,t){e.exports=Math.sign||function(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},function(e,t){var n=Math.expm1;e.exports=!n||n(10)>22025.465794806718||n(10)<22025.465794806718||-2e-17!=n(-2e-17)?function(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:n},function(e,t,n){"use strict";var r=n(31),i=n(0),o=n(13),a=n(12),s=n(49),c=n(88),p=n(46),d=n(18),u=n(5)("iterator"),l=!([].keys&&"next"in[].keys()),m=function(){return this};e.exports=function(e,t,n,f,h,g,y){c(n,t,f);var b,v,w,S=function(e){if(!l&&e in R)return R[e];switch(e){case"keys":case"values":return function(){return new n(this,e)}}return function(){return new n(this,e)}},x=t+" Iterator",I="values"==h,T=!1,R=e.prototype,k=R[u]||R["@@iterator"]||h&&R[h],C=k||S(h),O=h?I?S("entries"):C:void 0,$="Array"==t&&R.entries||k;if($&&(w=d($.call(new e)))!==Object.prototype&&w.next&&(p(w,x,!0),r||"function"==typeof w[u]||a(w,u,m)),I&&k&&"values"!==k.name&&(T=!0,C=function(){return k.call(this)}),r&&!y||!l&&!T&&R[u]||a(R,u,C),s[t]=C,s[x]=m,h)if(b={values:I?C:S("values"),keys:g?C:S("keys"),entries:O},y)for(v in b)v in R||o(R,v,b[v]);else i(i.P+i.F*(l||T),t,b);return b}},function(e,t,n){"use strict";var r=n(39),i=n(35),o=n(46),a={};n(12)(a,n(5)("iterator"),function(){return this}),e.exports=function(e,t,n){e.prototype=r(a,{next:i(1,n)}),o(e,t+" Iterator")}},function(e,t,n){var r=n(63),i=n(25);e.exports=function(e,t,n){if(r(t))throw TypeError("String#"+n+" doesn't accept regex!");return String(i(e))}},function(e,t,n){var r=n(5)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(n){try{return t[r]=!1,!"/./"[e](t)}catch(e){}}return!0}},function(e,t,n){var r=n(49),i=n(5)("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(r.Array===e||o[i]===e)}},function(e,t,n){"use strict";var r=n(8),i=n(35);e.exports=function(e,t,n){t in e?r.f(e,t,i(0,n)):e[t]=n}},function(e,t,n){var r=n(47),i=n(5)("iterator"),o=n(49);e.exports=n(19).getIteratorMethod=function(e){if(null!=e)return e[i]||e["@@iterator"]||o[r(e)]}},function(e,t,n){var r=n(245);e.exports=function(e,t){return new(r(e))(t)}},function(e,t,n){"use strict";var r=n(9),i=n(38),o=n(6);e.exports=function(e){for(var t=r(this),n=o(t.length),a=arguments.length,s=i(a>1?arguments[1]:void 0,n),c=a>2?arguments[2]:void 0,p=void 0===c?n:i(c,n);p>s;)t[s++]=e;return t}},function(e,t,n){"use strict";var r=n(33),i=n(121),o=n(49),a=n(16);e.exports=n(87)(Array,"Array",function(e,t){this._t=a(e),this._i=0,this._k=t},function(){var e=this._t,t=this._k,n=this._i++;return!e||n>=e.length?(this._t=void 0,i(1)):i(0,"keys"==t?n:"values"==t?e[n]:[n,e[n]])},"values"),o.Arguments=o.Array,r("keys"),r("values"),r("entries")},function(e,t,n){"use strict";var r,i,o=n(54),a=RegExp.prototype.exec,s=String.prototype.replace,c=a,p=(r=/a/,i=/b*/g,a.call(r,"a"),a.call(i,"a"),0!==r.lastIndex||0!==i.lastIndex),d=void 0!==/()??/.exec("")[1];(p||d)&&(c=function(e){var t,n,r,i,c=this;return d&&(n=new RegExp("^"+c.source+"$(?!\\s)",o.call(c))),p&&(t=c.lastIndex),r=a.call(c,e),p&&r&&(c.lastIndex=c.global?r.index+r[0].length:t),d&&r&&r.length>1&&s.call(r[0],n,function(){for(i=1;i<arguments.length-2;i++)void 0===arguments[i]&&(r[i]=void 0)}),r}),e.exports=c},function(e,t,n){"use strict";var r=n(62)(!0);e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},function(e,t,n){var r,i,o,a=n(20),s=n(111),c=n(80),p=n(76),d=n(2),u=d.process,l=d.setImmediate,m=d.clearImmediate,f=d.MessageChannel,h=d.Dispatch,g=0,y={},b=function(){var e=+this;if(y.hasOwnProperty(e)){var t=y[e];delete y[e],t()}},v=function(e){b.call(e.data)};l&&m||(l=function(e){for(var t=[],n=1;arguments.length>n;)t.push(arguments[n++]);return y[++g]=function(){s("function"==typeof e?e:Function(e),t)},r(g),g},m=function(e){delete y[e]},"process"==n(21)(u)?r=function(e){u.nextTick(a(b,e,1))}:h&&h.now?r=function(e){h.now(a(b,e,1))}:f?(o=(i=new f).port2,i.port1.onmessage=v,r=a(o.postMessage,o,1)):d.addEventListener&&"function"==typeof postMessage&&!d.importScripts?(r=function(e){d.postMessage(e+"","*")},d.addEventListener("message",v,!1)):r="onreadystatechange"in p("script")?function(e){c.appendChild(p("script")).onreadystatechange=function(){c.removeChild(this),b.call(e)}}:function(e){setTimeout(a(b,e,1),0)}),e.exports={set:l,clear:m}},function(e,t,n){var r=n(2),i=n(99).set,o=r.MutationObserver||r.WebKitMutationObserver,a=r.process,s=r.Promise,c="process"==n(21)(a);e.exports=function(){var e,t,n,p=function(){var r,i;for(c&&(r=a.domain)&&r.exit();e;){i=e.fn,e=e.next;try{i()}catch(r){throw e?n():t=void 0,r}}t=void 0,r&&r.enter()};if(c)n=function(){a.nextTick(p)};else if(!o||r.navigator&&r.navigator.standalone)if(s&&s.resolve){var d=s.resolve(void 0);n=function(){d.then(p)}}else n=function(){i.call(r,p)};else{var u=!0,l=document.createTextNode("");new o(p).observe(l,{characterData:!0}),n=function(){l.data=u=!u}}return function(r){var i={fn:r,next:void 0};t&&(t.next=i),e||(e=i,n()),t=i}}},function(e,t,n){"use strict";var r=n(10);function i(e){var t,n;this.promise=new e(function(e,r){if(void 0!==t||void 0!==n)throw TypeError("Bad Promise constructor");t=e,n=r}),this.resolve=r(t),this.reject=r(n)}e.exports.f=function(e){return new i(e)}},function(e,t,n){"use strict";var r=n(2),i=n(7),o=n(31),a=n(69),s=n(12),c=n(44),p=n(3),d=n(42),u=n(22),l=n(6),m=n(131),f=n(40).f,h=n(8).f,g=n(95),y=n(46),b="prototype",v="Wrong index!",w=r.ArrayBuffer,S=r.DataView,x=r.Math,I=r.RangeError,T=r.Infinity,R=w,k=x.abs,C=x.pow,O=x.floor,$=x.log,E=x.LN2,D=i?"_b":"buffer",j=i?"_l":"byteLength",P=i?"_o":"byteOffset";function A(e,t,n){var r,i,o,a=new Array(n),s=8*n-t-1,c=(1<<s)-1,p=c>>1,d=23===t?C(2,-24)-C(2,-77):0,u=0,l=e<0||0===e&&1/e<0?1:0;for((e=k(e))!=e||e===T?(i=e!=e?1:0,r=c):(r=O($(e)/E),e*(o=C(2,-r))<1&&(r--,o*=2),(e+=r+p>=1?d/o:d*C(2,1-p))*o>=2&&(r++,o/=2),r+p>=c?(i=0,r=c):r+p>=1?(i=(e*o-1)*C(2,t),r+=p):(i=e*C(2,p-1)*C(2,t),r=0));t>=8;a[u++]=255&i,i/=256,t-=8);for(r=r<<t|i,s+=t;s>0;a[u++]=255&r,r/=256,s-=8);return a[--u]|=128*l,a}function M(e,t,n){var r,i=8*n-t-1,o=(1<<i)-1,a=o>>1,s=i-7,c=n-1,p=e[c--],d=127&p;for(p>>=7;s>0;d=256*d+e[c],c--,s-=8);for(r=d&(1<<-s)-1,d>>=-s,s+=t;s>0;r=256*r+e[c],c--,s-=8);if(0===d)d=1-a;else{if(d===o)return r?NaN:p?-T:T;r+=C(2,t),d-=a}return(p?-1:1)*r*C(2,d-t)}function N(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]}function _(e){return[255&e]}function L(e){return[255&e,e>>8&255]}function q(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function F(e){return A(e,52,8)}function U(e){return A(e,23,4)}function B(e,t,n){h(e[b],t,{get:function(){return this[n]}})}function W(e,t,n,r){var i=m(+n);if(i+t>e[j])throw I(v);var o=e[D]._b,a=i+e[P],s=o.slice(a,a+t);return r?s:s.reverse()}function H(e,t,n,r,i,o){var a=m(+n);if(a+t>e[j])throw I(v);for(var s=e[D]._b,c=a+e[P],p=r(+i),d=0;d<t;d++)s[c+d]=p[o?d:t-d-1]}if(a.ABV){if(!p(function(){w(1)})||!p(function(){new w(-1)})||p(function(){return new w,new w(1.5),new w(NaN),"ArrayBuffer"!=w.name})){for(var z,V=(w=function(e){return d(this,w),new R(m(e))})[b]=R[b],G=f(R),J=0;G.length>J;)(z=G[J++])in w||s(w,z,R[z]);o||(V.constructor=w)}var X=new S(new w(2)),Y=S[b].setInt8;X.setInt8(0,2147483648),X.setInt8(1,2147483649),!X.getInt8(0)&&X.getInt8(1)||c(S[b],{setInt8:function(e,t){Y.call(this,e,t<<24>>24)},setUint8:function(e,t){Y.call(this,e,t<<24>>24)}},!0)}else w=function(e){d(this,w,"ArrayBuffer");var t=m(e);this._b=g.call(new Array(t),0),this[j]=t},S=function(e,t,n){d(this,S,"DataView"),d(e,w,"DataView");var r=e[j],i=u(t);if(i<0||i>r)throw I("Wrong offset!");if(i+(n=void 0===n?r-i:l(n))>r)throw I("Wrong length!");this[D]=e,this[P]=i,this[j]=n},i&&(B(w,"byteLength","_l"),B(S,"buffer","_b"),B(S,"byteLength","_l"),B(S,"byteOffset","_o")),c(S[b],{getInt8:function(e){return W(this,1,e)[0]<<24>>24},getUint8:function(e){return W(this,1,e)[0]},getInt16:function(e){var t=W(this,2,e,arguments[1]);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=W(this,2,e,arguments[1]);return t[1]<<8|t[0]},getInt32:function(e){return N(W(this,4,e,arguments[1]))},getUint32:function(e){return N(W(this,4,e,arguments[1]))>>>0},getFloat32:function(e){return M(W(this,4,e,arguments[1]),23,4)},getFloat64:function(e){return M(W(this,8,e,arguments[1]),52,8)},setInt8:function(e,t){H(this,1,e,_,t)},setUint8:function(e,t){H(this,1,e,_,t)},setInt16:function(e,t){H(this,2,e,L,t,arguments[2])},setUint16:function(e,t){H(this,2,e,L,t,arguments[2])},setInt32:function(e,t){H(this,4,e,q,t,arguments[2])},setUint32:function(e,t){H(this,4,e,q,t,arguments[2])},setFloat32:function(e,t){H(this,4,e,U,t,arguments[2])},setFloat64:function(e,t){H(this,8,e,F,t,arguments[2])}});y(w,"ArrayBuffer"),y(S,"DataView"),s(S[b],a.VIEW,!0),t.ArrayBuffer=w,t.DataView=S},function(e,t,n){e.exports=!n(7)&&!n(3)(function(){return 7!=Object.defineProperty(n(76)("div"),"a",{get:function(){return 7}}).a})},function(e,t,n){t.f=n(5)},function(e,t,n){var r=n(15),i=n(16),o=n(59)(!1),a=n(78)("IE_PROTO");e.exports=function(e,t){var n,s=i(e),c=0,p=[];for(n in s)n!=a&&r(s,n)&&p.push(n);for(;t.length>c;)r(s,n=t[c++])&&(~o(p,n)||p.push(n));return p}},function(e,t,n){var r=n(8),i=n(1),o=n(37);e.exports=n(7)?Object.defineProperties:function(e,t){i(e);for(var n,a=o(t),s=a.length,c=0;s>c;)r.f(e,n=a[c++],t[n]);return e}},function(e,t,n){var r=n(16),i=n(40).f,o={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"[object Window]"==o.call(e)?function(e){try{return i(e)}catch(e){return a.slice()}}(e):i(r(e))}},function(e,t,n){"use strict";var r=n(7),i=n(37),o=n(60),a=n(53),s=n(9),c=n(52),p=Object.assign;e.exports=!p||n(3)(function(){var e={},t={},n=Symbol(),r="abcdefghijklmnopqrst";return e[n]=7,r.split("").forEach(function(e){t[e]=e}),7!=p({},e)[n]||Object.keys(p({},t)).join("")!=r})?function(e,t){for(var n=s(e),p=arguments.length,d=1,u=o.f,l=a.f;p>d;)for(var m,f=c(arguments[d++]),h=u?i(f).concat(u(f)):i(f),g=h.length,y=0;g>y;)m=h[y++],r&&!l.call(f,m)||(n[m]=f[m]);return n}:p},function(e,t){e.exports=Object.is||function(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}},function(e,t,n){"use strict";var r=n(10),i=n(4),o=n(111),a=[].slice,s={},c=function(e,t,n){if(!(t in s)){for(var r=[],i=0;i<t;i++)r[i]="a["+i+"]";s[t]=Function("F,a","return new F("+r.join(",")+")")}return s[t](e,n)};e.exports=Function.bind||function(e){var t=r(this),n=a.call(arguments,1),s=function(){var r=n.concat(a.call(arguments));return this instanceof s?c(t,r.length,r):o(t,r,e)};return i(t.prototype)&&(s.prototype=t.prototype),s}},function(e,t){e.exports=function(e,t,n){var r=void 0===n;switch(t.length){case 0:return r?e():e.call(n);case 1:return r?e(t[0]):e.call(n,t[0]);case 2:return r?e(t[0],t[1]):e.call(n,t[0],t[1]);case 3:return r?e(t[0],t[1],t[2]):e.call(n,t[0],t[1],t[2]);case 4:return r?e(t[0],t[1],t[2],t[3]):e.call(n,t[0],t[1],t[2],t[3])}return e.apply(n,t)}},function(e,t,n){var r=n(2).parseInt,i=n(48).trim,o=n(82),a=/^[-+]?0[xX]/;e.exports=8!==r(o+"08")||22!==r(o+"0x16")?function(e,t){var n=i(String(e),3);return r(n,t>>>0||(a.test(n)?16:10))}:r},function(e,t,n){var r=n(2).parseFloat,i=n(48).trim;e.exports=1/r(n(82)+"-0")!=-1/0?function(e){var t=i(String(e),3),n=r(t);return 0===n&&"-"==t.charAt(0)?-0:n}:r},function(e,t,n){var r=n(21);e.exports=function(e,t){if("number"!=typeof e&&"Number"!=r(e))throw TypeError(t);return+e}},function(e,t,n){var r=n(4),i=Math.floor;e.exports=function(e){return!r(e)&&isFinite(e)&&i(e)===e}},function(e,t){e.exports=Math.log1p||function(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},function(e,t,n){var r=n(85),i=Math.pow,o=i(2,-52),a=i(2,-23),s=i(2,127)*(2-a),c=i(2,-126);e.exports=Math.fround||function(e){var t,n,i=Math.abs(e),p=r(e);return i<c?p*(i/c/a+1/o-1/o)*c*a:(n=(t=(1+a/o)*i)-(t-i))>s||n!=n?p*(1/0):p*n}},function(e,t,n){var r=n(1);e.exports=function(e,t,n,i){try{return i?t(r(n)[0],n[1]):t(n)}catch(t){var o=e.return;throw void 0!==o&&r(o.call(e)),t}}},function(e,t,n){var r=n(10),i=n(9),o=n(52),a=n(6);e.exports=function(e,t,n,s,c){r(t);var p=i(e),d=o(p),u=a(p.length),l=c?u-1:0,m=c?-1:1;if(n<2)for(;;){if(l in d){s=d[l],l+=m;break}if(l+=m,c?l<0:u<=l)throw TypeError("Reduce of empty array with no initial value")}for(;c?l>=0:u>l;l+=m)l in d&&(s=t(s,d[l],l,p));return s}},function(e,t,n){"use strict";var r=n(9),i=n(38),o=n(6);e.exports=[].copyWithin||function(e,t){var n=r(this),a=o(n.length),s=i(e,a),c=i(t,a),p=arguments.length>2?arguments[2]:void 0,d=Math.min((void 0===p?a:i(p,a))-c,a-s),u=1;for(c<s&&s<c+d&&(u=-1,c+=d-1,s+=d-1);d-- >0;)c in n?n[s]=n[c]:delete n[s],s+=u,c+=u;return n}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,n){"use strict";var r=n(97);n(0)({target:"RegExp",proto:!0,forced:r!==/./.exec},{exec:r})},function(e,t,n){n(7)&&"g"!=/./g.flags&&n(8).f(RegExp.prototype,"flags",{configurable:!0,get:n(54)})},function(e,t){e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},function(e,t,n){var r=n(1),i=n(4),o=n(101);e.exports=function(e,t){if(r(e),i(t)&&t.constructor===e)return t;var n=o.f(e);return(0,n.resolve)(t),n.promise}},function(e,t,n){"use strict";var r=n(127),i=n(45);e.exports=n(68)("Map",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{get:function(e){var t=r.getEntry(i(this,"Map"),e);return t&&t.v},set:function(e,t){return r.def(i(this,"Map"),0===e?0:e,t)}},r,!0)},function(e,t,n){"use strict";var r=n(8).f,i=n(39),o=n(44),a=n(20),s=n(42),c=n(43),p=n(87),d=n(121),u=n(41),l=n(7),m=n(32).fastKey,f=n(45),h=l?"_s":"size",g=function(e,t){var n,r=m(t);if("F"!==r)return e._i[r];for(n=e._f;n;n=n.n)if(n.k==t)return n};e.exports={getConstructor:function(e,t,n,p){var d=e(function(e,r){s(e,d,t,"_i"),e._t=t,e._i=i(null),e._f=void 0,e._l=void 0,e[h]=0,null!=r&&c(r,n,e[p],e)});return o(d.prototype,{clear:function(){for(var e=f(this,t),n=e._i,r=e._f;r;r=r.n)r.r=!0,r.p&&(r.p=r.p.n=void 0),delete n[r.i];e._f=e._l=void 0,e[h]=0},delete:function(e){var n=f(this,t),r=g(n,e);if(r){var i=r.n,o=r.p;delete n._i[r.i],r.r=!0,o&&(o.n=i),i&&(i.p=o),n._f==r&&(n._f=i),n._l==r&&(n._l=o),n[h]--}return!!r},forEach:function(e){f(this,t);for(var n,r=a(e,arguments.length>1?arguments[1]:void 0,3);n=n?n.n:this._f;)for(r(n.v,n.k,this);n&&n.r;)n=n.p},has:function(e){return!!g(f(this,t),e)}}),l&&r(d.prototype,"size",{get:function(){return f(this,t)[h]}}),d},def:function(e,t,n){var r,i,o=g(e,t);return o?o.v=n:(e._l=o={i:i=m(t,!0),k:t,v:n,p:r=e._l,n:void 0,r:!1},e._f||(e._f=o),r&&(r.n=o),e[h]++,"F"!==i&&(e._i[i]=o)),e},getEntry:g,setStrong:function(e,t,n){p(e,t,function(e,n){this._t=f(e,t),this._k=n,this._l=void 0},function(){for(var e=this._k,t=this._l;t&&t.r;)t=t.p;return this._t&&(this._l=t=t?t.n:this._t._f)?d(0,"keys"==e?t.k:"values"==e?t.v:[t.k,t.v]):(this._t=void 0,d(1))},n?"entries":"values",!n,!0),u(t)}}},function(e,t,n){"use strict";var r=n(127),i=n(45);e.exports=n(68)("Set",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{add:function(e){return r.def(i(this,"Set"),e=0===e?0:e,e)}},r)},function(e,t,n){"use strict";var r,i=n(2),o=n(27)(0),a=n(13),s=n(32),c=n(108),p=n(130),d=n(4),u=n(45),l=n(45),m=!i.ActiveXObject&&"ActiveXObject"in i,f=s.getWeak,h=Object.isExtensible,g=p.ufstore,y=function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},b={get:function(e){if(d(e)){var t=f(e);return!0===t?g(u(this,"WeakMap")).get(e):t?t[this._i]:void 0}},set:function(e,t){return p.def(u(this,"WeakMap"),e,t)}},v=e.exports=n(68)("WeakMap",y,b,p,!0,!0);l&&m&&(c((r=p.getConstructor(y,"WeakMap")).prototype,b),s.NEED=!0,o(["delete","has","get","set"],function(e){var t=v.prototype,n=t[e];a(t,e,function(t,i){if(d(t)&&!h(t)){this._f||(this._f=new r);var o=this._f[e](t,i);return"set"==e?this:o}return n.call(this,t,i)})}))},function(e,t,n){"use strict";var r=n(44),i=n(32).getWeak,o=n(1),a=n(4),s=n(42),c=n(43),p=n(27),d=n(15),u=n(45),l=p(5),m=p(6),f=0,h=function(e){return e._l||(e._l=new g)},g=function(){this.a=[]},y=function(e,t){return l(e.a,function(e){return e[0]===t})};g.prototype={get:function(e){var t=y(this,e);if(t)return t[1]},has:function(e){return!!y(this,e)},set:function(e,t){var n=y(this,e);n?n[1]=t:this.a.push([e,t])},delete:function(e){var t=m(this.a,function(t){return t[0]===e});return~t&&this.a.splice(t,1),!!~t}},e.exports={getConstructor:function(e,t,n,o){var p=e(function(e,r){s(e,p,t,"_i"),e._t=t,e._i=f++,e._l=void 0,null!=r&&c(r,n,e[o],e)});return r(p.prototype,{delete:function(e){if(!a(e))return!1;var n=i(e);return!0===n?h(u(this,t)).delete(e):n&&d(n,this._i)&&delete n[this._i]},has:function(e){if(!a(e))return!1;var n=i(e);return!0===n?h(u(this,t)).has(e):n&&d(n,this._i)}}),p},def:function(e,t,n){var r=i(o(t),!0);return!0===r?h(e).set(t,n):r[e._i]=n,e},ufstore:h}},function(e,t,n){var r=n(22),i=n(6);e.exports=function(e){if(void 0===e)return 0;var t=r(e),n=i(t);if(t!==n)throw RangeError("Wrong length!");return n}},function(e,t,n){var r=n(40),i=n(60),o=n(1),a=n(2).Reflect;e.exports=a&&a.ownKeys||function(e){var t=r.f(o(e)),n=i.f;return n?t.concat(n(e)):t}},function(e,t,n){"use strict";var r=n(61),i=n(4),o=n(6),a=n(20),s=n(5)("isConcatSpreadable");e.exports=function e(t,n,c,p,d,u,l,m){for(var f,h,g=d,y=0,b=!!l&&a(l,m,3);y<p;){if(y in c){if(f=b?b(c[y],y,n):c[y],h=!1,i(f)&&(h=void 0!==(h=f[s])?!!h:r(f)),h&&u>0)g=e(t,n,f,o(f.length),g,u-1)-1;else{if(g>=9007199254740991)throw TypeError();t[g]=f}g++}y++}return g}},function(e,t,n){var r=n(6),i=n(84),o=n(25);e.exports=function(e,t,n,a){var s=String(o(e)),c=s.length,p=void 0===n?" ":String(n),d=r(t);if(d<=c||""==p)return s;var u=d-c,l=i.call(p,Math.ceil(u/p.length));return l.length>u&&(l=l.slice(0,u)),a?l+s:s+l}},function(e,t,n){var r=n(7),i=n(37),o=n(16),a=n(53).f;e.exports=function(e){return function(t){for(var n,s=o(t),c=i(s),p=c.length,d=0,u=[];p>d;)n=c[d++],r&&!a.call(s,n)||u.push(e?[n,s[n]]:s[n]);return u}}},function(e,t,n){var r=n(47),i=n(137);e.exports=function(e){return function(){if(r(this)!=e)throw TypeError(e+"#toJSON isn't generic");return i(this)}}},function(e,t,n){var r=n(43);e.exports=function(e,t){var n=[];return r(e,!1,n.push,n,t),n}},function(e,t){e.exports=Math.scale||function(e,t,n,r,i){return 0===arguments.length||e!=e||t!=t||n!=n||r!=r||i!=i?NaN:e===1/0||e===-1/0?e:(e-t)*(i-r)/(n-t)+r}},function(e,t,n){"use strict";const r=n(140),i=n(375),o=n(151),a=n(376);function s(e,t){e.host=e.host||o.HOST,e.port=e.port||o.PORT,e.secure=!!e.secure,e.useHostName=!!e.useHostName,e.alterPath=e.alterPath||(e=>e);const n={...e};n.path=e.alterPath(e.path),a(e.secure?i:r,n,t)}function c(e){return(t,n)=>("function"==typeof t&&(n=t,t=void 0),t=t||{},"function"==typeof n?void e(t,n):new Promise((n,r)=>{e(t,(e,t)=>{e?r(e):n(t)})}))}e.exports.Protocol=c(function(e,t){if(e.local){const e=n(377);t(null,e)}else e.path="/json/protocol",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.List=c(function(e,t){e.path="/json/list",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.New=c(function(e,t){e.path="/json/new",Object.prototype.hasOwnProperty.call(e,"url")&&(e.path+=`?${e.url}`),s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.Activate=c(function(e,t){e.path="/json/activate/"+e.id,s(e,e=>{t(e||null)})}),e.exports.Close=c(function(e,t){e.path="/json/close/"+e.id,s(e,e=>{t(e||null)})}),e.exports.Version=c(function(e,t){e.path="/json/version",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})})},function(e,t,n){(function(e){var r=n(356),i=n(143),o=n(367),a=n(368),s=n(75),c=t;c.request=function(t,n){t="string"==typeof t?s.parse(t):o(t);var i=-1===e.location.protocol.search(/^https?:$/)?"http:":"",a=t.protocol||i,c=t.hostname||t.host,p=t.port,d=t.path||"/";c&&-1!==c.indexOf(":")&&(c="["+c+"]"),t.url=(c?a+"//"+c:"")+(p?":"+p:"")+d,t.method=(t.method||"GET").toUpperCase(),t.headers=t.headers||{};var u=new r(t);return n&&u.on("response",n),u},c.get=function(e,t){var n=c.request(e,t);return n.end(),n},c.ClientRequest=r,c.IncomingMessage=i.IncomingMessage,c.Agent=function(){},c.Agent.defaultMaxSockets=4,c.globalAgent=new c.Agent,c.STATUS_CODES=a,c.METHODS=["CHECKOUT","CONNECT","COPY","DELETE","GET","HEAD","LOCK","M-SEARCH","MERGE","MKACTIVITY","MKCOL","MOVE","NOTIFY","OPTIONS","PATCH","POST","PROPFIND","PROPPATCH","PURGE","PUT","REPORT","SEARCH","SUBSCRIBE","TRACE","UNLOCK","UNSUBSCRIBE"]}).call(this,n(11))},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){(function(e){t.fetch=s(e.fetch)&&s(e.ReadableStream),t.writableStream=s(e.WritableStream),t.abortController=s(e.AbortController),t.blobConstructor=!1;try{new Blob([new ArrayBuffer(1)]),t.blobConstructor=!0}catch(e){}var n;function r(){if(void 0!==n)return n;if(e.XMLHttpRequest){n=new e.XMLHttpRequest;try{n.open("GET",e.XDomainRequest?"/":"https://example.com")}catch(e){n=null}}else n=null;return n}function i(e){var t=r();if(!t)return!1;try{return t.responseType=e,t.responseType===e}catch(e){}return!1}var o=void 0!==e.ArrayBuffer,a=o&&s(e.ArrayBuffer.prototype.slice);function s(e){return"function"==typeof e}t.arraybuffer=t.fetch||o&&i("arraybuffer"),t.msstream=!t.fetch&&a&&i("ms-stream"),t.mozchunkedarraybuffer=!t.fetch&&o&&i("moz-chunked-arraybuffer"),t.overrideMimeType=t.fetch||!!r()&&s(r().overrideMimeType),t.vbArray=s(e.VBArray),n=null}).call(this,n(11))},function(e,t,n){(function(e,r,i){var o=n(142),a=n(34),s=n(144),c=t.readyStates={UNSENT:0,OPENED:1,HEADERS_RECEIVED:2,LOADING:3,DONE:4},p=t.IncomingMessage=function(t,n,a,c){var p=this;if(s.Readable.call(p),p._mode=a,p.headers={},p.rawHeaders=[],p.trailers={},p.rawTrailers=[],p.on("end",function(){e.nextTick(function(){p.emit("close")})}),"fetch"===a){if(p._fetchResponse=n,p.url=n.url,p.statusCode=n.status,p.statusMessage=n.statusText,n.headers.forEach(function(e,t){p.headers[t.toLowerCase()]=e,p.rawHeaders.push(t,e)}),o.writableStream){var d=new WritableStream({write:function(e){return new Promise(function(t,n){p._destroyed?n():p.push(new r(e))?t():p._resumeFetch=t})},close:function(){i.clearTimeout(c),p._destroyed||p.push(null)},abort:function(e){p._destroyed||p.emit("error",e)}});try{return void n.body.pipeTo(d).catch(function(e){i.clearTimeout(c),p._destroyed||p.emit("error",e)})}catch(e){}}var u=n.body.getReader();!function e(){u.read().then(function(t){if(!p._destroyed){if(t.done)return i.clearTimeout(c),void p.push(null);p.push(new r(t.value)),e()}}).catch(function(e){i.clearTimeout(c),p._destroyed||p.emit("error",e)})}()}else{if(p._xhr=t,p._pos=0,p.url=t.responseURL,p.statusCode=t.status,p.statusMessage=t.statusText,t.getAllResponseHeaders().split(/\r?\n/).forEach(function(e){var t=e.match(/^([^:]+):\s*(.*)/);if(t){var n=t[1].toLowerCase();"set-cookie"===n?(void 0===p.headers[n]&&(p.headers[n]=[]),p.headers[n].push(t[2])):void 0!==p.headers[n]?p.headers[n]+=", "+t[2]:p.headers[n]=t[2],p.rawHeaders.push(t[1],t[2])}}),p._charset="x-user-defined",!o.overrideMimeType){var l=p.rawHeaders["mime-type"];if(l){var m=l.match(/;\s*charset=([^;])(;|$)/);m&&(p._charset=m[1].toLowerCase())}p._charset||(p._charset="utf-8")}}};a(p,s.Readable),p.prototype._read=function(){var e=this._resumeFetch;e&&(this._resumeFetch=null,e())},p.prototype._onXHRProgress=function(){var e=this,t=e._xhr,n=null;switch(e._mode){case"text:vbarray":if(t.readyState!==c.DONE)break;try{n=new i.VBArray(t.responseBody).toArray()}catch(e){}if(null!==n){e.push(new r(n));break}case"text":try{n=t.responseText}catch(t){e._mode="text:vbarray";break}if(n.length>e._pos){var o=n.substr(e._pos);if("x-user-defined"===e._charset){for(var a=new r(o.length),s=0;s<o.length;s++)a[s]=255&o.charCodeAt(s);e.push(a)}else e.push(o,e._charset);e._pos=n.length}break;case"arraybuffer":if(t.readyState!==c.DONE||!t.response)break;n=t.response,e.push(new r(new Uint8Array(n)));break;case"moz-chunked-arraybuffer":if(n=t.response,t.readyState!==c.LOADING||!n)break;e.push(new r(new Uint8Array(n)));break;case"ms-stream":if(n=t.response,t.readyState!==c.LOADING)break;var p=new i.MSStreamReader;p.onprogress=function(){p.result.byteLength>e._pos&&(e.push(new r(new Uint8Array(p.result.slice(e._pos)))),e._pos=p.result.byteLength)},p.onload=function(){e.push(null)},p.readAsArrayBuffer(n)}e._xhr.readyState===c.DONE&&"ms-stream"!==e._mode&&e.push(null)}}).call(this,n(30),n(57).Buffer,n(11))},function(e,t,n){(t=e.exports=n(145)).Stream=t,t.Readable=t,t.Writable=n(148),t.Duplex=n(50),t.Transform=n(150),t.PassThrough=n(365)},function(e,t,n){"use strict";(function(t,r){var i=n(73);e.exports=v;var o,a=n(141);v.ReadableState=b;n(56).EventEmitter;var s=function(e,t){return e.listeners(t).length},c=n(146),p=n(74).Buffer,d=t.Uint8Array||function(){};var u=n(58);u.inherits=n(34);var l=n(359),m=void 0;m=l&&l.debuglog?l.debuglog("stream"):function(){};var f,h=n(360),g=n(147);u.inherits(v,c);var y=["error","close","destroy","pause","resume"];function b(e,t){e=e||{};var r=t instanceof(o=o||n(50));this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.readableObjectMode);var i=e.highWaterMark,a=e.readableHighWaterMark,s=this.objectMode?16:16384;this.highWaterMark=i||0===i?i:r&&(a||0===a)?a:s,this.highWaterMark=Math.floor(this.highWaterMark),this.buffer=new h,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.destroyed=!1,this.defaultEncoding=e.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(f||(f=n(149).StringDecoder),this.decoder=new f(e.encoding),this.encoding=e.encoding)}function v(e){if(o=o||n(50),!(this instanceof v))return new v(e);this._readableState=new b(e,this),this.readable=!0,e&&("function"==typeof e.read&&(this._read=e.read),"function"==typeof e.destroy&&(this._destroy=e.destroy)),c.call(this)}function w(e,t,n,r,i){var o,a=e._readableState;null===t?(a.reading=!1,function(e,t){if(t.ended)return;if(t.decoder){var n=t.decoder.end();n&&n.length&&(t.buffer.push(n),t.length+=t.objectMode?1:n.length)}t.ended=!0,T(e)}(e,a)):(i||(o=function(e,t){var n;r=t,p.isBuffer(r)||r instanceof d||"string"==typeof t||void 0===t||e.objectMode||(n=new TypeError("Invalid non-string/buffer chunk"));var r;return n}(a,t)),o?e.emit("error",o):a.objectMode||t&&t.length>0?("string"==typeof t||a.objectMode||Object.getPrototypeOf(t)===p.prototype||(t=function(e){return p.from(e)}(t)),r?a.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):S(e,a,t,!0):a.ended?e.emit("error",new Error("stream.push() after EOF")):(a.reading=!1,a.decoder&&!n?(t=a.decoder.write(t),a.objectMode||0!==t.length?S(e,a,t,!1):k(e,a)):S(e,a,t,!1))):r||(a.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.length<e.highWaterMark||0===e.length)}(a)}function S(e,t,n,r){t.flowing&&0===t.length&&!t.sync?(e.emit("data",n),e.read(0)):(t.length+=t.objectMode?1:n.length,r?t.buffer.unshift(n):t.buffer.push(n),t.needReadable&&T(e)),k(e,t)}Object.defineProperty(v.prototype,"destroyed",{get:function(){return void 0!==this._readableState&&this._readableState.destroyed},set:function(e){this._readableState&&(this._readableState.destroyed=e)}}),v.prototype.destroy=g.destroy,v.prototype._undestroy=g.undestroy,v.prototype._destroy=function(e,t){this.push(null),t(e)},v.prototype.push=function(e,t){var n,r=this._readableState;return r.objectMode?n=!0:"string"==typeof e&&((t=t||r.defaultEncoding)!==r.encoding&&(e=p.from(e,t),t=""),n=!0),w(this,e,t,!1,n)},v.prototype.unshift=function(e){return w(this,e,null,!0,!1)},v.prototype.isPaused=function(){return!1===this._readableState.flowing},v.prototype.setEncoding=function(e){return f||(f=n(149).StringDecoder),this._readableState.decoder=new f(e),this._readableState.encoding=e,this};var x=8388608;function I(e,t){return e<=0||0===t.length&&t.ended?0:t.objectMode?1:e!=e?t.flowing&&t.length?t.buffer.head.data.length:t.length:(e>t.highWaterMark&&(t.highWaterMark=function(e){return e>=x?e=x:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function T(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(m("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?i.nextTick(R,e):R(e))}function R(e){m("emit readable"),e.emit("readable"),E(e)}function k(e,t){t.readingMore||(t.readingMore=!0,i.nextTick(C,e,t))}function C(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length<t.highWaterMark&&(m("maybeReadMore read 0"),e.read(0),n!==t.length);)n=t.length;t.readingMore=!1}function O(e){m("readable nexttick read 0"),e.read(0)}function $(e,t){t.reading||(m("resume read 0"),e.read(0)),t.resumeScheduled=!1,t.awaitDrain=0,e.emit("resume"),E(e),t.flowing&&!t.reading&&e.read(0)}function E(e){var t=e._readableState;for(m("flow",t.flowing);t.flowing&&null!==e.read(););}function D(e,t){return 0===t.length?null:(t.objectMode?n=t.buffer.shift():!e||e>=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;e<t.head.data.length?(r=t.head.data.slice(0,e),t.head.data=t.head.data.slice(e)):r=e===t.head.data.length?t.shift():n?function(e,t){var n=t.head,r=1,i=n.data;e-=i.length;for(;n=n.next;){var o=n.data,a=e>o.length?o.length:e;if(a===o.length?i+=o:i+=o.slice(0,e),0===(e-=a)){a===o.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=o.slice(a));break}++r}return t.length-=r,i}(e,t):function(e,t){var n=p.allocUnsafe(e),r=t.head,i=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var o=r.data,a=e>o.length?o.length:e;if(o.copy(n,n.length-e,0,a),0===(e-=a)){a===o.length?(++i,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=o.slice(a));break}++i}return t.length-=i,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function j(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,i.nextTick(P,t,e))}function P(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function A(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1}v.prototype.read=function(e){m("read",e),e=parseInt(e,10);var t=this._readableState,n=e;if(0!==e&&(t.emittedReadable=!1),0===e&&t.needReadable&&(t.length>=t.highWaterMark||t.ended))return m("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?j(this):T(this),null;if(0===(e=I(e,t))&&t.ended)return 0===t.length&&j(this),null;var r,i=t.needReadable;return m("need readable",i),(0===t.length||t.length-e<t.highWaterMark)&&m("length less than watermark",i=!0),t.ended||t.reading?m("reading or ended",i=!1):i&&(m("do read"),t.reading=!0,t.sync=!0,0===t.length&&(t.needReadable=!0),this._read(t.highWaterMark),t.sync=!1,t.reading||(e=I(n,t))),null===(r=e>0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&j(this)),null!==r&&this.emit("data",r),r},v.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},v.prototype.pipe=function(e,t){var n=this,o=this._readableState;switch(o.pipesCount){case 0:o.pipes=e;break;case 1:o.pipes=[o.pipes,e];break;default:o.pipes.push(e)}o.pipesCount+=1,m("pipe count=%d opts=%j",o.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?d:v;function p(t,r){m("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,m("cleanup"),e.removeListener("close",y),e.removeListener("finish",b),e.removeListener("drain",u),e.removeListener("error",g),e.removeListener("unpipe",p),n.removeListener("end",d),n.removeListener("end",v),n.removeListener("data",h),l=!0,!o.awaitDrain||e._writableState&&!e._writableState.needDrain||u())}function d(){m("onend"),e.end()}o.endEmitted?i.nextTick(c):n.once("end",c),e.on("unpipe",p);var u=function(e){return function(){var t=e._readableState;m("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&s(e,"data")&&(t.flowing=!0,E(e))}}(n);e.on("drain",u);var l=!1;var f=!1;function h(t){m("ondata"),f=!1,!1!==e.write(t)||f||((1===o.pipesCount&&o.pipes===e||o.pipesCount>1&&-1!==A(o.pipes,e))&&!l&&(m("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,f=!0),n.pause())}function g(t){m("onerror",t),v(),e.removeListener("error",g),0===s(e,"error")&&e.emit("error",t)}function y(){e.removeListener("finish",b),v()}function b(){m("onfinish"),e.removeListener("close",y),v()}function v(){m("unpipe"),n.unpipe(e)}return n.on("data",h),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?a(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",g),e.once("close",y),e.once("finish",b),e.emit("pipe",n),o.flowing||(m("pipe resume"),n.resume()),e},v.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,i=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var o=0;o<i;o++)r[o].emit("unpipe",this,n);return this}var a=A(t.pipes,e);return-1===a?this:(t.pipes.splice(a,1),t.pipesCount-=1,1===t.pipesCount&&(t.pipes=t.pipes[0]),e.emit("unpipe",this,n),this)},v.prototype.on=function(e,t){var n=c.prototype.on.call(this,e,t);if("data"===e)!1!==this._readableState.flowing&&this.resume();else if("readable"===e){var r=this._readableState;r.endEmitted||r.readableListening||(r.readableListening=r.needReadable=!0,r.emittedReadable=!1,r.reading?r.length&&T(this):i.nextTick(O,this))}return n},v.prototype.addListener=v.prototype.on,v.prototype.resume=function(){var e=this._readableState;return e.flowing||(m("resume"),e.flowing=!0,function(e,t){t.resumeScheduled||(t.resumeScheduled=!0,i.nextTick($,e,t))}(this,e)),this},v.prototype.pause=function(){return m("call pause flowing=%j",this._readableState.flowing),!1!==this._readableState.flowing&&(m("pause"),this._readableState.flowing=!1,this.emit("pause")),this},v.prototype.wrap=function(e){var t=this,n=this._readableState,r=!1;for(var i in e.on("end",function(){if(m("wrapped end"),n.decoder&&!n.ended){var e=n.decoder.end();e&&e.length&&t.push(e)}t.push(null)}),e.on("data",function(i){(m("wrapped data"),n.decoder&&(i=n.decoder.write(i)),n.objectMode&&null==i)||(n.objectMode||i&&i.length)&&(t.push(i)||(r=!0,e.pause()))}),e)void 0===this[i]&&"function"==typeof e[i]&&(this[i]=function(t){return function(){return e[t].apply(e,arguments)}}(i));for(var o=0;o<y.length;o++)e.on(y[o],this.emit.bind(this,y[o]));return this._read=function(t){m("wrapped _read",t),r&&(r=!1,e.resume())},this},Object.defineProperty(v.prototype,"readableHighWaterMark",{enumerable:!1,get:function(){return this._readableState.highWaterMark}}),v._fromList=D}).call(this,n(11),n(30))},function(e,t,n){e.exports=n(56).EventEmitter},function(e,t,n){"use strict";var r=n(73);function i(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,o=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return o||a?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(i,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(i,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";(function(t,r,i){var o=n(73);function a(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var i=r.callback;t.pendingcb--,i(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var s,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:o.nextTick;b.WritableState=y;var p=n(58);p.inherits=n(34);var d={deprecate:n(364)},u=n(146),l=n(74).Buffer,m=i.Uint8Array||function(){};var f,h=n(147);function g(){}function y(e,t){s=s||n(50),e=e||{};var r=t instanceof s;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var i=e.highWaterMark,p=e.writableHighWaterMark,d=this.objectMode?16:16384;this.highWaterMark=i||0===i?i:r&&(p||0===p)?p:d,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var u=!1===e.decodeStrings;this.decodeStrings=!u,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,i=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,i){--t.pendingcb,n?(o.nextTick(i,r),o.nextTick(T,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(i(r),e._writableState.errorEmitted=!0,e.emit("error",r),T(e,t))}(e,n,r,t,i);else{var a=x(n);a||n.corked||n.bufferProcessing||!n.bufferedRequest||S(e,n),r?c(w,e,n,a,i):w(e,n,a,i)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new a(this)}function b(e){if(s=s||n(50),!(f.call(b,this)||this instanceof s))return new b(e);this._writableState=new y(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),u.call(this)}function v(e,t,n,r,i,o,a){t.writelen=r,t.writecb=a,t.writing=!0,t.sync=!0,n?e._writev(i,t.onwrite):e._write(i,o,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),T(e,t)}function S(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,i=new Array(r),o=t.corkedRequestsFree;o.entry=n;for(var s=0,c=!0;n;)i[s]=n,n.isBuf||(c=!1),n=n.next,s+=1;i.allBuffers=c,v(e,t,!0,t.length,i,"",o.finish),t.pendingcb++,t.lastBufferedRequest=null,o.next?(t.corkedRequestsFree=o.next,o.next=null):t.corkedRequestsFree=new a(t),t.bufferedRequestCount=0}else{for(;n;){var p=n.chunk,d=n.encoding,u=n.callback;if(v(e,t,!1,t.objectMode?1:p.length,p,d,u),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function x(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function I(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),T(e,t)})}function T(e,t){var n=x(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,o.nextTick(I,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}p.inherits(b,u),y.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(y.prototype,"buffer",{get:d.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(f=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!f.call(this,e)||this===b&&(e&&e._writableState instanceof y)}})):f=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,i=this._writableState,a=!1,s=!i.objectMode&&(r=e,l.isBuffer(r)||r instanceof m);return s&&!l.isBuffer(e)&&(e=function(e){return l.from(e)}(e)),"function"==typeof t&&(n=t,t=null),s?t="buffer":t||(t=i.defaultEncoding),"function"!=typeof n&&(n=g),i.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),o.nextTick(t,n)}(this,n):(s||function(e,t,n,r){var i=!0,a=!1;return null===n?a=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(a=new TypeError("Invalid non-string/buffer chunk")),a&&(e.emit("error",a),o.nextTick(r,a),i=!1),i}(this,i,e,n))&&(i.pendingcb++,a=function(e,t,n,r,i,o){if(!n){var a=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=l.from(t,n));return t}(t,r,i);r!==a&&(n=!0,i="buffer",r=a)}var s=t.objectMode?1:r.length;t.length+=s;var c=t.length<t.highWaterMark;c||(t.needDrain=!0);if(t.writing||t.corked){var p=t.lastBufferedRequest;t.lastBufferedRequest={chunk:r,encoding:i,isBuf:n,callback:o,next:null},p?p.next=t.lastBufferedRequest:t.bufferedRequest=t.lastBufferedRequest,t.bufferedRequestCount+=1}else v(e,t,!1,s,r,i,o);return c}(this,i,s,e,t,n)),a},b.prototype.cork=function(){this._writableState.corked++},b.prototype.uncork=function(){var e=this._writableState;e.corked&&(e.corked--,e.writing||e.corked||e.finished||e.bufferProcessing||!e.bufferedRequest||S(this,e))},b.prototype.setDefaultEncoding=function(e){if("string"==typeof e&&(e=e.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((e+"").toLowerCase())>-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,T(e,t),n&&(t.finished?o.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=h.destroy,b.prototype._undestroy=h.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(30),n(362).setImmediate,n(11))},function(e,t,n){"use strict";var r=n(74).Buffer,i=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function o(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===i||!i(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=p,t=4;break;case"utf8":this.fillLast=s,t=4;break;case"base64":this.text=d,this.end=u,t=3;break;default:return this.write=l,void(this.end=m)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function a(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function s(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function p(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function d(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function u(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function l(e){return e.toString(this.encoding)}function m(e){return e&&e.length?this.write(e):""}t.StringDecoder=o,o.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n<e.length?t?t+this.text(e,n):this.text(e,n):t||""},o.prototype.end=function(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+"�":t},o.prototype.text=function(e,t){var n=function(e,t,n){var r=t.length-1;if(r<n)return 0;var i=a(t[r]);if(i>=0)return i>0&&(e.lastNeed=i-1),i;if(--r<n||-2===i)return 0;if((i=a(t[r]))>=0)return i>0&&(e.lastNeed=i-2),i;if(--r<n||-2===i)return 0;if((i=a(t[r]))>=0)return i>0&&(2===i?i=0:e.lastNeed=i-3),i;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},o.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";e.exports=a;var r=n(50),i=n(58);function o(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var i=this._readableState;i.reading=!1,(i.needReadable||i.length<i.highWaterMark)&&this._read(i.highWaterMark)}function a(e){if(!(this instanceof a))return new a(e);r.call(this,e),this._transformState={afterTransform:o.bind(this),needTransform:!1,transforming:!1,writecb:null,writechunk:null,writeencoding:null},this._readableState.needReadable=!0,this._readableState.sync=!1,e&&("function"==typeof e.transform&&(this._transform=e.transform),"function"==typeof e.flush&&(this._flush=e.flush)),this.on("prefinish",s)}function s(){var e=this;"function"==typeof this._flush?this._flush(function(t,n){c(e,t,n)}):c(this,null,null)}function c(e,t,n){if(t)return e.emit("error",t);if(null!=n&&e.push(n),e._writableState.length)throw new Error("Calling transform done when ws.length != 0");if(e._transformState.transforming)throw new Error("Calling transform done when still transforming");return e.push(null)}i.inherits=n(34),i.inherits(a,r),a.prototype.push=function(e,t){return this._transformState.needTransform=!1,r.prototype.push.call(this,e,t)},a.prototype._transform=function(e,t,n){throw new Error("_transform() is not implemented")},a.prototype._write=function(e,t,n){var r=this._transformState;if(r.writecb=n,r.writechunk=e,r.writeencoding=t,!r.transforming){var i=this._readableState;(r.needTransform||i.needReadable||i.length<i.highWaterMark)&&this._read(i.highWaterMark)}},a.prototype._read=function(e){var t=this._transformState;null!==t.writechunk&&t.writecb&&!t.transforming?(t.transforming=!0,this._transform(t.writechunk,t.writeencoding,t.afterTransform)):t.needTransform=!0},a.prototype._destroy=function(e,t){var n=this;r.prototype._destroy.call(this,e,function(e){t(e),n.emit("close")})}},function(e,t,n){"use strict";e.exports.HOST="localhost",e.exports.PORT=9222},function(e,t,n){n(153),e.exports=n(355)},function(e,t,n){"use strict";(function(e){if(n(154),n(351),n(352),e._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed");e._babelPolyfill=!0;var t="defineProperty";function r(e,n,r){e[n]||Object[t](e,n,{writable:!0,configurable:!0,value:r})}r(String.prototype,"padLeft","".padStart),r(String.prototype,"padRight","".padEnd),"pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach(function(e){[][e]&&r(Array,e,Function.call.bind([][e]))})}).call(this,n(11))},function(e,t,n){n(155),n(158),n(159),n(160),n(161),n(162),n(163),n(164),n(165),n(166),n(167),n(168),n(169),n(170),n(171),n(172),n(173),n(174),n(175),n(176),n(177),n(178),n(179),n(180),n(181),n(182),n(183),n(184),n(185),n(186),n(187),n(188),n(189),n(190),n(191),n(192),n(193),n(194),n(195),n(196),n(197),n(198),n(199),n(200),n(201),n(202),n(203),n(204),n(205),n(206),n(207),n(208),n(209),n(210),n(211),n(212),n(213),n(214),n(215),n(216),n(217),n(218),n(219),n(220),n(221),n(222),n(223),n(224),n(225),n(226),n(227),n(228),n(229),n(230),n(231),n(232),n(233),n(235),n(236),n(238),n(239),n(240),n(241),n(242),n(243),n(244),n(246),n(247),n(248),n(249),n(250),n(251),n(252),n(253),n(254),n(255),n(256),n(257),n(258),n(96),n(259),n(122),n(260),n(123),n(261),n(262),n(263),n(264),n(265),n(126),n(128),n(129),n(266),n(267),n(268),n(269),n(270),n(271),n(272),n(273),n(274),n(275),n(276),n(277),n(278),n(279),n(280),n(281),n(282),n(283),n(284),n(285),n(286),n(287),n(288),n(289),n(290),n(291),n(292),n(293),n(294),n(295),n(296),n(297),n(298),n(299),n(300),n(301),n(302),n(303),n(304),n(305),n(306),n(307),n(308),n(309),n(310),n(311),n(312),n(313),n(314),n(315),n(316),n(317),n(318),n(319),n(320),n(321),n(322),n(323),n(324),n(325),n(326),n(327),n(328),n(329),n(330),n(331),n(332),n(333),n(334),n(335),n(336),n(337),n(338),n(339),n(340),n(341),n(342),n(343),n(344),n(345),n(346),n(347),n(348),n(349),n(350),e.exports=n(19)},function(e,t,n){"use strict";var r=n(2),i=n(15),o=n(7),a=n(0),s=n(13),c=n(32).KEY,p=n(3),d=n(51),u=n(46),l=n(36),m=n(5),f=n(104),h=n(77),g=n(157),y=n(61),b=n(1),v=n(4),w=n(9),S=n(16),x=n(24),I=n(35),T=n(39),R=n(107),k=n(17),C=n(60),O=n(8),$=n(37),E=k.f,D=O.f,j=R.f,P=r.Symbol,A=r.JSON,M=A&&A.stringify,N=m("_hidden"),_=m("toPrimitive"),L={}.propertyIsEnumerable,q=d("symbol-registry"),F=d("symbols"),U=d("op-symbols"),B=Object.prototype,W="function"==typeof P&&!!C.f,H=r.QObject,z=!H||!H.prototype||!H.prototype.findChild,V=o&&p(function(){return 7!=T(D({},"a",{get:function(){return D(this,"a",{value:7}).a}})).a})?function(e,t,n){var r=E(B,t);r&&delete B[t],D(e,t,n),r&&e!==B&&D(B,t,r)}:D,G=function(e){var t=F[e]=T(P.prototype);return t._k=e,t},J=W&&"symbol"==typeof P.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof P},X=function(e,t,n){return e===B&&X(U,t,n),b(e),t=x(t,!0),b(n),i(F,t)?(n.enumerable?(i(e,N)&&e[N][t]&&(e[N][t]=!1),n=T(n,{enumerable:I(0,!1)})):(i(e,N)||D(e,N,I(1,{})),e[N][t]=!0),V(e,t,n)):D(e,t,n)},Y=function(e,t){b(e);for(var n,r=g(t=S(t)),i=0,o=r.length;o>i;)X(e,n=r[i++],t[n]);return e},K=function(e){var t=L.call(this,e=x(e,!0));return!(this===B&&i(F,e)&&!i(U,e))&&(!(t||!i(this,e)||!i(F,e)||i(this,N)&&this[N][e])||t)},Q=function(e,t){if(e=S(e),t=x(t,!0),e!==B||!i(F,t)||i(U,t)){var n=E(e,t);return!n||!i(F,t)||i(e,N)&&e[N][t]||(n.enumerable=!0),n}},Z=function(e){for(var t,n=j(S(e)),r=[],o=0;n.length>o;)i(F,t=n[o++])||t==N||t==c||r.push(t);return r},ee=function(e){for(var t,n=e===B,r=j(n?U:S(e)),o=[],a=0;r.length>a;)!i(F,t=r[a++])||n&&!i(B,t)||o.push(F[t]);return o};W||(s((P=function(){if(this instanceof P)throw TypeError("Symbol is not a constructor!");var e=l(arguments.length>0?arguments[0]:void 0),t=function(n){this===B&&t.call(U,n),i(this,N)&&i(this[N],e)&&(this[N][e]=!1),V(this,e,I(1,n))};return o&&z&&V(B,e,{configurable:!0,set:t}),G(e)}).prototype,"toString",function(){return this._k}),k.f=Q,O.f=X,n(40).f=R.f=Z,n(53).f=K,C.f=ee,o&&!n(31)&&s(B,"propertyIsEnumerable",K,!0),f.f=function(e){return G(m(e))}),a(a.G+a.W+a.F*!W,{Symbol:P});for(var te="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),ne=0;te.length>ne;)m(te[ne++]);for(var re=$(m.store),ie=0;re.length>ie;)h(re[ie++]);a(a.S+a.F*!W,"Symbol",{for:function(e){return i(q,e+="")?q[e]:q[e]=P(e)},keyFor:function(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var t in q)if(q[t]===e)return t},useSetter:function(){z=!0},useSimple:function(){z=!1}}),a(a.S+a.F*!W,"Object",{create:function(e,t){return void 0===t?T(e):Y(T(e),t)},defineProperty:X,defineProperties:Y,getOwnPropertyDescriptor:Q,getOwnPropertyNames:Z,getOwnPropertySymbols:ee});var oe=p(function(){C.f(1)});a(a.S+a.F*oe,"Object",{getOwnPropertySymbols:function(e){return C.f(w(e))}}),A&&a(a.S+a.F*(!W||p(function(){var e=P();return"[null]"!=M([e])||"{}"!=M({a:e})||"{}"!=M(Object(e))})),"JSON",{stringify:function(e){for(var t,n,r=[e],i=1;arguments.length>i;)r.push(arguments[i++]);if(n=t=r[1],(v(t)||void 0!==e)&&!J(e))return y(t)||(t=function(e,t){if("function"==typeof n&&(t=n.call(this,e,t)),!J(t))return t}),r[1]=t,M.apply(A,r)}}),P.prototype[_]||n(12)(P.prototype,_,P.prototype.valueOf),u(P,"Symbol"),u(Math,"Math",!0),u(r.JSON,"JSON",!0)},function(e,t,n){e.exports=n(51)("native-function-to-string",Function.toString)},function(e,t,n){var r=n(37),i=n(60),o=n(53);e.exports=function(e){var t=r(e),n=i.f;if(n)for(var a,s=n(e),c=o.f,p=0;s.length>p;)c.call(e,a=s[p++])&&t.push(a);return t}},function(e,t,n){var r=n(0);r(r.S,"Object",{create:n(39)})},function(e,t,n){var r=n(0);r(r.S+r.F*!n(7),"Object",{defineProperty:n(8).f})},function(e,t,n){var r=n(0);r(r.S+r.F*!n(7),"Object",{defineProperties:n(106)})},function(e,t,n){var r=n(16),i=n(17).f;n(26)("getOwnPropertyDescriptor",function(){return function(e,t){return i(r(e),t)}})},function(e,t,n){var r=n(9),i=n(18);n(26)("getPrototypeOf",function(){return function(e){return i(r(e))}})},function(e,t,n){var r=n(9),i=n(37);n(26)("keys",function(){return function(e){return i(r(e))}})},function(e,t,n){n(26)("getOwnPropertyNames",function(){return n(107).f})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("freeze",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("seal",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("preventExtensions",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4);n(26)("isFrozen",function(e){return function(t){return!r(t)||!!e&&e(t)}})},function(e,t,n){var r=n(4);n(26)("isSealed",function(e){return function(t){return!r(t)||!!e&&e(t)}})},function(e,t,n){var r=n(4);n(26)("isExtensible",function(e){return function(t){return!!r(t)&&(!e||e(t))}})},function(e,t,n){var r=n(0);r(r.S+r.F,"Object",{assign:n(108)})},function(e,t,n){var r=n(0);r(r.S,"Object",{is:n(109)})},function(e,t,n){var r=n(0);r(r.S,"Object",{setPrototypeOf:n(81).set})},function(e,t,n){"use strict";var r=n(47),i={};i[n(5)("toStringTag")]="z",i+""!="[object z]"&&n(13)(Object.prototype,"toString",function(){return"[object "+r(this)+"]"},!0)},function(e,t,n){var r=n(0);r(r.P,"Function",{bind:n(110)})},function(e,t,n){var r=n(8).f,i=Function.prototype,o=/^\s*function ([^ (]*)/;"name"in i||n(7)&&r(i,"name",{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(e){return""}}})},function(e,t,n){"use strict";var r=n(4),i=n(18),o=n(5)("hasInstance"),a=Function.prototype;o in a||n(8).f(a,o,{value:function(e){if("function"!=typeof this||!r(e))return!1;if(!r(this.prototype))return e instanceof this;for(;e=i(e);)if(this.prototype===e)return!0;return!1}})},function(e,t,n){var r=n(0),i=n(112);r(r.G+r.F*(parseInt!=i),{parseInt:i})},function(e,t,n){var r=n(0),i=n(113);r(r.G+r.F*(parseFloat!=i),{parseFloat:i})},function(e,t,n){"use strict";var r=n(2),i=n(15),o=n(21),a=n(83),s=n(24),c=n(3),p=n(40).f,d=n(17).f,u=n(8).f,l=n(48).trim,m=r.Number,f=m,h=m.prototype,g="Number"==o(n(39)(h)),y="trim"in String.prototype,b=function(e){var t=s(e,!1);if("string"==typeof t&&t.length>2){var n,r,i,o=(t=y?t.trim():l(t,3)).charCodeAt(0);if(43===o||45===o){if(88===(n=t.charCodeAt(2))||120===n)return NaN}else if(48===o){switch(t.charCodeAt(1)){case 66:case 98:r=2,i=49;break;case 79:case 111:r=8,i=55;break;default:return+t}for(var a,c=t.slice(2),p=0,d=c.length;p<d;p++)if((a=c.charCodeAt(p))<48||a>i)return NaN;return parseInt(c,r)}}return+t};if(!m(" 0o1")||!m("0b1")||m("+0x1")){m=function(e){var t=arguments.length<1?0:e,n=this;return n instanceof m&&(g?c(function(){h.valueOf.call(n)}):"Number"!=o(n))?a(new f(b(t)),n,m):b(t)};for(var v,w=n(7)?p(f):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),S=0;w.length>S;S++)i(f,v=w[S])&&!i(m,v)&&u(m,v,d(f,v));m.prototype=h,h.constructor=m,n(13)(r,"Number",m)}},function(e,t,n){"use strict";var r=n(0),i=n(22),o=n(114),a=n(84),s=1..toFixed,c=Math.floor,p=[0,0,0,0,0,0],d="Number.toFixed: incorrect invocation!",u=function(e,t){for(var n=-1,r=t;++n<6;)r+=e*p[n],p[n]=r%1e7,r=c(r/1e7)},l=function(e){for(var t=6,n=0;--t>=0;)n+=p[t],p[t]=c(n/e),n=n%e*1e7},m=function(){for(var e=6,t="";--e>=0;)if(""!==t||0===e||0!==p[e]){var n=String(p[e]);t=""===t?n:t+a.call("0",7-n.length)+n}return t},f=function(e,t,n){return 0===t?n:t%2==1?f(e,t-1,n*e):f(e*e,t/2,n)};r(r.P+r.F*(!!s&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!n(3)(function(){s.call({})})),"Number",{toFixed:function(e){var t,n,r,s,c=o(this,d),p=i(e),h="",g="0";if(p<0||p>20)throw RangeError(d);if(c!=c)return"NaN";if(c<=-1e21||c>=1e21)return String(c);if(c<0&&(h="-",c=-c),c>1e-21)if(n=(t=function(e){for(var t=0,n=e;n>=4096;)t+=12,n/=4096;for(;n>=2;)t+=1,n/=2;return t}(c*f(2,69,1))-69)<0?c*f(2,-t,1):c/f(2,t,1),n*=4503599627370496,(t=52-t)>0){for(u(0,n),r=p;r>=7;)u(1e7,0),r-=7;for(u(f(10,r,1),0),r=t-1;r>=23;)l(1<<23),r-=23;l(1<<r),u(1,1),l(2),g=m()}else u(0,n),u(1<<-t,0),g=m()+a.call("0",p);return g=p>0?h+((s=g.length)<=p?"0."+a.call("0",p-s)+g:g.slice(0,s-p)+"."+g.slice(s-p)):h+g}})},function(e,t,n){"use strict";var r=n(0),i=n(3),o=n(114),a=1..toPrecision;r(r.P+r.F*(i(function(){return"1"!==a.call(1,void 0)})||!i(function(){a.call({})})),"Number",{toPrecision:function(e){var t=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?a.call(t):a.call(t,e)}})},function(e,t,n){var r=n(0);r(r.S,"Number",{EPSILON:Math.pow(2,-52)})},function(e,t,n){var r=n(0),i=n(2).isFinite;r(r.S,"Number",{isFinite:function(e){return"number"==typeof e&&i(e)}})},function(e,t,n){var r=n(0);r(r.S,"Number",{isInteger:n(115)})},function(e,t,n){var r=n(0);r(r.S,"Number",{isNaN:function(e){return e!=e}})},function(e,t,n){var r=n(0),i=n(115),o=Math.abs;r(r.S,"Number",{isSafeInteger:function(e){return i(e)&&o(e)<=9007199254740991}})},function(e,t,n){var r=n(0);r(r.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},function(e,t,n){var r=n(0);r(r.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},function(e,t,n){var r=n(0),i=n(113);r(r.S+r.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},function(e,t,n){var r=n(0),i=n(112);r(r.S+r.F*(Number.parseInt!=i),"Number",{parseInt:i})},function(e,t,n){var r=n(0),i=n(116),o=Math.sqrt,a=Math.acosh;r(r.S+r.F*!(a&&710==Math.floor(a(Number.MAX_VALUE))&&a(1/0)==1/0),"Math",{acosh:function(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},function(e,t,n){var r=n(0),i=Math.asinh;r(r.S+r.F*!(i&&1/i(0)>0),"Math",{asinh:function e(t){return isFinite(t=+t)&&0!=t?t<0?-e(-t):Math.log(t+Math.sqrt(t*t+1)):t}})},function(e,t,n){var r=n(0),i=Math.atanh;r(r.S+r.F*!(i&&1/i(-0)<0),"Math",{atanh:function(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},function(e,t,n){var r=n(0),i=n(85);r(r.S,"Math",{cbrt:function(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{clz32:function(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},function(e,t,n){var r=n(0),i=Math.exp;r(r.S,"Math",{cosh:function(e){return(i(e=+e)+i(-e))/2}})},function(e,t,n){var r=n(0),i=n(86);r(r.S+r.F*(i!=Math.expm1),"Math",{expm1:i})},function(e,t,n){var r=n(0);r(r.S,"Math",{fround:n(117)})},function(e,t,n){var r=n(0),i=Math.abs;r(r.S,"Math",{hypot:function(e,t){for(var n,r,o=0,a=0,s=arguments.length,c=0;a<s;)c<(n=i(arguments[a++]))?(o=o*(r=c/n)*r+1,c=n):o+=n>0?(r=n/c)*r:n;return c===1/0?1/0:c*Math.sqrt(o)}})},function(e,t,n){var r=n(0),i=Math.imul;r(r.S+r.F*n(3)(function(){return-5!=i(4294967295,5)||2!=i.length}),"Math",{imul:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r;return 0|i*o+((65535&n>>>16)*o+i*(65535&r>>>16)<<16>>>0)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{log10:function(e){return Math.log(e)*Math.LOG10E}})},function(e,t,n){var r=n(0);r(r.S,"Math",{log1p:n(116)})},function(e,t,n){var r=n(0);r(r.S,"Math",{log2:function(e){return Math.log(e)/Math.LN2}})},function(e,t,n){var r=n(0);r(r.S,"Math",{sign:n(85)})},function(e,t,n){var r=n(0),i=n(86),o=Math.exp;r(r.S+r.F*n(3)(function(){return-2e-17!=!Math.sinh(-2e-17)}),"Math",{sinh:function(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},function(e,t,n){var r=n(0),i=n(86),o=Math.exp;r(r.S,"Math",{tanh:function(e){var t=i(e=+e),n=i(-e);return t==1/0?1:n==1/0?-1:(t-n)/(o(e)+o(-e))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{trunc:function(e){return(e>0?Math.floor:Math.ceil)(e)}})},function(e,t,n){var r=n(0),i=n(38),o=String.fromCharCode,a=String.fromCodePoint;r(r.S+r.F*(!!a&&1!=a.length),"String",{fromCodePoint:function(e){for(var t,n=[],r=arguments.length,a=0;r>a;){if(t=+arguments[a++],i(t,1114111)!==t)throw RangeError(t+" is not a valid code point");n.push(t<65536?o(t):o(55296+((t-=65536)>>10),t%1024+56320))}return n.join("")}})},function(e,t,n){var r=n(0),i=n(16),o=n(6);r(r.S,"String",{raw:function(e){for(var t=i(e.raw),n=o(t.length),r=arguments.length,a=[],s=0;n>s;)a.push(String(t[s++])),s<r&&a.push(String(arguments[s]));return a.join("")}})},function(e,t,n){"use strict";n(48)("trim",function(e){return function(){return e(this,3)}})},function(e,t,n){"use strict";var r=n(62)(!0);n(87)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){"use strict";var r=n(0),i=n(62)(!1);r(r.P,"String",{codePointAt:function(e){return i(this,e)}})},function(e,t,n){"use strict";var r=n(0),i=n(6),o=n(89),a="".endsWith;r(r.P+r.F*n(90)("endsWith"),"String",{endsWith:function(e){var t=o(this,e,"endsWith"),n=arguments.length>1?arguments[1]:void 0,r=i(t.length),s=void 0===n?r:Math.min(i(n),r),c=String(e);return a?a.call(t,c,s):t.slice(s-c.length,s)===c}})},function(e,t,n){"use strict";var r=n(0),i=n(89);r(r.P+r.F*n(90)("includes"),"String",{includes:function(e){return!!~i(this,e,"includes").indexOf(e,arguments.length>1?arguments[1]:void 0)}})},function(e,t,n){var r=n(0);r(r.P,"String",{repeat:n(84)})},function(e,t,n){"use strict";var r=n(0),i=n(6),o=n(89),a="".startsWith;r(r.P+r.F*n(90)("startsWith"),"String",{startsWith:function(e){var t=o(this,e,"startsWith"),n=i(Math.min(arguments.length>1?arguments[1]:void 0,t.length)),r=String(e);return a?a.call(t,r,n):t.slice(n,n+r.length)===r}})},function(e,t,n){"use strict";n(14)("anchor",function(e){return function(t){return e(this,"a","name",t)}})},function(e,t,n){"use strict";n(14)("big",function(e){return function(){return e(this,"big","","")}})},function(e,t,n){"use strict";n(14)("blink",function(e){return function(){return e(this,"blink","","")}})},function(e,t,n){"use strict";n(14)("bold",function(e){return function(){return e(this,"b","","")}})},function(e,t,n){"use strict";n(14)("fixed",function(e){return function(){return e(this,"tt","","")}})},function(e,t,n){"use strict";n(14)("fontcolor",function(e){return function(t){return e(this,"font","color",t)}})},function(e,t,n){"use strict";n(14)("fontsize",function(e){return function(t){return e(this,"font","size",t)}})},function(e,t,n){"use strict";n(14)("italics",function(e){return function(){return e(this,"i","","")}})},function(e,t,n){"use strict";n(14)("link",function(e){return function(t){return e(this,"a","href",t)}})},function(e,t,n){"use strict";n(14)("small",function(e){return function(){return e(this,"small","","")}})},function(e,t,n){"use strict";n(14)("strike",function(e){return function(){return e(this,"strike","","")}})},function(e,t,n){"use strict";n(14)("sub",function(e){return function(){return e(this,"sub","","")}})},function(e,t,n){"use strict";n(14)("sup",function(e){return function(){return e(this,"sup","","")}})},function(e,t,n){var r=n(0);r(r.S,"Date",{now:function(){return(new Date).getTime()}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24);r(r.P+r.F*n(3)(function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})}),"Date",{toJSON:function(e){var t=i(this),n=o(t);return"number"!=typeof n||isFinite(n)?t.toISOString():null}})},function(e,t,n){var r=n(0),i=n(234);r(r.P+r.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},function(e,t,n){"use strict";var r=n(3),i=Date.prototype.getTime,o=Date.prototype.toISOString,a=function(e){return e>9?e:"0"+e};e.exports=r(function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-5e13-1))})||!r(function(){o.call(new Date(NaN))})?function(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,t=e.getUTCFullYear(),n=e.getUTCMilliseconds(),r=t<0?"-":t>9999?"+":"";return r+("00000"+Math.abs(t)).slice(r?-6:-4)+"-"+a(e.getUTCMonth()+1)+"-"+a(e.getUTCDate())+"T"+a(e.getUTCHours())+":"+a(e.getUTCMinutes())+":"+a(e.getUTCSeconds())+"."+(n>99?n:"0"+a(n))+"Z"}:o},function(e,t,n){var r=Date.prototype,i=r.toString,o=r.getTime;new Date(NaN)+""!="Invalid Date"&&n(13)(r,"toString",function(){var e=o.call(this);return e==e?i.call(this):"Invalid Date"})},function(e,t,n){var r=n(5)("toPrimitive"),i=Date.prototype;r in i||n(12)(i,r,n(237))},function(e,t,n){"use strict";var r=n(1),i=n(24);e.exports=function(e){if("string"!==e&&"number"!==e&&"default"!==e)throw TypeError("Incorrect hint");return i(r(this),"number"!=e)}},function(e,t,n){var r=n(0);r(r.S,"Array",{isArray:n(61)})},function(e,t,n){"use strict";var r=n(20),i=n(0),o=n(9),a=n(118),s=n(91),c=n(6),p=n(92),d=n(93);i(i.S+i.F*!n(64)(function(e){Array.from(e)}),"Array",{from:function(e){var t,n,i,u,l=o(e),m="function"==typeof this?this:Array,f=arguments.length,h=f>1?arguments[1]:void 0,g=void 0!==h,y=0,b=d(l);if(g&&(h=r(h,f>2?arguments[2]:void 0,2)),null==b||m==Array&&s(b))for(n=new m(t=c(l.length));t>y;y++)p(n,y,g?h(l[y],y):l[y]);else for(u=b.call(l),n=new m;!(i=u.next()).done;y++)p(n,y,g?a(u,h,[i.value,y],!0):i.value);return n.length=y,n}})},function(e,t,n){"use strict";var r=n(0),i=n(92);r(r.S+r.F*n(3)(function(){function e(){}return!(Array.of.call(e)instanceof e)}),"Array",{of:function(){for(var e=0,t=arguments.length,n=new("function"==typeof this?this:Array)(t);t>e;)i(n,e,arguments[e++]);return n.length=t,n}})},function(e,t,n){"use strict";var r=n(0),i=n(16),o=[].join;r(r.P+r.F*(n(52)!=Object||!n(23)(o)),"Array",{join:function(e){return o.call(i(this),void 0===e?",":e)}})},function(e,t,n){"use strict";var r=n(0),i=n(80),o=n(21),a=n(38),s=n(6),c=[].slice;r(r.P+r.F*n(3)(function(){i&&c.call(i)}),"Array",{slice:function(e,t){var n=s(this.length),r=o(this);if(t=void 0===t?n:t,"Array"==r)return c.call(this,e,t);for(var i=a(e,n),p=a(t,n),d=s(p-i),u=new Array(d),l=0;l<d;l++)u[l]="String"==r?this.charAt(i+l):this[i+l];return u}})},function(e,t,n){"use strict";var r=n(0),i=n(10),o=n(9),a=n(3),s=[].sort,c=[1,2,3];r(r.P+r.F*(a(function(){c.sort(void 0)})||!a(function(){c.sort(null)})||!n(23)(s)),"Array",{sort:function(e){return void 0===e?s.call(o(this)):s.call(o(this),i(e))}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(0),o=n(23)([].forEach,!0);r(r.P+r.F*!o,"Array",{forEach:function(e){return i(this,e,arguments[1])}})},function(e,t,n){var r=n(4),i=n(61),o=n(5)("species");e.exports=function(e){var t;return i(e)&&("function"!=typeof(t=e.constructor)||t!==Array&&!i(t.prototype)||(t=void 0),r(t)&&null===(t=t[o])&&(t=void 0)),void 0===t?Array:t}},function(e,t,n){"use strict";var r=n(0),i=n(27)(1);r(r.P+r.F*!n(23)([].map,!0),"Array",{map:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(2);r(r.P+r.F*!n(23)([].filter,!0),"Array",{filter:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(3);r(r.P+r.F*!n(23)([].some,!0),"Array",{some:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(4);r(r.P+r.F*!n(23)([].every,!0),"Array",{every:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(119);r(r.P+r.F*!n(23)([].reduce,!0),"Array",{reduce:function(e){return i(this,e,arguments.length,arguments[1],!1)}})},function(e,t,n){"use strict";var r=n(0),i=n(119);r(r.P+r.F*!n(23)([].reduceRight,!0),"Array",{reduceRight:function(e){return i(this,e,arguments.length,arguments[1],!0)}})},function(e,t,n){"use strict";var r=n(0),i=n(59)(!1),o=[].indexOf,a=!!o&&1/[1].indexOf(1,-0)<0;r(r.P+r.F*(a||!n(23)(o)),"Array",{indexOf:function(e){return a?o.apply(this,arguments)||0:i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(16),o=n(22),a=n(6),s=[].lastIndexOf,c=!!s&&1/[1].lastIndexOf(1,-0)<0;r(r.P+r.F*(c||!n(23)(s)),"Array",{lastIndexOf:function(e){if(c)return s.apply(this,arguments)||0;var t=i(this),n=a(t.length),r=n-1;for(arguments.length>1&&(r=Math.min(r,o(arguments[1]))),r<0&&(r=n+r);r>=0;r--)if(r in t&&t[r]===e)return r||0;return-1}})},function(e,t,n){var r=n(0);r(r.P,"Array",{copyWithin:n(120)}),n(33)("copyWithin")},function(e,t,n){var r=n(0);r(r.P,"Array",{fill:n(95)}),n(33)("fill")},function(e,t,n){"use strict";var r=n(0),i=n(27)(5),o=!0;"find"in[]&&Array(1).find(function(){o=!1}),r(r.P+r.F*o,"Array",{find:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)("find")},function(e,t,n){"use strict";var r=n(0),i=n(27)(6),o="findIndex",a=!0;o in[]&&Array(1)[o](function(){a=!1}),r(r.P+r.F*a,"Array",{findIndex:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)(o)},function(e,t,n){n(41)("Array")},function(e,t,n){var r=n(2),i=n(83),o=n(8).f,a=n(40).f,s=n(63),c=n(54),p=r.RegExp,d=p,u=p.prototype,l=/a/g,m=/a/g,f=new p(l)!==l;if(n(7)&&(!f||n(3)(function(){return m[n(5)("match")]=!1,p(l)!=l||p(m)==m||"/a/i"!=p(l,"i")}))){p=function(e,t){var n=this instanceof p,r=s(e),o=void 0===t;return!n&&r&&e.constructor===p&&o?e:i(f?new d(r&&!o?e.source:e,t):d((r=e instanceof p)?e.source:e,r&&o?c.call(e):t),n?this:u,p)};for(var h=function(e){e in p||o(p,e,{configurable:!0,get:function(){return d[e]},set:function(t){d[e]=t}})},g=a(d),y=0;g.length>y;)h(g[y++]);u.constructor=p,p.prototype=u,n(13)(r,"RegExp",p)}n(41)("RegExp")},function(e,t,n){"use strict";n(123);var r=n(1),i=n(54),o=n(7),a=/./.toString,s=function(e){n(13)(RegExp.prototype,"toString",e,!0)};n(3)(function(){return"/a/b"!=a.call({source:"a",flags:"b"})})?s(function(){var e=r(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)}):"toString"!=a.name&&s(function(){return a.call(this)})},function(e,t,n){"use strict";var r=n(1),i=n(6),o=n(98),a=n(65);n(66)("match",1,function(e,t,n,s){return[function(n){var r=e(this),i=null==n?void 0:n[t];return void 0!==i?i.call(n,r):new RegExp(n)[t](String(r))},function(e){var t=s(n,e,this);if(t.done)return t.value;var c=r(e),p=String(this);if(!c.global)return a(c,p);var d=c.unicode;c.lastIndex=0;for(var u,l=[],m=0;null!==(u=a(c,p));){var f=String(u[0]);l[m]=f,""===f&&(c.lastIndex=o(p,i(c.lastIndex),d)),m++}return 0===m?null:l}]})},function(e,t,n){"use strict";var r=n(1),i=n(9),o=n(6),a=n(22),s=n(98),c=n(65),p=Math.max,d=Math.min,u=Math.floor,l=/\$([$&`']|\d\d?|<[^>]*>)/g,m=/\$([$&`']|\d\d?)/g;n(66)("replace",2,function(e,t,n,f){return[function(r,i){var o=e(this),a=null==r?void 0:r[t];return void 0!==a?a.call(r,o,i):n.call(String(o),r,i)},function(e,t){var i=f(n,e,this,t);if(i.done)return i.value;var u=r(e),l=String(this),m="function"==typeof t;m||(t=String(t));var g=u.global;if(g){var y=u.unicode;u.lastIndex=0}for(var b=[];;){var v=c(u,l);if(null===v)break;if(b.push(v),!g)break;""===String(v[0])&&(u.lastIndex=s(l,o(u.lastIndex),y))}for(var w,S="",x=0,I=0;I<b.length;I++){v=b[I];for(var T=String(v[0]),R=p(d(a(v.index),l.length),0),k=[],C=1;C<v.length;C++)k.push(void 0===(w=v[C])?w:String(w));var O=v.groups;if(m){var $=[T].concat(k,R,l);void 0!==O&&$.push(O);var E=String(t.apply(void 0,$))}else E=h(T,l,R,k,O,t);R>=x&&(S+=l.slice(x,R)+E,x=R+T.length)}return S+l.slice(x)}];function h(e,t,r,o,a,s){var c=r+e.length,p=o.length,d=m;return void 0!==a&&(a=i(a),d=l),n.call(s,d,function(n,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,r);case"'":return t.slice(c);case"<":s=a[i.slice(1,-1)];break;default:var d=+i;if(0===d)return n;if(d>p){var l=u(d/10);return 0===l?n:l<=p?void 0===o[l-1]?i.charAt(1):o[l-1]+i.charAt(1):n}s=o[d-1]}return void 0===s?"":s})}})},function(e,t,n){"use strict";var r=n(1),i=n(109),o=n(65);n(66)("search",1,function(e,t,n,a){return[function(n){var r=e(this),i=null==n?void 0:n[t];return void 0!==i?i.call(n,r):new RegExp(n)[t](String(r))},function(e){var t=a(n,e,this);if(t.done)return t.value;var s=r(e),c=String(this),p=s.lastIndex;i(p,0)||(s.lastIndex=0);var d=o(s,c);return i(s.lastIndex,p)||(s.lastIndex=p),null===d?-1:d.index}]})},function(e,t,n){"use strict";var r=n(63),i=n(1),o=n(55),a=n(98),s=n(6),c=n(65),p=n(97),d=n(3),u=Math.min,l=[].push,m=!d(function(){RegExp(4294967295,"y")});n(66)("split",2,function(e,t,n,d){var f;return f="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(e,t){var i=String(this);if(void 0===e&&0===t)return[];if(!r(e))return n.call(i,e,t);for(var o,a,s,c=[],d=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.unicode?"u":"")+(e.sticky?"y":""),u=0,m=void 0===t?4294967295:t>>>0,f=new RegExp(e.source,d+"g");(o=p.call(f,i))&&!((a=f.lastIndex)>u&&(c.push(i.slice(u,o.index)),o.length>1&&o.index<i.length&&l.apply(c,o.slice(1)),s=o[0].length,u=a,c.length>=m));)f.lastIndex===o.index&&f.lastIndex++;return u===i.length?!s&&f.test("")||c.push(""):c.push(i.slice(u)),c.length>m?c.slice(0,m):c}:"0".split(void 0,0).length?function(e,t){return void 0===e&&0===t?[]:n.call(this,e,t)}:n,[function(n,r){var i=e(this),o=null==n?void 0:n[t];return void 0!==o?o.call(n,i,r):f.call(String(i),n,r)},function(e,t){var r=d(f,e,this,t,f!==n);if(r.done)return r.value;var p=i(e),l=String(this),h=o(p,RegExp),g=p.unicode,y=(p.ignoreCase?"i":"")+(p.multiline?"m":"")+(p.unicode?"u":"")+(m?"y":"g"),b=new h(m?p:"^(?:"+p.source+")",y),v=void 0===t?4294967295:t>>>0;if(0===v)return[];if(0===l.length)return null===c(b,l)?[l]:[];for(var w=0,S=0,x=[];S<l.length;){b.lastIndex=m?S:0;var I,T=c(b,m?l:l.slice(S));if(null===T||(I=u(s(b.lastIndex+(m?0:S)),l.length))===w)S=a(l,S,g);else{if(x.push(l.slice(w,S)),x.length===v)return x;for(var R=1;R<=T.length-1;R++)if(x.push(T[R]),x.length===v)return x;S=w=I}}return x.push(l.slice(w)),x}]})},function(e,t,n){"use strict";var r,i,o,a,s=n(31),c=n(2),p=n(20),d=n(47),u=n(0),l=n(4),m=n(10),f=n(42),h=n(43),g=n(55),y=n(99).set,b=n(100)(),v=n(101),w=n(124),S=n(67),x=n(125),I=c.TypeError,T=c.process,R=T&&T.versions,k=R&&R.v8||"",C=c.Promise,O="process"==d(T),$=function(){},E=i=v.f,D=!!function(){try{var e=C.resolve(1),t=(e.constructor={})[n(5)("species")]=function(e){e($,$)};return(O||"function"==typeof PromiseRejectionEvent)&&e.then($)instanceof t&&0!==k.indexOf("6.6")&&-1===S.indexOf("Chrome/66")}catch(e){}}(),j=function(e){var t;return!(!l(e)||"function"!=typeof(t=e.then))&&t},P=function(e,t){if(!e._n){e._n=!0;var n=e._c;b(function(){for(var r=e._v,i=1==e._s,o=0,a=function(t){var n,o,a,s=i?t.ok:t.fail,c=t.resolve,p=t.reject,d=t.domain;try{s?(i||(2==e._h&&N(e),e._h=1),!0===s?n=r:(d&&d.enter(),n=s(r),d&&(d.exit(),a=!0)),n===t.promise?p(I("Promise-chain cycle")):(o=j(n))?o.call(n,c,p):c(n)):p(r)}catch(e){d&&!a&&d.exit(),p(e)}};n.length>o;)a(n[o++]);e._c=[],e._n=!1,t&&!e._h&&A(e)})}},A=function(e){y.call(c,function(){var t,n,r,i=e._v,o=M(e);if(o&&(t=w(function(){O?T.emit("unhandledRejection",i,e):(n=c.onunhandledrejection)?n({promise:e,reason:i}):(r=c.console)&&r.error&&r.error("Unhandled promise rejection",i)}),e._h=O||M(e)?2:1),e._a=void 0,o&&t.e)throw t.v})},M=function(e){return 1!==e._h&&0===(e._a||e._c).length},N=function(e){y.call(c,function(){var t;O?T.emit("rejectionHandled",e):(t=c.onrejectionhandled)&&t({promise:e,reason:e._v})})},_=function(e){var t=this;t._d||(t._d=!0,(t=t._w||t)._v=e,t._s=2,t._a||(t._a=t._c.slice()),P(t,!0))},L=function(e){var t,n=this;if(!n._d){n._d=!0,n=n._w||n;try{if(n===e)throw I("Promise can't be resolved itself");(t=j(e))?b(function(){var r={_w:n,_d:!1};try{t.call(e,p(L,r,1),p(_,r,1))}catch(e){_.call(r,e)}}):(n._v=e,n._s=1,P(n,!1))}catch(e){_.call({_w:n,_d:!1},e)}}};D||(C=function(e){f(this,C,"Promise","_h"),m(e),r.call(this);try{e(p(L,this,1),p(_,this,1))}catch(e){_.call(this,e)}},(r=function(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=n(44)(C.prototype,{then:function(e,t){var n=E(g(this,C));return n.ok="function"!=typeof e||e,n.fail="function"==typeof t&&t,n.domain=O?T.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&P(this,!1),n.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new r;this.promise=e,this.resolve=p(L,e,1),this.reject=p(_,e,1)},v.f=E=function(e){return e===C||e===a?new o(e):i(e)}),u(u.G+u.W+u.F*!D,{Promise:C}),n(46)(C,"Promise"),n(41)("Promise"),a=n(19).Promise,u(u.S+u.F*!D,"Promise",{reject:function(e){var t=E(this);return(0,t.reject)(e),t.promise}}),u(u.S+u.F*(s||!D),"Promise",{resolve:function(e){return x(s&&this===a?C:this,e)}}),u(u.S+u.F*!(D&&n(64)(function(e){C.all(e).catch($)})),"Promise",{all:function(e){var t=this,n=E(t),r=n.resolve,i=n.reject,o=w(function(){var n=[],o=0,a=1;h(e,!1,function(e){var s=o++,c=!1;n.push(void 0),a++,t.resolve(e).then(function(e){c||(c=!0,n[s]=e,--a||r(n))},i)}),--a||r(n)});return o.e&&i(o.v),n.promise},race:function(e){var t=this,n=E(t),r=n.reject,i=w(function(){h(e,!1,function(e){t.resolve(e).then(n.resolve,r)})});return i.e&&r(i.v),n.promise}})},function(e,t,n){"use strict";var r=n(130),i=n(45);n(68)("WeakSet",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{add:function(e){return r.def(i(this,"WeakSet"),e,!0)}},r,!1,!0)},function(e,t,n){"use strict";var r=n(0),i=n(69),o=n(102),a=n(1),s=n(38),c=n(6),p=n(4),d=n(2).ArrayBuffer,u=n(55),l=o.ArrayBuffer,m=o.DataView,f=i.ABV&&d.isView,h=l.prototype.slice,g=i.VIEW;r(r.G+r.W+r.F*(d!==l),{ArrayBuffer:l}),r(r.S+r.F*!i.CONSTR,"ArrayBuffer",{isView:function(e){return f&&f(e)||p(e)&&g in e}}),r(r.P+r.U+r.F*n(3)(function(){return!new l(2).slice(1,void 0).byteLength}),"ArrayBuffer",{slice:function(e,t){if(void 0!==h&&void 0===t)return h.call(a(this),e);for(var n=a(this).byteLength,r=s(e,n),i=s(void 0===t?n:t,n),o=new(u(this,l))(c(i-r)),p=new m(this),d=new m(o),f=0;r<i;)d.setUint8(f++,p.getUint8(r++));return o}}),n(41)("ArrayBuffer")},function(e,t,n){var r=n(0);r(r.G+r.W+r.F*!n(69).ABV,{DataView:n(102).DataView})},function(e,t,n){n(28)("Int8",1,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint8",1,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint8",1,function(e){return function(t,n,r){return e(this,t,n,r)}},!0)},function(e,t,n){n(28)("Int16",2,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint16",2,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Int32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Float32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Float64",8,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){var r=n(0),i=n(10),o=n(1),a=(n(2).Reflect||{}).apply,s=Function.apply;r(r.S+r.F*!n(3)(function(){a(function(){})}),"Reflect",{apply:function(e,t,n){var r=i(e),c=o(n);return a?a(r,t,c):s.call(r,t,c)}})},function(e,t,n){var r=n(0),i=n(39),o=n(10),a=n(1),s=n(4),c=n(3),p=n(110),d=(n(2).Reflect||{}).construct,u=c(function(){function e(){}return!(d(function(){},[],e)instanceof e)}),l=!c(function(){d(function(){})});r(r.S+r.F*(u||l),"Reflect",{construct:function(e,t){o(e),a(t);var n=arguments.length<3?e:o(arguments[2]);if(l&&!u)return d(e,t,n);if(e==n){switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3])}var r=[null];return r.push.apply(r,t),new(p.apply(e,r))}var c=n.prototype,m=i(s(c)?c:Object.prototype),f=Function.apply.call(e,m,t);return s(f)?f:m}})},function(e,t,n){var r=n(8),i=n(0),o=n(1),a=n(24);i(i.S+i.F*n(3)(function(){Reflect.defineProperty(r.f({},1,{value:1}),1,{value:2})}),"Reflect",{defineProperty:function(e,t,n){o(e),t=a(t,!0),o(n);try{return r.f(e,t,n),!0}catch(e){return!1}}})},function(e,t,n){var r=n(0),i=n(17).f,o=n(1);r(r.S,"Reflect",{deleteProperty:function(e,t){var n=i(o(e),t);return!(n&&!n.configurable)&&delete e[t]}})},function(e,t,n){"use strict";var r=n(0),i=n(1),o=function(e){this._t=i(e),this._i=0;var t,n=this._k=[];for(t in e)n.push(t)};n(88)(o,"Object",function(){var e,t=this._k;do{if(this._i>=t.length)return{value:void 0,done:!0}}while(!((e=t[this._i++])in this._t));return{value:e,done:!1}}),r(r.S,"Reflect",{enumerate:function(e){return new o(e)}})},function(e,t,n){var r=n(17),i=n(18),o=n(15),a=n(0),s=n(4),c=n(1);a(a.S,"Reflect",{get:function e(t,n){var a,p,d=arguments.length<3?t:arguments[2];return c(t)===d?t[n]:(a=r.f(t,n))?o(a,"value")?a.value:void 0!==a.get?a.get.call(d):void 0:s(p=i(t))?e(p,n,d):void 0}})},function(e,t,n){var r=n(17),i=n(0),o=n(1);i(i.S,"Reflect",{getOwnPropertyDescriptor:function(e,t){return r.f(o(e),t)}})},function(e,t,n){var r=n(0),i=n(18),o=n(1);r(r.S,"Reflect",{getPrototypeOf:function(e){return i(o(e))}})},function(e,t,n){var r=n(0);r(r.S,"Reflect",{has:function(e,t){return t in e}})},function(e,t,n){var r=n(0),i=n(1),o=Object.isExtensible;r(r.S,"Reflect",{isExtensible:function(e){return i(e),!o||o(e)}})},function(e,t,n){var r=n(0);r(r.S,"Reflect",{ownKeys:n(132)})},function(e,t,n){var r=n(0),i=n(1),o=Object.preventExtensions;r(r.S,"Reflect",{preventExtensions:function(e){i(e);try{return o&&o(e),!0}catch(e){return!1}}})},function(e,t,n){var r=n(8),i=n(17),o=n(18),a=n(15),s=n(0),c=n(35),p=n(1),d=n(4);s(s.S,"Reflect",{set:function e(t,n,s){var u,l,m=arguments.length<4?t:arguments[3],f=i.f(p(t),n);if(!f){if(d(l=o(t)))return e(l,n,s,m);f=c(0)}if(a(f,"value")){if(!1===f.writable||!d(m))return!1;if(u=i.f(m,n)){if(u.get||u.set||!1===u.writable)return!1;u.value=s,r.f(m,n,u)}else r.f(m,n,c(0,s));return!0}return void 0!==f.set&&(f.set.call(m,s),!0)}})},function(e,t,n){var r=n(0),i=n(81);i&&r(r.S,"Reflect",{setPrototypeOf:function(e,t){i.check(e,t);try{return i.set(e,t),!0}catch(e){return!1}}})},function(e,t,n){"use strict";var r=n(0),i=n(59)(!0);r(r.P,"Array",{includes:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)("includes")},function(e,t,n){"use strict";var r=n(0),i=n(133),o=n(9),a=n(6),s=n(10),c=n(94);r(r.P,"Array",{flatMap:function(e){var t,n,r=o(this);return s(e),t=a(r.length),n=c(r,0),i(n,r,r,t,0,1,e,arguments[1]),n}}),n(33)("flatMap")},function(e,t,n){"use strict";var r=n(0),i=n(133),o=n(9),a=n(6),s=n(22),c=n(94);r(r.P,"Array",{flatten:function(){var e=arguments[0],t=o(this),n=a(t.length),r=c(t,0);return i(r,t,t,n,0,void 0===e?1:s(e)),r}}),n(33)("flatten")},function(e,t,n){"use strict";var r=n(0),i=n(62)(!0);r(r.P,"String",{at:function(e){return i(this,e)}})},function(e,t,n){"use strict";var r=n(0),i=n(134),o=n(67),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);r(r.P+r.F*a,"String",{padStart:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},function(e,t,n){"use strict";var r=n(0),i=n(134),o=n(67),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);r(r.P+r.F*a,"String",{padEnd:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},function(e,t,n){"use strict";n(48)("trimLeft",function(e){return function(){return e(this,1)}},"trimStart")},function(e,t,n){"use strict";n(48)("trimRight",function(e){return function(){return e(this,2)}},"trimEnd")},function(e,t,n){"use strict";var r=n(0),i=n(25),o=n(6),a=n(63),s=n(54),c=RegExp.prototype,p=function(e,t){this._r=e,this._s=t};n(88)(p,"RegExp String",function(){var e=this._r.exec(this._s);return{value:e,done:null===e}}),r(r.P,"String",{matchAll:function(e){if(i(this),!a(e))throw TypeError(e+" is not a regexp!");var t=String(this),n="flags"in c?String(e.flags):s.call(e),r=new RegExp(e.source,~n.indexOf("g")?n:"g"+n);return r.lastIndex=o(e.lastIndex),new p(r,t)}})},function(e,t,n){n(77)("asyncIterator")},function(e,t,n){n(77)("observable")},function(e,t,n){var r=n(0),i=n(132),o=n(16),a=n(17),s=n(92);r(r.S,"Object",{getOwnPropertyDescriptors:function(e){for(var t,n,r=o(e),c=a.f,p=i(r),d={},u=0;p.length>u;)void 0!==(n=c(r,t=p[u++]))&&s(d,t,n);return d}})},function(e,t,n){var r=n(0),i=n(135)(!1);r(r.S,"Object",{values:function(e){return i(e)}})},function(e,t,n){var r=n(0),i=n(135)(!0);r(r.S,"Object",{entries:function(e){return i(e)}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(10),a=n(8);n(7)&&r(r.P+n(70),"Object",{__defineGetter__:function(e,t){a.f(i(this),e,{get:o(t),enumerable:!0,configurable:!0})}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(10),a=n(8);n(7)&&r(r.P+n(70),"Object",{__defineSetter__:function(e,t){a.f(i(this),e,{set:o(t),enumerable:!0,configurable:!0})}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24),a=n(18),s=n(17).f;n(7)&&r(r.P+n(70),"Object",{__lookupGetter__:function(e){var t,n=i(this),r=o(e,!0);do{if(t=s(n,r))return t.get}while(n=a(n))}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24),a=n(18),s=n(17).f;n(7)&&r(r.P+n(70),"Object",{__lookupSetter__:function(e){var t,n=i(this),r=o(e,!0);do{if(t=s(n,r))return t.set}while(n=a(n))}})},function(e,t,n){var r=n(0);r(r.P+r.R,"Map",{toJSON:n(136)("Map")})},function(e,t,n){var r=n(0);r(r.P+r.R,"Set",{toJSON:n(136)("Set")})},function(e,t,n){n(71)("Map")},function(e,t,n){n(71)("Set")},function(e,t,n){n(71)("WeakMap")},function(e,t,n){n(71)("WeakSet")},function(e,t,n){n(72)("Map")},function(e,t,n){n(72)("Set")},function(e,t,n){n(72)("WeakMap")},function(e,t,n){n(72)("WeakSet")},function(e,t,n){var r=n(0);r(r.G,{global:n(2)})},function(e,t,n){var r=n(0);r(r.S,"System",{global:n(2)})},function(e,t,n){var r=n(0),i=n(21);r(r.S,"Error",{isError:function(e){return"Error"===i(e)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{clamp:function(e,t,n){return Math.min(n,Math.max(t,e))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{DEG_PER_RAD:Math.PI/180})},function(e,t,n){var r=n(0),i=180/Math.PI;r(r.S,"Math",{degrees:function(e){return e*i}})},function(e,t,n){var r=n(0),i=n(138),o=n(117);r(r.S,"Math",{fscale:function(e,t,n,r,a){return o(i(e,t,n,r,a))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{iaddh:function(e,t,n,r){var i=e>>>0,o=n>>>0;return(t>>>0)+(r>>>0)+((i&o|(i|o)&~(i+o>>>0))>>>31)|0}})},function(e,t,n){var r=n(0);r(r.S,"Math",{isubh:function(e,t,n,r){var i=e>>>0,o=n>>>0;return(t>>>0)-(r>>>0)-((~i&o|~(i^o)&i-o>>>0)>>>31)|0}})},function(e,t,n){var r=n(0);r(r.S,"Math",{imulh:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r,a=n>>16,s=r>>16,c=(a*o>>>0)+(i*o>>>16);return a*s+(c>>16)+((i*s>>>0)+(65535&c)>>16)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{RAD_PER_DEG:180/Math.PI})},function(e,t,n){var r=n(0),i=Math.PI/180;r(r.S,"Math",{radians:function(e){return e*i}})},function(e,t,n){var r=n(0);r(r.S,"Math",{scale:n(138)})},function(e,t,n){var r=n(0);r(r.S,"Math",{umulh:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r,a=n>>>16,s=r>>>16,c=(a*o>>>0)+(i*o>>>16);return a*s+(c>>>16)+((i*s>>>0)+(65535&c)>>>16)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{signbit:function(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},function(e,t,n){"use strict";var r=n(0),i=n(19),o=n(2),a=n(55),s=n(125);r(r.P+r.R,"Promise",{finally:function(e){var t=a(this,i.Promise||o.Promise),n="function"==typeof e;return this.then(n?function(n){return s(t,e()).then(function(){return n})}:e,n?function(n){return s(t,e()).then(function(){throw n})}:e)}})},function(e,t,n){"use strict";var r=n(0),i=n(101),o=n(124);r(r.S,"Promise",{try:function(e){var t=i.f(this),n=o(e);return(n.e?t.reject:t.resolve)(n.v),t.promise}})},function(e,t,n){var r=n(29),i=n(1),o=r.key,a=r.set;r.exp({defineMetadata:function(e,t,n,r){a(e,t,i(n),o(r))}})},function(e,t,n){var r=n(29),i=n(1),o=r.key,a=r.map,s=r.store;r.exp({deleteMetadata:function(e,t){var n=arguments.length<3?void 0:o(arguments[2]),r=a(i(t),n,!1);if(void 0===r||!r.delete(e))return!1;if(r.size)return!0;var c=s.get(t);return c.delete(n),!!c.size||s.delete(t)}})},function(e,t,n){var r=n(29),i=n(1),o=n(18),a=r.has,s=r.get,c=r.key,p=function(e,t,n){if(a(e,t,n))return s(e,t,n);var r=o(t);return null!==r?p(e,r,n):void 0};r.exp({getMetadata:function(e,t){return p(e,i(t),arguments.length<3?void 0:c(arguments[2]))}})},function(e,t,n){var r=n(128),i=n(137),o=n(29),a=n(1),s=n(18),c=o.keys,p=o.key,d=function(e,t){var n=c(e,t),o=s(e);if(null===o)return n;var a=d(o,t);return a.length?n.length?i(new r(n.concat(a))):a:n};o.exp({getMetadataKeys:function(e){return d(a(e),arguments.length<2?void 0:p(arguments[1]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.get,a=r.key;r.exp({getOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.keys,a=r.key;r.exp({getOwnMetadataKeys:function(e){return o(i(e),arguments.length<2?void 0:a(arguments[1]))}})},function(e,t,n){var r=n(29),i=n(1),o=n(18),a=r.has,s=r.key,c=function(e,t,n){if(a(e,t,n))return!0;var r=o(t);return null!==r&&c(e,r,n)};r.exp({hasMetadata:function(e,t){return c(e,i(t),arguments.length<3?void 0:s(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.has,a=r.key;r.exp({hasOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=n(10),a=r.key,s=r.set;r.exp({metadata:function(e,t){return function(n,r){s(e,t,(void 0!==r?i:o)(n),a(r))}}})},function(e,t,n){var r=n(0),i=n(100)(),o=n(2).process,a="process"==n(21)(o);r(r.G,{asap:function(e){var t=a&&o.domain;i(t?t.bind(e):e)}})},function(e,t,n){"use strict";var r=n(0),i=n(2),o=n(19),a=n(100)(),s=n(5)("observable"),c=n(10),p=n(1),d=n(42),u=n(44),l=n(12),m=n(43),f=m.RETURN,h=function(e){return null==e?void 0:c(e)},g=function(e){var t=e._c;t&&(e._c=void 0,t())},y=function(e){return void 0===e._o},b=function(e){y(e)||(e._o=void 0,g(e))},v=function(e,t){p(e),this._c=void 0,this._o=e,e=new w(this);try{var n=t(e),r=n;null!=n&&("function"==typeof n.unsubscribe?n=function(){r.unsubscribe()}:c(n),this._c=n)}catch(t){return void e.error(t)}y(this)&&g(this)};v.prototype=u({},{unsubscribe:function(){b(this)}});var w=function(e){this._s=e};w.prototype=u({},{next:function(e){var t=this._s;if(!y(t)){var n=t._o;try{var r=h(n.next);if(r)return r.call(n,e)}catch(e){try{b(t)}finally{throw e}}}},error:function(e){var t=this._s;if(y(t))throw e;var n=t._o;t._o=void 0;try{var r=h(n.error);if(!r)throw e;e=r.call(n,e)}catch(e){try{g(t)}finally{throw e}}return g(t),e},complete:function(e){var t=this._s;if(!y(t)){var n=t._o;t._o=void 0;try{var r=h(n.complete);e=r?r.call(n,e):void 0}catch(e){try{g(t)}finally{throw e}}return g(t),e}}});var S=function(e){d(this,S,"Observable","_f")._f=c(e)};u(S.prototype,{subscribe:function(e){return new v(e,this._f)},forEach:function(e){var t=this;return new(o.Promise||i.Promise)(function(n,r){c(e);var i=t.subscribe({next:function(t){try{return e(t)}catch(e){r(e),i.unsubscribe()}},error:r,complete:n})})}}),u(S,{from:function(e){var t="function"==typeof this?this:S,n=h(p(e)[s]);if(n){var r=p(n.call(e));return r.constructor===t?r:new t(function(e){return r.subscribe(e)})}return new t(function(t){var n=!1;return a(function(){if(!n){try{if(m(e,!1,function(e){if(t.next(e),n)return f})===f)return}catch(e){if(n)throw e;return void t.error(e)}t.complete()}}),function(){n=!0}})},of:function(){for(var e=0,t=arguments.length,n=new Array(t);e<t;)n[e]=arguments[e++];return new("function"==typeof this?this:S)(function(e){var t=!1;return a(function(){if(!t){for(var r=0;r<n.length;++r)if(e.next(n[r]),t)return;e.complete()}}),function(){t=!0}})}}),l(S.prototype,s,function(){return this}),r(r.G,{Observable:S}),n(41)("Observable")},function(e,t,n){var r=n(2),i=n(0),o=n(67),a=[].slice,s=/MSIE .\./.test(o),c=function(e){return function(t,n){var r=arguments.length>2,i=!!r&&a.call(arguments,2);return e(r?function(){("function"==typeof t?t:Function(t)).apply(this,i)}:t,n)}};i(i.G+i.B+i.F*s,{setTimeout:c(r.setTimeout),setInterval:c(r.setInterval)})},function(e,t,n){var r=n(0),i=n(99);r(r.G+r.B,{setImmediate:i.set,clearImmediate:i.clear})},function(e,t,n){for(var r=n(96),i=n(37),o=n(13),a=n(2),s=n(12),c=n(49),p=n(5),d=p("iterator"),u=p("toStringTag"),l=c.Array,m={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},f=i(m),h=0;h<f.length;h++){var g,y=f[h],b=m[y],v=a[y],w=v&&v.prototype;if(w&&(w[d]||s(w,d,l),w[u]||s(w,u,y),c[y]=l,b))for(g in r)w[g]||o(w,g,r[g],!0)}},function(e,t,n){(function(t){!function(t){"use strict";var n,r=Object.prototype,i=r.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",s=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag",p="object"==typeof e,d=t.regeneratorRuntime;if(d)p&&(e.exports=d);else{(d=t.regeneratorRuntime=p?e.exports:{}).wrap=w;var u="suspendedStart",l="suspendedYield",m="executing",f="completed",h={},g={};g[a]=function(){return this};var y=Object.getPrototypeOf,b=y&&y(y(D([])));b&&b!==r&&i.call(b,a)&&(g=b);var v=T.prototype=x.prototype=Object.create(g);I.prototype=v.constructor=T,T.constructor=I,T[c]=I.displayName="GeneratorFunction",d.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===I||"GeneratorFunction"===(t.displayName||t.name))},d.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,T):(e.__proto__=T,c in e||(e[c]="GeneratorFunction")),e.prototype=Object.create(v),e},d.awrap=function(e){return{__await:e}},R(k.prototype),k.prototype[s]=function(){return this},d.AsyncIterator=k,d.async=function(e,t,n,r){var i=new k(w(e,t,n,r));return d.isGeneratorFunction(t)?i:i.next().then(function(e){return e.done?e.value:i.next()})},R(v),v[c]="Generator",v[a]=function(){return this},v.toString=function(){return"[object Generator]"},d.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var r=t.pop();if(r in e)return n.value=r,n.done=!1,n}return n.done=!0,n}},d.values=D,E.prototype={constructor:E,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=n,this.done=!1,this.delegate=null,this.method="next",this.arg=n,this.tryEntries.forEach($),!e)for(var t in this)"t"===t.charAt(0)&&i.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=n)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function r(r,i){return s.type="throw",s.arg=e,t.next=r,i&&(t.method="next",t.arg=n),!!i}for(var o=this.tryEntries.length-1;o>=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var c=i.call(a,"catchLoc"),p=i.call(a,"finallyLoc");if(c&&p){if(this.prev<a.catchLoc)return r(a.catchLoc,!0);if(this.prev<a.finallyLoc)return r(a.finallyLoc)}else if(c){if(this.prev<a.catchLoc)return r(a.catchLoc,!0)}else{if(!p)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return r(a.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var o=r;break}}o&&("break"===e||"continue"===e)&&o.tryLoc<=t&&t<=o.finallyLoc&&(o=null);var a=o?o.completion:{};return a.type=e,a.arg=t,o?(this.method="next",this.next=o.finallyLoc,h):this.complete(a)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),$(n),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var i=r.arg;$(n)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:D(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=n),h}}}function w(e,t,n,r){var i=t&&t.prototype instanceof x?t:x,o=Object.create(i.prototype),a=new E(r||[]);return o._invoke=function(e,t,n){var r=u;return function(i,o){if(r===m)throw new Error("Generator is already running");if(r===f){if("throw"===i)throw o;return j()}for(n.method=i,n.arg=o;;){var a=n.delegate;if(a){var s=C(a,n);if(s){if(s===h)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(r===u)throw r=f,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r=m;var c=S(e,t,n);if("normal"===c.type){if(r=n.done?f:l,c.arg===h)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(r=f,n.method="throw",n.arg=c.arg)}}}(e,n,a),o}function S(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}function x(){}function I(){}function T(){}function R(e){["next","throw","return"].forEach(function(t){e[t]=function(e){return this._invoke(t,e)}})}function k(e){function n(t,r,o,a){var s=S(e[t],e,r);if("throw"!==s.type){var c=s.arg,p=c.value;return p&&"object"==typeof p&&i.call(p,"__await")?Promise.resolve(p.__await).then(function(e){n("next",e,o,a)},function(e){n("throw",e,o,a)}):Promise.resolve(p).then(function(e){c.value=e,o(c)},a)}a(s.arg)}var r;"object"==typeof t.process&&t.process.domain&&(n=t.process.domain.bind(n)),this._invoke=function(e,t){function i(){return new Promise(function(r,i){n(e,t,r,i)})}return r=r?r.then(i,i):i()}}function C(e,t){var r=e.iterator[t.method];if(r===n){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=n,C(e,t),"throw"===t.method))return h;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var i=S(r,e.iterator,t.arg);if("throw"===i.type)return t.method="throw",t.arg=i.arg,t.delegate=null,h;var o=i.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=n),t.delegate=null,h):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,h)}function O(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function $(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function E(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(O,this),this.reset(!0)}function D(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,o=function t(){for(;++r<e.length;)if(i.call(e,r))return t.value=e[r],t.done=!1,t;return t.value=n,t.done=!0,t};return o.next=o}}return{next:j}}function j(){return{value:n,done:!0}}}("object"==typeof t?t:"object"==typeof window?window:"object"==typeof self?self:this)}).call(this,n(11))},function(e,t,n){n(353),e.exports=n(19).RegExp.escape},function(e,t,n){var r=n(0),i=n(354)(/[\\^$*+?.()|[\]{}]/g,"\\$&");r(r.S,"RegExp",{escape:function(e){return i(e)}})},function(e,t){e.exports=function(e,t){var n=t===Object(t)?function(e){return t[e]}:t;return function(t){return String(t).replace(e,n)}}},function(e,t,n){"use strict";(function(t){const r=n(56),i=n(139),o=n(378);e.exports=function(e,n){"function"==typeof e&&(n=e,e=void 0);const i=new r;return"function"==typeof n?(t.nextTick(()=>{new o(e,i)}),i.once("connect",n)):new Promise((t,n)=>{i.once("connect",t),i.once("error",n),new o(e,i)})},e.exports.Protocol=i.Protocol,e.exports.List=i.List,e.exports.New=i.New,e.exports.Activate=i.Activate,e.exports.Close=i.Close,e.exports.Version=i.Version}).call(this,n(30))},function(e,t,n){(function(t,r,i){var o=n(142),a=n(34),s=n(143),c=n(144),p=n(366),d=s.IncomingMessage,u=s.readyStates;var l=e.exports=function(e){var n,r=this;c.Writable.call(r),r._opts=e,r._body=[],r._headers={},e.auth&&r.setHeader("Authorization","Basic "+new t(e.auth).toString("base64")),Object.keys(e.headers).forEach(function(t){r.setHeader(t,e.headers[t])});var i=!0;if("disable-fetch"===e.mode||"requestTimeout"in e&&!o.abortController)i=!1,n=!0;else if("prefer-streaming"===e.mode)n=!1;else if("allow-wrong-content-type"===e.mode)n=!o.overrideMimeType;else{if(e.mode&&"default"!==e.mode&&"prefer-fast"!==e.mode)throw new Error("Invalid value for opts.mode");n=!0}r._mode=function(e,t){return o.fetch&&t?"fetch":o.mozchunkedarraybuffer?"moz-chunked-arraybuffer":o.msstream?"ms-stream":o.arraybuffer&&e?"arraybuffer":o.vbArray&&e?"text:vbarray":"text"}(n,i),r._fetchTimer=null,r.on("finish",function(){r._onFinish()})};a(l,c.Writable),l.prototype.setHeader=function(e,t){var n=e.toLowerCase();-1===m.indexOf(n)&&(this._headers[n]={name:e,value:t})},l.prototype.getHeader=function(e){var t=this._headers[e.toLowerCase()];return t?t.value:null},l.prototype.removeHeader=function(e){delete this._headers[e.toLowerCase()]},l.prototype._onFinish=function(){var e=this;if(!e._destroyed){var n=e._opts,a=e._headers,s=null;"GET"!==n.method&&"HEAD"!==n.method&&(s=o.arraybuffer?p(t.concat(e._body)):o.blobConstructor?new r.Blob(e._body.map(function(e){return p(e)}),{type:(a["content-type"]||{}).value||""}):t.concat(e._body).toString());var c=[];if(Object.keys(a).forEach(function(e){var t=a[e].name,n=a[e].value;Array.isArray(n)?n.forEach(function(e){c.push([t,e])}):c.push([t,n])}),"fetch"===e._mode){var d=null;if(o.abortController){var l=new AbortController;d=l.signal,e._fetchAbortController=l,"requestTimeout"in n&&0!==n.requestTimeout&&(e._fetchTimer=r.setTimeout(function(){e.emit("requestTimeout"),e._fetchAbortController&&e._fetchAbortController.abort()},n.requestTimeout))}r.fetch(e._opts.url,{method:e._opts.method,headers:c,body:s||void 0,mode:"cors",credentials:n.withCredentials?"include":"same-origin",signal:d}).then(function(t){e._fetchResponse=t,e._connect()},function(t){r.clearTimeout(e._fetchTimer),e._destroyed||e.emit("error",t)})}else{var m=e._xhr=new r.XMLHttpRequest;try{m.open(e._opts.method,e._opts.url,!0)}catch(t){return void i.nextTick(function(){e.emit("error",t)})}"responseType"in m&&(m.responseType=e._mode.split(":")[0]),"withCredentials"in m&&(m.withCredentials=!!n.withCredentials),"text"===e._mode&&"overrideMimeType"in m&&m.overrideMimeType("text/plain; charset=x-user-defined"),"requestTimeout"in n&&(m.timeout=n.requestTimeout,m.ontimeout=function(){e.emit("requestTimeout")}),c.forEach(function(e){m.setRequestHeader(e[0],e[1])}),e._response=null,m.onreadystatechange=function(){switch(m.readyState){case u.LOADING:case u.DONE:e._onXHRProgress()}},"moz-chunked-arraybuffer"===e._mode&&(m.onprogress=function(){e._onXHRProgress()}),m.onerror=function(){e._destroyed||e.emit("error",new Error("XHR error"))};try{m.send(s)}catch(t){return void i.nextTick(function(){e.emit("error",t)})}}}},l.prototype._onXHRProgress=function(){(function(e){try{var t=e.status;return null!==t&&0!==t}catch(e){return!1}})(this._xhr)&&!this._destroyed&&(this._response||this._connect(),this._response._onXHRProgress())},l.prototype._connect=function(){var e=this;e._destroyed||(e._response=new d(e._xhr,e._fetchResponse,e._mode,e._fetchTimer),e._response.on("error",function(t){e.emit("error",t)}),e.emit("response",e._response))},l.prototype._write=function(e,t,n){this._body.push(e),n()},l.prototype.abort=l.prototype.destroy=function(){this._destroyed=!0,r.clearTimeout(this._fetchTimer),this._response&&(this._response._destroyed=!0),this._xhr?this._xhr.abort():this._fetchAbortController&&this._fetchAbortController.abort()},l.prototype.end=function(e,t,n){"function"==typeof e&&(n=e,e=void 0),c.Writable.prototype.end.call(this,e,t,n)},l.prototype.flushHeaders=function(){},l.prototype.setTimeout=function(){},l.prototype.setNoDelay=function(){},l.prototype.setSocketKeepAlive=function(){};var m=["accept-charset","accept-encoding","access-control-request-headers","access-control-request-method","connection","content-length","cookie","cookie2","date","dnt","expect","host","keep-alive","origin","referer","te","trailer","transfer-encoding","upgrade","via"]}).call(this,n(57).Buffer,n(11),n(30))},function(e,t,n){"use strict";t.byteLength=function(e){var t=p(e),n=t[0],r=t[1];return 3*(n+r)/4-r},t.toByteArray=function(e){for(var t,n=p(e),r=n[0],a=n[1],s=new o(function(e,t,n){return 3*(t+n)/4-n}(0,r,a)),c=0,d=a>0?r-4:r,u=0;u<d;u+=4)t=i[e.charCodeAt(u)]<<18|i[e.charCodeAt(u+1)]<<12|i[e.charCodeAt(u+2)]<<6|i[e.charCodeAt(u+3)],s[c++]=t>>16&255,s[c++]=t>>8&255,s[c++]=255&t;2===a&&(t=i[e.charCodeAt(u)]<<2|i[e.charCodeAt(u+1)]>>4,s[c++]=255&t);1===a&&(t=i[e.charCodeAt(u)]<<10|i[e.charCodeAt(u+1)]<<4|i[e.charCodeAt(u+2)]>>2,s[c++]=t>>8&255,s[c++]=255&t);return s},t.fromByteArray=function(e){for(var t,n=e.length,i=n%3,o=[],a=0,s=n-i;a<s;a+=16383)o.push(d(e,a,a+16383>s?s:a+16383));1===i?(t=e[n-1],o.push(r[t>>2]+r[t<<4&63]+"==")):2===i&&(t=(e[n-2]<<8)+e[n-1],o.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return o.join("")};for(var r=[],i=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,c=a.length;s<c;++s)r[s]=a[s],i[a.charCodeAt(s)]=s;function p(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function d(e,t,n){for(var i,o,a=[],s=t;s<n;s+=3)i=(e[s]<<16&16711680)+(e[s+1]<<8&65280)+(255&e[s+2]),a.push(r[(o=i)>>18&63]+r[o>>12&63]+r[o>>6&63]+r[63&o]);return a.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,i){var o,a,s=8*i-r-1,c=(1<<s)-1,p=c>>1,d=-7,u=n?i-1:0,l=n?-1:1,m=e[t+u];for(u+=l,o=m&(1<<-d)-1,m>>=-d,d+=s;d>0;o=256*o+e[t+u],u+=l,d-=8);for(a=o&(1<<-d)-1,o>>=-d,d+=r;d>0;a=256*a+e[t+u],u+=l,d-=8);if(0===o)o=1-p;else{if(o===c)return a?NaN:1/0*(m?-1:1);a+=Math.pow(2,r),o-=p}return(m?-1:1)*a*Math.pow(2,o-r)},t.write=function(e,t,n,r,i,o){var a,s,c,p=8*o-i-1,d=(1<<p)-1,u=d>>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,m=r?0:o-1,f=r?1:-1,h=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,a=d):(a=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-a))<1&&(a--,c*=2),(t+=a+u>=1?l/c:l*Math.pow(2,1-u))*c>=2&&(a++,c/=2),a+u>=d?(s=0,a=d):a+u>=1?(s=(t*c-1)*Math.pow(2,i),a+=u):(s=t*Math.pow(2,u-1)*Math.pow(2,i),a=0));i>=8;e[n+m]=255&s,m+=f,s/=256,i-=8);for(a=a<<i|s,p+=i;p>0;e[n+m]=255&a,m+=f,a/=256,p-=8);e[n+m-f]|=128*h}},function(e,t){},function(e,t,n){"use strict";var r=n(74).Buffer,i=n(361);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,i,o=r.allocUnsafe(e>>>0),a=this.head,s=0;a;)t=a.data,n=o,i=s,t.copy(n,i),s+=a.data.length,a=a.next;return o},e}(),i&&i.inspect&&i.inspect.custom&&(e.exports.prototype[i.inspect.custom]=function(){var e=i.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,i=Function.prototype.apply;function o(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new o(i.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new o(i.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},o.prototype.unref=o.prototype.ref=function(){},o.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(363),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(11))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,i,o,a,s,c=1,p={},d=!1,u=e.document,l=Object.getPrototypeOf&&Object.getPrototypeOf(e);l=l&&l.setTimeout?l:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){f(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((o=new MessageChannel).port1.onmessage=function(e){f(e.data)},r=function(e){o.port2.postMessage(e)}):u&&"onreadystatechange"in u.createElement("script")?(i=u.documentElement,r=function(e){var t=u.createElement("script");t.onreadystatechange=function(){f(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):r=function(e){setTimeout(f,0,e)}:(a="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&f(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),r=function(t){e.postMessage(a+t,"*")}),l.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n<t.length;n++)t[n]=arguments[n+1];var i={callback:e,args:t};return p[c]=i,r(c),c++},l.clearImmediate=m}function m(e){delete p[e]}function f(e){if(d)setTimeout(f,0,e);else{var t=p[e];if(t){d=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{m(e),d=!1}}}}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,n(11),n(30))},function(e,t,n){(function(t){function n(e){try{if(!t.localStorage)return!1}catch(e){return!1}var n=t.localStorage[e];return null!=n&&"true"===String(n).toLowerCase()}e.exports=function(e,t){if(n("noDeprecation"))return e;var r=!1;return function(){if(!r){if(n("throwDeprecation"))throw new Error(t);n("traceDeprecation")?console.trace(t):console.warn(t),r=!0}return e.apply(this,arguments)}}}).call(this,n(11))},function(e,t,n){"use strict";e.exports=o;var r=n(150),i=n(58);function o(e){if(!(this instanceof o))return new o(e);r.call(this,e)}i.inherits=n(34),i.inherits(o,r),o.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(57).Buffer;e.exports=function(e){if(e instanceof Uint8Array){if(0===e.byteOffset&&e.byteLength===e.buffer.byteLength)return e.buffer;if("function"==typeof e.buffer.slice)return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)}if(r.isBuffer(e)){for(var t=new Uint8Array(e.length),n=e.length,i=0;i<n;i++)t[i]=e[i];return t.buffer}throw new Error("Argument must be a Buffer")}},function(e,t){e.exports=function(){for(var e={},t=0;t<arguments.length;t++){var r=arguments[t];for(var i in r)n.call(r,i)&&(e[i]=r[i])}return e};var n=Object.prototype.hasOwnProperty},function(e,t){e.exports={100:"Continue",101:"Switching Protocols",102:"Processing",200:"OK",201:"Created",202:"Accepted",203:"Non-Authoritative Information",204:"No Content",205:"Reset Content",206:"Partial Content",207:"Multi-Status",208:"Already Reported",226:"IM Used",300:"Multiple Choices",301:"Moved Permanently",302:"Found",303:"See Other",304:"Not Modified",305:"Use Proxy",307:"Temporary Redirect",308:"Permanent Redirect",400:"Bad Request",401:"Unauthorized",402:"Payment Required",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",406:"Not Acceptable",407:"Proxy Authentication Required",408:"Request Timeout",409:"Conflict",410:"Gone",411:"Length Required",412:"Precondition Failed",413:"Payload Too Large",414:"URI Too Long",415:"Unsupported Media Type",416:"Range Not Satisfiable",417:"Expectation Failed",418:"I'm a teapot",421:"Misdirected Request",422:"Unprocessable Entity",423:"Locked",424:"Failed Dependency",425:"Unordered Collection",426:"Upgrade Required",428:"Precondition Required",429:"Too Many Requests",431:"Request Header Fields Too Large",451:"Unavailable For Legal Reasons",500:"Internal Server Error",501:"Not Implemented",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout",505:"HTTP Version Not Supported",506:"Variant Also Negotiates",507:"Insufficient Storage",508:"Loop Detected",509:"Bandwidth Limit Exceeded",510:"Not Extended",511:"Network Authentication Required"}},function(e,t,n){(function(e,r){var i;/*! https://mths.be/punycode v1.4.1 by @mathias */!function(o){t&&t.nodeType,e&&e.nodeType;var a="object"==typeof r&&r;a.global!==a&&a.window!==a&&a.self;var s,c=2147483647,p=36,d=1,u=26,l=38,m=700,f=72,h=128,g="-",y=/^xn--/,b=/[^\x20-\x7E]/,v=/[\x2E\u3002\uFF0E\uFF61]/g,w={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},S=p-d,x=Math.floor,I=String.fromCharCode;function T(e){throw new RangeError(w[e])}function R(e,t){for(var n=e.length,r=[];n--;)r[n]=t(e[n]);return r}function k(e,t){var n=e.split("@"),r="";return n.length>1&&(r=n[0]+"@",e=n[1]),r+R((e=e.replace(v,".")).split("."),t).join(".")}function C(e){for(var t,n,r=[],i=0,o=e.length;i<o;)(t=e.charCodeAt(i++))>=55296&&t<=56319&&i<o?56320==(64512&(n=e.charCodeAt(i++)))?r.push(((1023&t)<<10)+(1023&n)+65536):(r.push(t),i--):r.push(t);return r}function O(e){return R(e,function(e){var t="";return e>65535&&(t+=I((e-=65536)>>>10&1023|55296),e=56320|1023&e),t+=I(e)}).join("")}function $(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function E(e,t,n){var r=0;for(e=n?x(e/m):e>>1,e+=x(e/t);e>S*u>>1;r+=p)e=x(e/S);return x(r+(S+1)*e/(e+l))}function D(e){var t,n,r,i,o,a,s,l,m,y,b,v=[],w=e.length,S=0,I=h,R=f;for((n=e.lastIndexOf(g))<0&&(n=0),r=0;r<n;++r)e.charCodeAt(r)>=128&&T("not-basic"),v.push(e.charCodeAt(r));for(i=n>0?n+1:0;i<w;){for(o=S,a=1,s=p;i>=w&&T("invalid-input"),((l=(b=e.charCodeAt(i++))-48<10?b-22:b-65<26?b-65:b-97<26?b-97:p)>=p||l>x((c-S)/a))&&T("overflow"),S+=l*a,!(l<(m=s<=R?d:s>=R+u?u:s-R));s+=p)a>x(c/(y=p-m))&&T("overflow"),a*=y;R=E(S-o,t=v.length+1,0==o),x(S/t)>c-I&&T("overflow"),I+=x(S/t),S%=t,v.splice(S++,0,I)}return O(v)}function j(e){var t,n,r,i,o,a,s,l,m,y,b,v,w,S,R,k=[];for(v=(e=C(e)).length,t=h,n=0,o=f,a=0;a<v;++a)(b=e[a])<128&&k.push(I(b));for(r=i=k.length,i&&k.push(g);r<v;){for(s=c,a=0;a<v;++a)(b=e[a])>=t&&b<s&&(s=b);for(s-t>x((c-n)/(w=r+1))&&T("overflow"),n+=(s-t)*w,t=s,a=0;a<v;++a)if((b=e[a])<t&&++n>c&&T("overflow"),b==t){for(l=n,m=p;!(l<(y=m<=o?d:m>=o+u?u:m-o));m+=p)R=l-y,S=p-y,k.push(I($(y+R%S,0))),l=x(R/S);k.push(I($(l,0))),o=E(n,w,r==i),n=0,++r}++n,++t}return k.join("")}s={version:"1.4.1",ucs2:{decode:C,encode:O},decode:D,encode:j,toASCII:function(e){return k(e,function(e){return b.test(e)?"xn--"+j(e):e})},toUnicode:function(e){return k(e,function(e){return y.test(e)?D(e.slice(4).toLowerCase()):e})}},void 0===(i=function(){return s}.call(t,n,t,e))||(e.exports=i)}()}).call(this,n(370)(e),n(11))},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},function(e,t,n){"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,t,n){"use strict";t.decode=t.parse=n(373),t.encode=t.stringify=n(374)},function(e,t,n){"use strict";function r(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,n,o){t=t||"&",n=n||"=";var a={};if("string"!=typeof e||0===e.length)return a;var s=/\+/g;e=e.split(t);var c=1e3;o&&"number"==typeof o.maxKeys&&(c=o.maxKeys);var p=e.length;c>0&&p>c&&(p=c);for(var d=0;d<p;++d){var u,l,m,f,h=e[d].replace(s,"%20"),g=h.indexOf(n);g>=0?(u=h.substr(0,g),l=h.substr(g+1)):(u=h,l=""),m=decodeURIComponent(u),f=decodeURIComponent(l),r(a,m)?i(a[m])?a[m].push(f):a[m]=[a[m],f]:a[m]=f}return a};var i=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},function(e,t,n){"use strict";var r=function(e){switch(typeof e){case"string":return e;case"boolean":return e?"true":"false";case"number":return isFinite(e)?e:"";default:return""}};e.exports=function(e,t,n,s){return t=t||"&",n=n||"=",null===e&&(e=void 0),"object"==typeof e?o(a(e),function(a){var s=encodeURIComponent(r(a))+n;return i(e[a])?o(e[a],function(e){return s+encodeURIComponent(r(e))}).join(t):s+encodeURIComponent(r(e[a]))}).join(t):s?encodeURIComponent(r(s))+n+encodeURIComponent(r(e)):""};var i=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)};function o(e,t){if(e.map)return e.map(t);for(var n=[],r=0;r<e.length;r++)n.push(t(e[r],r));return n}var a=Object.keys||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t}},function(e,t,n){var r=n(140),i=n(75),o=e.exports;for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);function s(e){if("string"==typeof e&&(e=i.parse(e)),e.protocol||(e.protocol="https:"),"https:"!==e.protocol)throw new Error('Protocol "'+e.protocol+'" not supported. Expected "https:"');return e}o.request=function(e,t){return e=s(e),r.request.call(this,e,t)},o.get=function(e,t){return e=s(e),r.get.call(this,e,t)}},function(e,t){e.exports=function(e,t,n){window.criRequest(t,n)}},function(e){e.exports=JSON.parse('{"version":{"major":"1","minor":"3"},"domains":[{"domain":"Accessibility","experimental":true,"dependencies":["DOM"],"types":[{"id":"AXNodeId","description":"Unique accessibility node identifier.","type":"string"},{"id":"AXValueType","description":"Enum of possible property types.","type":"string","enum":["boolean","tristate","booleanOrUndefined","idref","idrefList","integer","node","nodeList","number","string","computedString","token","tokenList","domRelation","role","internalRole","valueUndefined"]},{"id":"AXValueSourceType","description":"Enum of possible property sources.","type":"string","enum":["attribute","implicit","style","contents","placeholder","relatedElement"]},{"id":"AXValueNativeSourceType","description":"Enum of possible native property sources (as a subtype of a particular AXValueSourceType).","type":"string","enum":["figcaption","label","labelfor","labelwrapped","legend","tablecaption","title","other"]},{"id":"AXValueSource","description":"A single source for a computed AX property.","type":"object","properties":[{"name":"type","description":"What type of source this is.","$ref":"AXValueSourceType"},{"name":"value","description":"The value of this property source.","optional":true,"$ref":"AXValue"},{"name":"attribute","description":"The name of the relevant attribute, if any.","optional":true,"type":"string"},{"name":"attributeValue","description":"The value of the relevant attribute, if any.","optional":true,"$ref":"AXValue"},{"name":"superseded","description":"Whether this source is superseded by a higher priority source.","optional":true,"type":"boolean"},{"name":"nativeSource","description":"The native markup source for this value, e.g. a <label> element.","optional":true,"$ref":"AXValueNativeSourceType"},{"name":"nativeSourceValue","description":"The value, such as a node or node list, of the native source.","optional":true,"$ref":"AXValue"},{"name":"invalid","description":"Whether the value for this property is invalid.","optional":true,"type":"boolean"},{"name":"invalidReason","description":"Reason for the value being invalid, if it is.","optional":true,"type":"string"}]},{"id":"AXRelatedNode","type":"object","properties":[{"name":"backendDOMNodeId","description":"The BackendNodeId of the related DOM node.","$ref":"DOM.BackendNodeId"},{"name":"idref","description":"The IDRef value provided, if any.","optional":true,"type":"string"},{"name":"text","description":"The text alternative of this node in the current context.","optional":true,"type":"string"}]},{"id":"AXProperty","type":"object","properties":[{"name":"name","description":"The name of this property.","$ref":"AXPropertyName"},{"name":"value","description":"The value of this property.","$ref":"AXValue"}]},{"id":"AXValue","description":"A single computed AX property.","type":"object","properties":[{"name":"type","description":"The type of this value.","$ref":"AXValueType"},{"name":"value","description":"The computed value of this property.","optional":true,"type":"any"},{"name":"relatedNodes","description":"One or more related nodes, if applicable.","optional":true,"type":"array","items":{"$ref":"AXRelatedNode"}},{"name":"sources","description":"The sources which contributed to the computation of this property.","optional":true,"type":"array","items":{"$ref":"AXValueSource"}}]},{"id":"AXPropertyName","description":"Values of AXProperty name:\\n- from \'busy\' to \'roledescription\': states which apply to every AX node\\n- from \'live\' to \'root\': attributes which apply to nodes in live regions\\n- from \'autocomplete\' to \'valuetext\': attributes which apply to widgets\\n- from \'checked\' to \'selected\': states which apply to widgets\\n- from \'activedescendant\' to \'owns\' - relationships between elements other than parent/child/sibling.","type":"string","enum":["busy","disabled","editable","focusable","focused","hidden","hiddenRoot","invalid","keyshortcuts","settable","roledescription","live","atomic","relevant","root","autocomplete","hasPopup","level","multiselectable","orientation","multiline","readonly","required","valuemin","valuemax","valuetext","checked","expanded","modal","pressed","selected","activedescendant","controls","describedby","details","errormessage","flowto","labelledby","owns"]},{"id":"AXNode","description":"A node in the accessibility tree.","type":"object","properties":[{"name":"nodeId","description":"Unique identifier for this node.","$ref":"AXNodeId"},{"name":"ignored","description":"Whether this node is ignored for accessibility","type":"boolean"},{"name":"ignoredReasons","description":"Collection of reasons why this node is hidden.","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"role","description":"This `Node`\'s role, whether explicit or implicit.","optional":true,"$ref":"AXValue"},{"name":"name","description":"The accessible name for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"description","description":"The accessible description for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"value","description":"The value for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"properties","description":"All other properties","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"childIds","description":"IDs for each of this node\'s child nodes.","optional":true,"type":"array","items":{"$ref":"AXNodeId"}},{"name":"backendDOMNodeId","description":"The backend ID for the associated DOM node, if any.","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"disable","description":"Disables the accessibility domain."},{"name":"enable","description":"Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."},{"name":"getPartialAXTree","description":"Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get the partial accessibility tree for.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"fetchRelatives","description":"Whether to fetch this nodes ancestors, siblings and children. Defaults to true.","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\\nchildren, if requested.","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getFullAXTree","description":"Fetches the entire accessibility tree","experimental":true,"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]}]},{"domain":"Animation","experimental":true,"dependencies":["Runtime","DOM"],"types":[{"id":"Animation","description":"Animation instance.","type":"object","properties":[{"name":"id","description":"`Animation`\'s id.","type":"string"},{"name":"name","description":"`Animation`\'s name.","type":"string"},{"name":"pausedState","description":"`Animation`\'s internal paused state.","type":"boolean"},{"name":"playState","description":"`Animation`\'s play state.","type":"string"},{"name":"playbackRate","description":"`Animation`\'s playback rate.","type":"number"},{"name":"startTime","description":"`Animation`\'s start time.","type":"number"},{"name":"currentTime","description":"`Animation`\'s current time.","type":"number"},{"name":"type","description":"Animation type of `Animation`.","type":"string","enum":["CSSTransition","CSSAnimation","WebAnimation"]},{"name":"source","description":"`Animation`\'s source animation node.","optional":true,"$ref":"AnimationEffect"},{"name":"cssId","description":"A unique ID for `Animation` representing the sources that triggered this CSS\\nanimation/transition.","optional":true,"type":"string"}]},{"id":"AnimationEffect","description":"AnimationEffect instance","type":"object","properties":[{"name":"delay","description":"`AnimationEffect`\'s delay.","type":"number"},{"name":"endDelay","description":"`AnimationEffect`\'s end delay.","type":"number"},{"name":"iterationStart","description":"`AnimationEffect`\'s iteration start.","type":"number"},{"name":"iterations","description":"`AnimationEffect`\'s iterations.","type":"number"},{"name":"duration","description":"`AnimationEffect`\'s iteration duration.","type":"number"},{"name":"direction","description":"`AnimationEffect`\'s playback direction.","type":"string"},{"name":"fill","description":"`AnimationEffect`\'s fill mode.","type":"string"},{"name":"backendNodeId","description":"`AnimationEffect`\'s target node.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"keyframesRule","description":"`AnimationEffect`\'s keyframes.","optional":true,"$ref":"KeyframesRule"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]},{"id":"KeyframesRule","description":"Keyframes Rule","type":"object","properties":[{"name":"name","description":"CSS keyframed animation\'s name.","optional":true,"type":"string"},{"name":"keyframes","description":"List of animation keyframes.","type":"array","items":{"$ref":"KeyframeStyle"}}]},{"id":"KeyframeStyle","description":"Keyframe Style","type":"object","properties":[{"name":"offset","description":"Keyframe\'s time offset.","type":"string"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]}],"commands":[{"name":"disable","description":"Disables animation domain notifications."},{"name":"enable","description":"Enables animation domain notifications."},{"name":"getCurrentTime","description":"Returns the current time of the an animation.","parameters":[{"name":"id","description":"Id of animation.","type":"string"}],"returns":[{"name":"currentTime","description":"Current time of the page.","type":"number"}]},{"name":"getPlaybackRate","description":"Gets the playback rate of the document timeline.","returns":[{"name":"playbackRate","description":"Playback rate for animations on page.","type":"number"}]},{"name":"releaseAnimations","description":"Releases a set of animations to no longer be manipulated.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}}]},{"name":"resolveAnimation","description":"Gets the remote object of the Animation.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"}],"returns":[{"name":"remoteObject","description":"Corresponding remote object.","$ref":"Runtime.RemoteObject"}]},{"name":"seekAnimations","description":"Seek a set of animations to a particular time within each animation.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}},{"name":"currentTime","description":"Set the current time of each animation.","type":"number"}]},{"name":"setPaused","description":"Sets the paused state of a set of animations.","parameters":[{"name":"animations","description":"Animations to set the pause state of.","type":"array","items":{"type":"string"}},{"name":"paused","description":"Paused state to set to.","type":"boolean"}]},{"name":"setPlaybackRate","description":"Sets the playback rate of the document timeline.","parameters":[{"name":"playbackRate","description":"Playback rate for animations on page","type":"number"}]},{"name":"setTiming","description":"Sets the timing of an animation node.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"},{"name":"duration","description":"Duration of the animation.","type":"number"},{"name":"delay","description":"Delay of the animation.","type":"number"}]}],"events":[{"name":"animationCanceled","description":"Event for when an animation has been cancelled.","parameters":[{"name":"id","description":"Id of the animation that was cancelled.","type":"string"}]},{"name":"animationCreated","description":"Event for each animation that has been created.","parameters":[{"name":"id","description":"Id of the animation that was created.","type":"string"}]},{"name":"animationStarted","description":"Event for animation that has been started.","parameters":[{"name":"animation","description":"Animation that was started.","$ref":"Animation"}]}]},{"domain":"ApplicationCache","experimental":true,"types":[{"id":"ApplicationCacheResource","description":"Detailed application cache resource information.","type":"object","properties":[{"name":"url","description":"Resource url.","type":"string"},{"name":"size","description":"Resource size.","type":"integer"},{"name":"type","description":"Resource type.","type":"string"}]},{"id":"ApplicationCache","description":"Detailed application cache information.","type":"object","properties":[{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"size","description":"Application cache size.","type":"number"},{"name":"creationTime","description":"Application cache creation time.","type":"number"},{"name":"updateTime","description":"Application cache update time.","type":"number"},{"name":"resources","description":"Application cache resources.","type":"array","items":{"$ref":"ApplicationCacheResource"}}]},{"id":"FrameWithManifest","description":"Frame identifier - manifest URL pair.","type":"object","properties":[{"name":"frameId","description":"Frame identifier.","$ref":"Page.FrameId"},{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"status","description":"Application cache status.","type":"integer"}]}],"commands":[{"name":"enable","description":"Enables application cache domain notifications."},{"name":"getApplicationCacheForFrame","description":"Returns relevant application cache data for the document in given frame.","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose application cache is retrieved.","$ref":"Page.FrameId"}],"returns":[{"name":"applicationCache","description":"Relevant application cache data for the document in given frame.","$ref":"ApplicationCache"}]},{"name":"getFramesWithManifests","description":"Returns array of frame identifiers with manifest urls for each frame containing a document\\nassociated with some application cache.","returns":[{"name":"frameIds","description":"Array of frame identifiers with manifest urls for each frame containing a document\\nassociated with some application cache.","type":"array","items":{"$ref":"FrameWithManifest"}}]},{"name":"getManifestForFrame","description":"Returns manifest URL for document in the given frame.","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose manifest is retrieved.","$ref":"Page.FrameId"}],"returns":[{"name":"manifestURL","description":"Manifest URL for document in the given frame.","type":"string"}]}],"events":[{"name":"applicationCacheStatusUpdated","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose application cache updated status.","$ref":"Page.FrameId"},{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"status","description":"Updated application cache status.","type":"integer"}]},{"name":"networkStateUpdated","parameters":[{"name":"isNowOnline","type":"boolean"}]}]},{"domain":"Audits","description":"Audits domain allows investigation of page violations and possible improvements.","experimental":true,"dependencies":["Network"],"commands":[{"name":"getEncodedResponse","description":"Returns the response body and size if it were re-encoded with the specified settings. Only\\napplies to images.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"Network.RequestId"},{"name":"encoding","description":"The encoding to use.","type":"string","enum":["webp","jpeg","png"]},{"name":"quality","description":"The quality of the encoding (0-1). (defaults to 1)","optional":true,"type":"number"},{"name":"sizeOnly","description":"Whether to only return the size information (defaults to false).","optional":true,"type":"boolean"}],"returns":[{"name":"body","description":"The encoded body as a base64 string. Omitted if sizeOnly is true.","optional":true,"type":"string"},{"name":"originalSize","description":"Size before re-encoding.","type":"integer"},{"name":"encodedSize","description":"Size after re-encoding.","type":"integer"}]}]},{"domain":"BackgroundService","description":"Defines events for background web platform features.","experimental":true,"types":[{"id":"ServiceName","description":"The Background Service that will be associated with the commands/events.\\nEvery Background Service operates independently, but they share the same\\nAPI.","type":"string","enum":["backgroundFetch","backgroundSync","pushMessaging","notifications","paymentHandler"]},{"id":"EventMetadata","description":"A key-value pair for additional event information to pass along.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"BackgroundServiceEvent","type":"object","properties":[{"name":"timestamp","description":"Timestamp of the event (in seconds).","$ref":"Network.TimeSinceEpoch"},{"name":"origin","description":"The origin this event belongs to.","type":"string"},{"name":"serviceWorkerRegistrationId","description":"The Service Worker ID that initiated the event.","$ref":"ServiceWorker.RegistrationID"},{"name":"service","description":"The Background Service this event belongs to.","$ref":"ServiceName"},{"name":"eventName","description":"A description of the event.","type":"string"},{"name":"instanceId","description":"An identifier that groups related events together.","type":"string"},{"name":"eventMetadata","description":"A list of event-specific information.","type":"array","items":{"$ref":"EventMetadata"}}]}],"commands":[{"name":"startObserving","description":"Enables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"stopObserving","description":"Disables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"setRecording","description":"Set the recording state for the service.","parameters":[{"name":"shouldRecord","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"clearEvents","description":"Clears all stored data for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]}],"events":[{"name":"recordingStateChanged","description":"Called when the recording state for the service has been updated.","parameters":[{"name":"isRecording","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"backgroundServiceEventReceived","description":"Called with all existing backgroundServiceEvents when enabled, and all new\\nevents afterwards if enabled and recording.","parameters":[{"name":"backgroundServiceEvent","$ref":"BackgroundServiceEvent"}]}]},{"domain":"Browser","description":"The Browser domain defines methods and events for browser managing.","types":[{"id":"WindowID","experimental":true,"type":"integer"},{"id":"WindowState","description":"The state of the browser window.","experimental":true,"type":"string","enum":["normal","minimized","maximized","fullscreen"]},{"id":"Bounds","description":"Browser window bounds information","experimental":true,"type":"object","properties":[{"name":"left","description":"The offset from the left edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"top","description":"The offset from the top edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"width","description":"The window width in pixels.","optional":true,"type":"integer"},{"name":"height","description":"The window height in pixels.","optional":true,"type":"integer"},{"name":"windowState","description":"The window state. Default to normal.","optional":true,"$ref":"WindowState"}]},{"id":"PermissionType","experimental":true,"type":"string","enum":["accessibilityEvents","audioCapture","backgroundSync","backgroundFetch","clipboardRead","clipboardWrite","durableStorage","flash","geolocation","midi","midiSysex","notifications","paymentHandler","periodicBackgroundSync","protectedMediaIdentifier","sensors","videoCapture","idleDetection","wakeLockScreen","wakeLockSystem"]},{"id":"Bucket","description":"Chrome histogram bucket.","experimental":true,"type":"object","properties":[{"name":"low","description":"Minimum value (inclusive).","type":"integer"},{"name":"high","description":"Maximum value (exclusive).","type":"integer"},{"name":"count","description":"Number of samples.","type":"integer"}]},{"id":"Histogram","description":"Chrome histogram.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name.","type":"string"},{"name":"sum","description":"Sum of sample values.","type":"integer"},{"name":"count","description":"Total number of samples.","type":"integer"},{"name":"buckets","description":"Buckets.","type":"array","items":{"$ref":"Bucket"}}]}],"commands":[{"name":"grantPermissions","description":"Grant specific permissions to the given origin and reject all others.","experimental":true,"parameters":[{"name":"origin","type":"string"},{"name":"permissions","type":"array","items":{"$ref":"PermissionType"}},{"name":"browserContextId","description":"BrowserContext to override permissions. When omitted, default browser context is used.","optional":true,"$ref":"Target.BrowserContextID"}]},{"name":"resetPermissions","description":"Reset all permission management for all origins.","experimental":true,"parameters":[{"name":"browserContextId","description":"BrowserContext to reset permissions. When omitted, default browser context is used.","optional":true,"$ref":"Target.BrowserContextID"}]},{"name":"close","description":"Close browser gracefully."},{"name":"crash","description":"Crashes browser on the main thread.","experimental":true},{"name":"crashGpuProcess","description":"Crashes GPU process.","experimental":true},{"name":"getVersion","description":"Returns version information.","returns":[{"name":"protocolVersion","description":"Protocol version.","type":"string"},{"name":"product","description":"Product name.","type":"string"},{"name":"revision","description":"Product revision.","type":"string"},{"name":"userAgent","description":"User-Agent.","type":"string"},{"name":"jsVersion","description":"V8 version.","type":"string"}]},{"name":"getBrowserCommandLine","description":"Returns the command line switches for the browser process if, and only if\\n--enable-automation is on the commandline.","experimental":true,"returns":[{"name":"arguments","description":"Commandline parameters","type":"array","items":{"type":"string"}}]},{"name":"getHistograms","description":"Get Chrome histograms.","experimental":true,"parameters":[{"name":"query","description":"Requested substring in name. Only histograms which have query as a\\nsubstring in their name are extracted. An empty or absent query returns\\nall histograms.","optional":true,"type":"string"},{"name":"delta","description":"If true, retrieve delta since last call.","optional":true,"type":"boolean"}],"returns":[{"name":"histograms","description":"Histograms.","type":"array","items":{"$ref":"Histogram"}}]},{"name":"getHistogram","description":"Get a Chrome histogram by name.","experimental":true,"parameters":[{"name":"name","description":"Requested histogram name.","type":"string"},{"name":"delta","description":"If true, retrieve delta since last call.","optional":true,"type":"boolean"}],"returns":[{"name":"histogram","description":"Histogram.","$ref":"Histogram"}]},{"name":"getWindowBounds","description":"Get position and size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"}],"returns":[{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"getWindowForTarget","description":"Get the browser window that contains the devtools target.","experimental":true,"parameters":[{"name":"targetId","description":"Devtools agent host id. If called as a part of the session, associated targetId is used.","optional":true,"$ref":"Target.TargetID"}],"returns":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"setWindowBounds","description":"Set position and/or size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"New window bounds. The \'minimized\', \'maximized\' and \'fullscreen\' states cannot be combined\\nwith \'left\', \'top\', \'width\' or \'height\'. Leaves unspecified fields unchanged.","$ref":"Bounds"}]},{"name":"setDockTile","description":"Set dock tile details, platform-specific.","experimental":true,"parameters":[{"name":"badgeLabel","optional":true,"type":"string"},{"name":"image","description":"Png encoded image.","optional":true,"type":"string"}]}]},{"domain":"CSS","description":"This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\\nhave an associated `id` used in subsequent operations on the related object. Each object type has\\na specific `id` structure, and those are not interchangeable between objects of different kinds.\\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.","experimental":true,"dependencies":["DOM"],"types":[{"id":"StyleSheetId","type":"string"},{"id":"StyleSheetOrigin","description":"Stylesheet type: \\"injected\\" for stylesheets injected via extension, \\"user-agent\\" for user-agent\\nstylesheets, \\"inspector\\" for stylesheets created by the inspector (i.e. those holding the \\"via\\ninspector\\" rules), \\"regular\\" for regular stylesheets.","type":"string","enum":["injected","user-agent","inspector","regular"]},{"id":"PseudoElementMatches","description":"CSS rule collection for a single pseudo style.","type":"object","properties":[{"name":"pseudoType","description":"Pseudo element type.","$ref":"DOM.PseudoType"},{"name":"matches","description":"Matches of CSS rules applicable to the pseudo style.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedStyleEntry","description":"Inherited CSS rule collection from ancestor node.","type":"object","properties":[{"name":"inlineStyle","description":"The ancestor node\'s inline style, if any, in the style inheritance chain.","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"Matches of CSS rules matching the ancestor node in the style inheritance chain.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"RuleMatch","description":"Match data for a CSS rule.","type":"object","properties":[{"name":"rule","description":"CSS rule in the match.","$ref":"CSSRule"},{"name":"matchingSelectors","description":"Matching selector indices in the rule\'s selectorList selectors (0-based).","type":"array","items":{"type":"integer"}}]},{"id":"Value","description":"Data for a simple selector (these are delimited by commas in a selector list).","type":"object","properties":[{"name":"text","description":"Value text.","type":"string"},{"name":"range","description":"Value range in the underlying resource (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"SelectorList","description":"Selector list data.","type":"object","properties":[{"name":"selectors","description":"Selectors in the list.","type":"array","items":{"$ref":"Value"}},{"name":"text","description":"Rule selector text.","type":"string"}]},{"id":"CSSStyleSheetHeader","description":"CSS stylesheet metainformation.","type":"object","properties":[{"name":"styleSheetId","description":"The stylesheet identifier.","$ref":"StyleSheetId"},{"name":"frameId","description":"Owner frame identifier.","$ref":"Page.FrameId"},{"name":"sourceURL","description":"Stylesheet resource URL.","type":"string"},{"name":"sourceMapURL","description":"URL of source map associated with the stylesheet (if any).","optional":true,"type":"string"},{"name":"origin","description":"Stylesheet origin.","$ref":"StyleSheetOrigin"},{"name":"title","description":"Stylesheet title.","type":"string"},{"name":"ownerNode","description":"The backend id for the owner node of the stylesheet.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"disabled","description":"Denotes whether the stylesheet is disabled.","type":"boolean"},{"name":"hasSourceURL","description":"Whether the sourceURL field value comes from the sourceURL comment.","optional":true,"type":"boolean"},{"name":"isInline","description":"Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\\ndocument.written STYLE tags.","type":"boolean"},{"name":"startLine","description":"Line offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"startColumn","description":"Column offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"length","description":"Size of the content (in characters).","type":"number"}]},{"id":"CSSRule","description":"CSS rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"selectorList","description":"Rule selector data.","$ref":"SelectorList"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"},{"name":"media","description":"Media list array (for rules involving media queries). The array enumerates media queries\\nstarting with the innermost one, going outwards.","optional":true,"type":"array","items":{"$ref":"CSSMedia"}}]},{"id":"RuleUsage","description":"CSS coverage information.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","$ref":"StyleSheetId"},{"name":"startOffset","description":"Offset of the start of the rule (including selector) from the beginning of the stylesheet.","type":"number"},{"name":"endOffset","description":"Offset of the end of the rule body from the beginning of the stylesheet.","type":"number"},{"name":"used","description":"Indicates whether the rule was actually used by some element in the page.","type":"boolean"}]},{"id":"SourceRange","description":"Text range within a resource. All numbers are zero-based.","type":"object","properties":[{"name":"startLine","description":"Start line of range.","type":"integer"},{"name":"startColumn","description":"Start column of range (inclusive).","type":"integer"},{"name":"endLine","description":"End line of range","type":"integer"},{"name":"endColumn","description":"End column of range (exclusive).","type":"integer"}]},{"id":"ShorthandEntry","type":"object","properties":[{"name":"name","description":"Shorthand name.","type":"string"},{"name":"value","description":"Shorthand value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]},{"id":"CSSStyle","description":"CSS style representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"cssProperties","description":"CSS properties in the style.","type":"array","items":{"$ref":"CSSProperty"}},{"name":"shorthandEntries","description":"Computed values for all shorthands found in the style.","type":"array","items":{"$ref":"ShorthandEntry"}},{"name":"cssText","description":"Style declaration text (if available).","optional":true,"type":"string"},{"name":"range","description":"Style declaration range in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSProperty","description":"CSS property declaration data.","type":"object","properties":[{"name":"name","description":"The property name.","type":"string"},{"name":"value","description":"The property value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"implicit","description":"Whether the property is implicit (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"text","description":"The full property text as specified in the style.","optional":true,"type":"string"},{"name":"parsedOk","description":"Whether the property is understood by the browser (implies `true` if absent).","optional":true,"type":"boolean"},{"name":"disabled","description":"Whether the property is disabled by the user (present for source-based properties only).","optional":true,"type":"boolean"},{"name":"range","description":"The entire property range in the enclosing style declaration (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSMedia","description":"CSS media rule descriptor.","type":"object","properties":[{"name":"text","description":"Media query text.","type":"string"},{"name":"source","description":"Source of the media query: \\"mediaRule\\" if specified by a @media rule, \\"importRule\\" if\\nspecified by an @import rule, \\"linkedSheet\\" if specified by a \\"media\\" attribute in a linked\\nstylesheet\'s LINK tag, \\"inlineSheet\\" if specified by a \\"media\\" attribute in an inline\\nstylesheet\'s STYLE tag.","type":"string","enum":["mediaRule","importRule","linkedSheet","inlineSheet"]},{"name":"sourceURL","description":"URL of the document containing the media query description.","optional":true,"type":"string"},{"name":"range","description":"The associated rule (@media or @import) header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"mediaList","description":"Array of media queries.","optional":true,"type":"array","items":{"$ref":"MediaQuery"}}]},{"id":"MediaQuery","description":"Media query descriptor.","type":"object","properties":[{"name":"expressions","description":"Array of media query expressions.","type":"array","items":{"$ref":"MediaQueryExpression"}},{"name":"active","description":"Whether the media query condition is satisfied.","type":"boolean"}]},{"id":"MediaQueryExpression","description":"Media query expression descriptor.","type":"object","properties":[{"name":"value","description":"Media query expression value.","type":"number"},{"name":"unit","description":"Media query expression units.","type":"string"},{"name":"feature","description":"Media query expression feature.","type":"string"},{"name":"valueRange","description":"The associated range of the value text in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"},{"name":"computedLength","description":"Computed length of media query expression (if applicable).","optional":true,"type":"number"}]},{"id":"PlatformFontUsage","description":"Information about amount of glyphs that were rendered with given font.","type":"object","properties":[{"name":"familyName","description":"Font\'s family name reported by platform.","type":"string"},{"name":"isCustomFont","description":"Indicates if the font was downloaded or resolved locally.","type":"boolean"},{"name":"glyphCount","description":"Amount of glyphs that were rendered with this font.","type":"number"}]},{"id":"FontFace","description":"Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions","type":"object","properties":[{"name":"fontFamily","description":"The font-family.","type":"string"},{"name":"fontStyle","description":"The font-style.","type":"string"},{"name":"fontVariant","description":"The font-variant.","type":"string"},{"name":"fontWeight","description":"The font-weight.","type":"string"},{"name":"fontStretch","description":"The font-stretch.","type":"string"},{"name":"unicodeRange","description":"The unicode-range.","type":"string"},{"name":"src","description":"The src.","type":"string"},{"name":"platformFontFamily","description":"The resolved platform font family","type":"string"}]},{"id":"CSSKeyframesRule","description":"CSS keyframes rule representation.","type":"object","properties":[{"name":"animationName","description":"Animation name.","$ref":"Value"},{"name":"keyframes","description":"List of keyframes.","type":"array","items":{"$ref":"CSSKeyframeRule"}}]},{"id":"CSSKeyframeRule","description":"CSS keyframe rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"keyText","description":"Associated key text.","$ref":"Value"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"StyleDeclarationEdit","description":"A descriptor of operation to mutate style declaration text.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier.","$ref":"StyleSheetId"},{"name":"range","description":"The range of the style text in the enclosing stylesheet.","$ref":"SourceRange"},{"name":"text","description":"New style text.","type":"string"}]}],"commands":[{"name":"addRule","description":"Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\\nposition specified by `location`.","parameters":[{"name":"styleSheetId","description":"The css style sheet identifier where a new rule should be inserted.","$ref":"StyleSheetId"},{"name":"ruleText","description":"The text of a new rule.","type":"string"},{"name":"location","description":"Text position of a new rule in the target style sheet.","$ref":"SourceRange"}],"returns":[{"name":"rule","description":"The newly created rule.","$ref":"CSSRule"}]},{"name":"collectClassNames","description":"Returns all class names from specified stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"createStyleSheet","description":"Creates a new special \\"via-inspector\\" stylesheet in the frame with given `frameId`.","parameters":[{"name":"frameId","description":"Identifier of the frame where \\"via-inspector\\" stylesheet should be created.","$ref":"Page.FrameId"}],"returns":[{"name":"styleSheetId","description":"Identifier of the created \\"via-inspector\\" stylesheet.","$ref":"StyleSheetId"}]},{"name":"disable","description":"Disables the CSS agent for the given page."},{"name":"enable","description":"Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\\nenabled until the result of this command is received."},{"name":"forcePseudoState","description":"Ensures that the given node will have specified pseudo-classes whenever its style is computed by\\nthe browser.","parameters":[{"name":"nodeId","description":"The element id for which to force the pseudo state.","$ref":"DOM.NodeId"},{"name":"forcedPseudoClasses","description":"Element pseudo classes to force when computing the element\'s style.","type":"array","items":{"type":"string"}}]},{"name":"getBackgroundColors","parameters":[{"name":"nodeId","description":"Id of the node to get background colors for.","$ref":"DOM.NodeId"}],"returns":[{"name":"backgroundColors","description":"The range of background colors behind this element, if it contains any visible text. If no\\nvisible text is present, this will be undefined. In the case of a flat background color,\\nthis will consist of simply that color. In the case of a gradient, this will consist of each\\nof the color stops. For anything more complicated, this will be an empty array. Images will\\nbe ignored (as if the image had failed to load).","optional":true,"type":"array","items":{"type":"string"}},{"name":"computedFontSize","description":"The computed font size for this node, as a CSS computed value string (e.g. \'12px\').","optional":true,"type":"string"},{"name":"computedFontWeight","description":"The computed font weight for this node, as a CSS computed value string (e.g. \'normal\' or\\n\'100\').","optional":true,"type":"string"}]},{"name":"getComputedStyleForNode","description":"Returns the computed style for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"computedStyle","description":"Computed style for the specified DOM node.","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"getInlineStylesForNode","description":"Returns the styles defined inline (explicitly in the \\"style\\" attribute and implicitly, using DOM\\nattributes) for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"}]},{"name":"getMatchedStylesForNode","description":"Returns requested styles for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"CSS rules matching this node, from all applicable stylesheets.","optional":true,"type":"array","items":{"$ref":"RuleMatch"}},{"name":"pseudoElements","description":"Pseudo style matches for this node.","optional":true,"type":"array","items":{"$ref":"PseudoElementMatches"}},{"name":"inherited","description":"A chain of inherited styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedStyleEntry"}},{"name":"cssKeyframesRules","description":"A list of CSS keyframed animations matching this node.","optional":true,"type":"array","items":{"$ref":"CSSKeyframesRule"}}]},{"name":"getMediaQueries","description":"Returns all media queries parsed by the rendering engine.","returns":[{"name":"medias","type":"array","items":{"$ref":"CSSMedia"}}]},{"name":"getPlatformFontsForNode","description":"Requests information about platform fonts which we used to render child TextNodes in the given\\nnode.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"fonts","description":"Usage statistics for every employed platform font.","type":"array","items":{"$ref":"PlatformFontUsage"}}]},{"name":"getStyleSheetText","description":"Returns the current textual content for a stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"text","description":"The stylesheet text.","type":"string"}]},{"name":"setEffectivePropertyValueForNode","description":"Find a rule with the given active property for the given node and set the new value for this\\nproperty","parameters":[{"name":"nodeId","description":"The element id for which to set property.","$ref":"DOM.NodeId"},{"name":"propertyName","type":"string"},{"name":"value","type":"string"}]},{"name":"setKeyframeKey","description":"Modifies the keyframe rule key text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"keyText","type":"string"}],"returns":[{"name":"keyText","description":"The resulting key text after modification.","$ref":"Value"}]},{"name":"setMediaText","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"media","description":"The resulting CSS media rule after modification.","$ref":"CSSMedia"}]},{"name":"setRuleSelector","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"selector","type":"string"}],"returns":[{"name":"selectorList","description":"The resulting selector list after modification.","$ref":"SelectorList"}]},{"name":"setStyleSheetText","description":"Sets the new stylesheet text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"text","type":"string"}],"returns":[{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"}]},{"name":"setStyleTexts","description":"Applies specified style edits one after another in the given order.","parameters":[{"name":"edits","type":"array","items":{"$ref":"StyleDeclarationEdit"}}],"returns":[{"name":"styles","description":"The resulting styles after modification.","type":"array","items":{"$ref":"CSSStyle"}}]},{"name":"startRuleUsageTracking","description":"Enables the selector recording."},{"name":"stopRuleUsageTracking","description":"Stop tracking rule usage and return the list of rules that were used since last call to\\n`takeCoverageDelta` (or since start of coverage instrumentation)","returns":[{"name":"ruleUsage","type":"array","items":{"$ref":"RuleUsage"}}]},{"name":"takeCoverageDelta","description":"Obtain list of rules that became used since last call to this method (or since start of coverage\\ninstrumentation)","returns":[{"name":"coverage","type":"array","items":{"$ref":"RuleUsage"}}]}],"events":[{"name":"fontsUpdated","description":"Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\\nweb font","parameters":[{"name":"font","description":"The web font that has loaded.","optional":true,"$ref":"FontFace"}]},{"name":"mediaQueryResultChanged","description":"Fires whenever a MediaQuery result changes (for example, after a browser window has been\\nresized.) The current implementation considers only viewport-dependent media features."},{"name":"styleSheetAdded","description":"Fired whenever an active document stylesheet is added.","parameters":[{"name":"header","description":"Added stylesheet metainfo.","$ref":"CSSStyleSheetHeader"}]},{"name":"styleSheetChanged","description":"Fired whenever a stylesheet is changed as a result of the client operation.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}]},{"name":"styleSheetRemoved","description":"Fired whenever an active document stylesheet is removed.","parameters":[{"name":"styleSheetId","description":"Identifier of the removed stylesheet.","$ref":"StyleSheetId"}]}]},{"domain":"CacheStorage","experimental":true,"types":[{"id":"CacheId","description":"Unique identifier of the Cache object.","type":"string"},{"id":"CachedResponseType","description":"type of HTTP response cached","type":"string","enum":["basic","cors","default","error","opaqueResponse","opaqueRedirect"]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"requestURL","description":"Request URL.","type":"string"},{"name":"requestMethod","description":"Request method.","type":"string"},{"name":"requestHeaders","description":"Request headers","type":"array","items":{"$ref":"Header"}},{"name":"responseTime","description":"Number of seconds since epoch.","type":"number"},{"name":"responseStatus","description":"HTTP response status code.","type":"integer"},{"name":"responseStatusText","description":"HTTP response status text.","type":"string"},{"name":"responseType","description":"HTTP response type","$ref":"CachedResponseType"},{"name":"responseHeaders","description":"Response headers","type":"array","items":{"$ref":"Header"}}]},{"id":"Cache","description":"Cache identifier.","type":"object","properties":[{"name":"cacheId","description":"An opaque unique id of the cache.","$ref":"CacheId"},{"name":"securityOrigin","description":"Security origin of the cache.","type":"string"},{"name":"cacheName","description":"The name of the cache.","type":"string"}]},{"id":"Header","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"CachedResponse","description":"Cached response","type":"object","properties":[{"name":"body","description":"Entry content, base64-encoded.","type":"string"}]}],"commands":[{"name":"deleteCache","description":"Deletes a cache.","parameters":[{"name":"cacheId","description":"Id of cache for deletion.","$ref":"CacheId"}]},{"name":"deleteEntry","description":"Deletes a cache entry.","parameters":[{"name":"cacheId","description":"Id of cache where the entry will be deleted.","$ref":"CacheId"},{"name":"request","description":"URL spec of the request.","type":"string"}]},{"name":"requestCacheNames","description":"Requests cache names.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"}],"returns":[{"name":"caches","description":"Caches for the security origin.","type":"array","items":{"$ref":"Cache"}}]},{"name":"requestCachedResponse","description":"Fetches cache entry.","parameters":[{"name":"cacheId","description":"Id of cache that contains the entry.","$ref":"CacheId"},{"name":"requestURL","description":"URL spec of the request.","type":"string"},{"name":"requestHeaders","description":"headers of the request.","type":"array","items":{"$ref":"Header"}}],"returns":[{"name":"response","description":"Response read from the cache.","$ref":"CachedResponse"}]},{"name":"requestEntries","description":"Requests data from cache.","parameters":[{"name":"cacheId","description":"ID of cache to get entries from.","$ref":"CacheId"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"pathFilter","description":"If present, only return the entries containing this substring in the path","optional":true,"type":"string"}],"returns":[{"name":"cacheDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"returnCount","description":"Count of returned entries from this storage. If pathFilter is empty, it\\nis the count of all entries from this storage.","type":"number"}]}]},{"domain":"Cast","description":"A domain for interacting with Cast, Presentation API, and Remote Playback API\\nfunctionalities.","experimental":true,"types":[{"id":"Sink","type":"object","properties":[{"name":"name","type":"string"},{"name":"id","type":"string"},{"name":"session","description":"Text describing the current session. Present only if there is an active\\nsession on the sink.","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Starts observing for sinks that can be used for tab mirroring, and if set,\\nsinks compatible with |presentationUrl| as well. When sinks are found, a\\n|sinksUpdated| event is fired.\\nAlso starts observing for issue messages. When an issue is added or removed,\\nan |issueUpdated| event is fired.","parameters":[{"name":"presentationUrl","optional":true,"type":"string"}]},{"name":"disable","description":"Stops observing for sinks and issues."},{"name":"setSinkToUse","description":"Sets a sink to be used when the web page requests the browser to choose a\\nsink via Presentation API, Remote Playback API, or Cast SDK.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startTabMirroring","description":"Starts mirroring the tab to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"stopCasting","description":"Stops the active Cast session on the sink.","parameters":[{"name":"sinkName","type":"string"}]}],"events":[{"name":"sinksUpdated","description":"This is fired whenever the list of available sinks changes. A sink is a\\ndevice or a software surface that you can cast to.","parameters":[{"name":"sinks","type":"array","items":{"$ref":"Sink"}}]},{"name":"issueUpdated","description":"This is fired whenever the outstanding issue/error message changes.\\n|issueMessage| is empty if there is no issue.","parameters":[{"name":"issueMessage","type":"string"}]}]},{"domain":"DOM","description":"This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\\nand never sends the same node twice. It is client\'s responsibility to collect information about\\nthe nodes that were sent to the client.<p>Note that `iframe` owner elements will return\\ncorresponding document elements as their child nodes.</p>","dependencies":["Runtime"],"types":[{"id":"NodeId","description":"Unique DOM node identifier.","type":"integer"},{"id":"BackendNodeId","description":"Unique DOM node identifier used to reference a node that may not have been pushed to the\\nfront-end.","type":"integer"},{"id":"BackendNode","description":"Backend node with a friendly name.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"backendNodeId","$ref":"BackendNodeId"}]},{"id":"PseudoType","description":"Pseudo element type.","type":"string","enum":["first-line","first-letter","before","after","backdrop","selection","first-line-inherited","scrollbar","scrollbar-thumb","scrollbar-button","scrollbar-track","scrollbar-track-piece","scrollbar-corner","resizer","input-list-button"]},{"id":"ShadowRootType","description":"Shadow root type.","type":"string","enum":["user-agent","open","closed"]},{"id":"Node","description":"DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\\nDOMNode is a base node mirror type.","type":"object","properties":[{"name":"nodeId","description":"Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\\nwill only push node with given `id` once. It is aware of all requested nodes and will only\\nfire DOM events for nodes known to the client.","$ref":"NodeId"},{"name":"parentId","description":"The id of the parent node if any.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"The BackendNodeId for this node.","$ref":"BackendNodeId"},{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"localName","description":"`Node`\'s localName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"childNodeCount","description":"Child count for `Container` nodes.","optional":true,"type":"integer"},{"name":"children","description":"Child nodes of this node when requested with children.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"attributes","description":"Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.","optional":true,"type":"array","items":{"type":"string"}},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType`\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType`\'s systemId.","optional":true,"type":"string"},{"name":"internalSubset","description":"`DocumentType`\'s internalSubset.","optional":true,"type":"string"},{"name":"xmlVersion","description":"`Document`\'s XML version in case of XML documents.","optional":true,"type":"string"},{"name":"name","description":"`Attr`\'s name.","optional":true,"type":"string"},{"name":"value","description":"`Attr`\'s value.","optional":true,"type":"string"},{"name":"pseudoType","description":"Pseudo element type for this node.","optional":true,"$ref":"PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"ShadowRootType"},{"name":"frameId","description":"Frame ID for frame owner elements.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocument","description":"Content document for frame owner elements.","optional":true,"$ref":"Node"},{"name":"shadowRoots","description":"Shadow root list for given element host.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"templateContent","description":"Content document fragment for template elements.","optional":true,"$ref":"Node"},{"name":"pseudoElements","description":"Pseudo elements associated with this node.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"importedDocument","description":"Import document for the HTMLImport links.","optional":true,"$ref":"Node"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","optional":true,"type":"array","items":{"$ref":"BackendNode"}},{"name":"isSVG","description":"Whether the node is SVG.","optional":true,"type":"boolean"}]},{"id":"RGBA","description":"A structure holding an RGBA color.","type":"object","properties":[{"name":"r","description":"The red component, in the [0-255] range.","type":"integer"},{"name":"g","description":"The green component, in the [0-255] range.","type":"integer"},{"name":"b","description":"The blue component, in the [0-255] range.","type":"integer"},{"name":"a","description":"The alpha component, in the [0-1] range (default: 1).","optional":true,"type":"number"}]},{"id":"Quad","description":"An array of quad vertices, x immediately followed by y for each point, points clock-wise.","type":"array","items":{"type":"number"}},{"id":"BoxModel","description":"Box model.","type":"object","properties":[{"name":"content","description":"Content box","$ref":"Quad"},{"name":"padding","description":"Padding box","$ref":"Quad"},{"name":"border","description":"Border box","$ref":"Quad"},{"name":"margin","description":"Margin box","$ref":"Quad"},{"name":"width","description":"Node width","type":"integer"},{"name":"height","description":"Node height","type":"integer"},{"name":"shapeOutside","description":"Shape outside coordinates","optional":true,"$ref":"ShapeOutsideInfo"}]},{"id":"ShapeOutsideInfo","description":"CSS Shape Outside details.","type":"object","properties":[{"name":"bounds","description":"Shape bounds","$ref":"Quad"},{"name":"shape","description":"Shape coordinate details","type":"array","items":{"type":"any"}},{"name":"marginShape","description":"Margin shape bounds","type":"array","items":{"type":"any"}}]},{"id":"Rect","description":"Rectangle.","type":"object","properties":[{"name":"x","description":"X coordinate","type":"number"},{"name":"y","description":"Y coordinate","type":"number"},{"name":"width","description":"Rectangle width","type":"number"},{"name":"height","description":"Rectangle height","type":"number"}]}],"commands":[{"name":"collectClassNamesFromSubtree","description":"Collects class names for the node with given id and all of it\'s child nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to collect class names.","$ref":"NodeId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"copyTo","description":"Creates a deep copy of the specified node and places it into the target container before the\\ngiven anchor.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to copy.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the copy into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop the copy before this node (if absent, the copy becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Id of the node clone.","$ref":"NodeId"}]},{"name":"describeNode","description":"Describes node given its id, does not require domain to be enabled. Does not start tracking any\\nobjects, can be used for automation.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"node","description":"Node description.","$ref":"Node"}]},{"name":"disable","description":"Disables DOM agent for the given page."},{"name":"discardSearchResults","description":"Discards search results from the session with the given id. `getSearchResults` should no longer\\nbe called for that search.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"}]},{"name":"enable","description":"Enables DOM agent for the given page."},{"name":"focus","description":"Focuses the given element.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getAttributes","description":"Returns attributes for the specified node.","parameters":[{"name":"nodeId","description":"Id of the node to retrieve attibutes for.","$ref":"NodeId"}],"returns":[{"name":"attributes","description":"An interleaved array of node attribute names and values.","type":"array","items":{"type":"string"}}]},{"name":"getBoxModel","description":"Returns boxes for the given node.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"model","description":"Box model for the node.","$ref":"BoxModel"}]},{"name":"getContentQuads","description":"Returns quads that describe node position on the page. This method\\nmight return multiple quads for inline nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"quads","description":"Quads that describe node layout relative to viewport.","type":"array","items":{"$ref":"Quad"}}]},{"name":"getDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"root","description":"Resulting node.","$ref":"Node"}]},{"name":"getFlattenedDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"Resulting node.","type":"array","items":{"$ref":"Node"}}]},{"name":"getNodeForLocation","description":"Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\\neither returned or not.","experimental":true,"parameters":[{"name":"x","description":"X coordinate.","type":"integer"},{"name":"y","description":"Y coordinate.","type":"integer"},{"name":"includeUserAgentShadowDOM","description":"False to skip to the nearest non-UA shadow root ancestor (default: false).","optional":true,"type":"boolean"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getOuterHTML","description":"Returns node\'s HTML markup.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"outerHTML","description":"Outer HTML markup.","type":"string"}]},{"name":"getRelayoutBoundary","description":"Returns the id of the nearest ancestor that is a relayout boundary.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node.","$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Relayout boundary node id for the given node.","$ref":"NodeId"}]},{"name":"getSearchResults","description":"Returns search results from given `fromIndex` to given `toIndex` from the search with the given\\nidentifier.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"fromIndex","description":"Start index of the search result to be returned.","type":"integer"},{"name":"toIndex","description":"End index of the search result to be returned.","type":"integer"}],"returns":[{"name":"nodeIds","description":"Ids of the search result nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"hideHighlight","description":"Hides any highlight.","redirect":"Overlay"},{"name":"highlightNode","description":"Highlights DOM node.","redirect":"Overlay"},{"name":"highlightRect","description":"Highlights given rectangle.","redirect":"Overlay"},{"name":"markUndoableState","description":"Marks last undoable state.","experimental":true},{"name":"moveTo","description":"Moves node into the new container, places it before the given anchor.","parameters":[{"name":"nodeId","description":"Id of the node to move.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the moved node into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop node before this one (if absent, the moved node becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"New id of the moved node.","$ref":"NodeId"}]},{"name":"performSearch","description":"Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\\n`cancelSearch` to end this search session.","experimental":true,"parameters":[{"name":"query","description":"Plain text or query selector or XPath search query.","type":"string"},{"name":"includeUserAgentShadowDOM","description":"True to search in user agent shadow DOM.","optional":true,"type":"boolean"}],"returns":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"resultCount","description":"Number of search results.","type":"integer"}]},{"name":"pushNodeByPathToFrontend","description":"Requests that the node is sent to the caller given its path. // FIXME, use XPath","experimental":true,"parameters":[{"name":"path","description":"Path to node in the proprietary format.","type":"string"}],"returns":[{"name":"nodeId","description":"Id of the node for given path.","$ref":"NodeId"}]},{"name":"pushNodesByBackendIdsToFrontend","description":"Requests that a batch of nodes is sent to the caller given their backend node ids.","experimental":true,"parameters":[{"name":"backendNodeIds","description":"The array of backend node ids.","type":"array","items":{"$ref":"BackendNodeId"}}],"returns":[{"name":"nodeIds","description":"The array of ids of pushed nodes that correspond to the backend ids specified in\\nbackendNodeIds.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"querySelector","description":"Executes `querySelector` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeId","description":"Query selector result.","$ref":"NodeId"}]},{"name":"querySelectorAll","description":"Executes `querySelectorAll` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeIds","description":"Query selector result.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"redo","description":"Re-does the last undone action.","experimental":true},{"name":"removeAttribute","description":"Removes attribute with given name from an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to remove attribute from.","$ref":"NodeId"},{"name":"name","description":"Name of the attribute to remove.","type":"string"}]},{"name":"removeNode","description":"Removes node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to remove.","$ref":"NodeId"}]},{"name":"requestChildNodes","description":"Requests that children of the node with given id are returned to the caller in form of\\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\\nthe specified depth.","parameters":[{"name":"nodeId","description":"Id of the node to get children for.","$ref":"NodeId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the sub-tree\\n(default is false).","optional":true,"type":"boolean"}]},{"name":"requestNode","description":"Requests that the node is sent to the caller given the JavaScript node object reference. All\\nnodes that form the path from the node to the root are also sent to the client as a series of\\n`setChildNodes` notifications.","parameters":[{"name":"objectId","description":"JavaScript object id to convert into node.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodeId","description":"Node id for given object.","$ref":"NodeId"}]},{"name":"resolveNode","description":"Resolves the JavaScript node object for a given NodeId or BackendNodeId.","parameters":[{"name":"nodeId","description":"Id of the node to resolve.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Backend identifier of the node to resolve.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"executionContextId","description":"Execution context in which to resolve the node.","optional":true,"$ref":"Runtime.ExecutionContextId"}],"returns":[{"name":"object","description":"JavaScript object wrapper for given node.","$ref":"Runtime.RemoteObject"}]},{"name":"setAttributeValue","description":"Sets attribute for an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to set attribute for.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"setAttributesAsText","description":"Sets attributes on element with given id. This method is useful when user edits some existing\\nattribute value and types in several attribute name/value pairs.","parameters":[{"name":"nodeId","description":"Id of the element to set attributes for.","$ref":"NodeId"},{"name":"text","description":"Text with a number of attributes. Will parse this text using HTML parser.","type":"string"},{"name":"name","description":"Attribute name to replace with new attributes derived from text in case text parsed\\nsuccessfully.","optional":true,"type":"string"}]},{"name":"setFileInputFiles","description":"Sets files for the given file input element.","parameters":[{"name":"files","description":"Array of file paths to set.","type":"array","items":{"type":"string"}},{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getFileInfo","description":"Returns file information for the given\\nFile wrapper.","experimental":true,"parameters":[{"name":"objectId","description":"JavaScript object id of the node wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"path","type":"string"}]},{"name":"setInspectedNode","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","experimental":true,"parameters":[{"name":"nodeId","description":"DOM node id to be accessible by means of $x command line API.","$ref":"NodeId"}]},{"name":"setNodeName","description":"Sets node name for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set name for.","$ref":"NodeId"},{"name":"name","description":"New node\'s name.","type":"string"}],"returns":[{"name":"nodeId","description":"New node\'s id.","$ref":"NodeId"}]},{"name":"setNodeValue","description":"Sets node value for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set value for.","$ref":"NodeId"},{"name":"value","description":"New node\'s value.","type":"string"}]},{"name":"setOuterHTML","description":"Sets node HTML markup, returns new node id.","parameters":[{"name":"nodeId","description":"Id of the node to set markup for.","$ref":"NodeId"},{"name":"outerHTML","description":"Outer HTML markup to set.","type":"string"}]},{"name":"undo","description":"Undoes the last performed action.","experimental":true},{"name":"getFrameOwner","description":"Returns iframe node that owns iframe with the given domain.","experimental":true,"parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]}],"events":[{"name":"attributeModified","description":"Fired when `Element`\'s attribute is modified.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"attributeRemoved","description":"Fired when `Element`\'s attribute is removed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"A ttribute name.","type":"string"}]},{"name":"characterDataModified","description":"Mirrors `DOMCharacterDataModified` event.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"characterData","description":"New text value.","type":"string"}]},{"name":"childNodeCountUpdated","description":"Fired when `Container`\'s child node count has changed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"childNodeCount","description":"New node count.","type":"integer"}]},{"name":"childNodeInserted","description":"Mirrors `DOMNodeInserted` event.","parameters":[{"name":"parentNodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"previousNodeId","description":"If of the previous siblint.","$ref":"NodeId"},{"name":"node","description":"Inserted node data.","$ref":"Node"}]},{"name":"childNodeRemoved","description":"Mirrors `DOMNodeRemoved` event.","parameters":[{"name":"parentNodeId","description":"Parent id.","$ref":"NodeId"},{"name":"nodeId","description":"Id of the node that has been removed.","$ref":"NodeId"}]},{"name":"distributedNodesUpdated","description":"Called when distrubution is changed.","experimental":true,"parameters":[{"name":"insertionPointId","description":"Insertion point where distrubuted nodes were updated.","$ref":"NodeId"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","type":"array","items":{"$ref":"BackendNode"}}]},{"name":"documentUpdated","description":"Fired when `Document` has been totally updated. Node ids are no longer valid."},{"name":"inlineStyleInvalidated","description":"Fired when `Element`\'s inline style is modified via a CSS property modification.","experimental":true,"parameters":[{"name":"nodeIds","description":"Ids of the nodes for which the inline styles have been invalidated.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"pseudoElementAdded","description":"Called when a pseudo element is added to an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElement","description":"The added pseudo element.","$ref":"Node"}]},{"name":"pseudoElementRemoved","description":"Called when a pseudo element is removed from an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElementId","description":"The removed pseudo element id.","$ref":"NodeId"}]},{"name":"setChildNodes","description":"Fired when backend wants to provide client with the missing DOM structure. This happens upon\\nmost of the calls requesting node ids.","parameters":[{"name":"parentId","description":"Parent node id to populate with children.","$ref":"NodeId"},{"name":"nodes","description":"Child nodes array.","type":"array","items":{"$ref":"Node"}}]},{"name":"shadowRootPopped","description":"Called when shadow root is popped from the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"rootId","description":"Shadow root id.","$ref":"NodeId"}]},{"name":"shadowRootPushed","description":"Called when shadow root is pushed into the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"root","description":"Shadow root.","$ref":"Node"}]}]},{"domain":"DOMDebugger","description":"DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\\nexecution will stop on these operations as if there was a regular breakpoint set.","dependencies":["DOM","Debugger","Runtime"],"types":[{"id":"DOMBreakpointType","description":"DOM breakpoint type.","type":"string","enum":["subtree-modified","attribute-modified","node-removed"]},{"id":"EventListener","description":"Object event listener.","type":"object","properties":[{"name":"type","description":"`EventListener`\'s type.","type":"string"},{"name":"useCapture","description":"`EventListener`\'s useCapture.","type":"boolean"},{"name":"passive","description":"`EventListener`\'s passive flag.","type":"boolean"},{"name":"once","description":"`EventListener`\'s once flag.","type":"boolean"},{"name":"scriptId","description":"Script id of the handler code.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","type":"integer"},{"name":"handler","description":"Event handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"originalHandler","description":"Event original handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"backendNodeId","description":"Node the listener is added to (if any).","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"getEventListeners","description":"Returns event listeners of the given object.","parameters":[{"name":"objectId","description":"Identifier of the object to return listeners for.","$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false). Reports listeners for all contexts if pierce is enabled.","optional":true,"type":"boolean"}],"returns":[{"name":"listeners","description":"Array of relevant listeners.","type":"array","items":{"$ref":"EventListener"}}]},{"name":"removeDOMBreakpoint","description":"Removes DOM breakpoint that was set using `setDOMBreakpoint`.","parameters":[{"name":"nodeId","description":"Identifier of the node to remove breakpoint from.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the breakpoint to remove.","$ref":"DOMBreakpointType"}]},{"name":"removeEventListenerBreakpoint","description":"Removes breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"Event name.","type":"string"},{"name":"targetName","description":"EventTarget interface name.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeXHRBreakpoint","description":"Removes breakpoint from XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring.","type":"string"}]},{"name":"setDOMBreakpoint","description":"Sets breakpoint on particular operation with DOM.","parameters":[{"name":"nodeId","description":"Identifier of the node to set breakpoint on.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the operation to stop upon.","$ref":"DOMBreakpointType"}]},{"name":"setEventListenerBreakpoint","description":"Sets breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"DOM Event name to stop on (any DOM event will do).","type":"string"},{"name":"targetName","description":"EventTarget interface name to stop on. If equal to `\\"*\\"` or not provided, will stop on any\\nEventTarget.","experimental":true,"optional":true,"type":"string"}]},{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"setXHRBreakpoint","description":"Sets breakpoint on XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring. All XHRs having this substring in the URL will get stopped upon.","type":"string"}]}]},{"domain":"DOMSnapshot","description":"This domain facilitates obtaining document snapshots with DOM, layout, and style information.","experimental":true,"dependencies":["CSS","DOM","DOMDebugger","Page"],"types":[{"id":"DOMNode","description":"A Node in the DOM tree.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"type":"string"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"type":"string"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"type":"boolean"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"type":"boolean"},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","$ref":"DOM.BackendNodeId"},{"name":"childNodeIndexes","description":"The indexes of the node\'s child nodes in the `domNodes` array returned by `getSnapshot`, if\\nany.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"attributes","description":"Attributes of an `Element` node.","optional":true,"type":"array","items":{"$ref":"NameValue"}},{"name":"pseudoElementIndexes","description":"Indexes of pseudo elements associated with this node in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"layoutNodeIndex","description":"The index of the node\'s related layout tree node in the `layoutTreeNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"contentLanguage","description":"Only set for documents, contains the document\'s content language.","optional":true,"type":"string"},{"name":"documentEncoding","description":"Only set for documents, contains the document\'s character set encoding.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","optional":true,"type":"string"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocumentIndex","description":"The index of a frame owner element\'s content document in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"DOM.PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"DOM.ShadowRootType"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"type":"boolean"},{"name":"eventListeners","description":"Details of the node\'s event listeners, if any.","optional":true,"type":"array","items":{"$ref":"DOMDebugger.EventListener"}},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"type":"string"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"type":"string"},{"name":"scrollOffsetX","description":"Scroll offsets, set when this node is a Document.","optional":true,"type":"number"},{"name":"scrollOffsetY","optional":true,"type":"number"}]},{"id":"InlineTextBox","description":"Details of post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"startCharacterIndex","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"integer"},{"name":"numCharacters","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"integer"}]},{"id":"LayoutTreeNode","description":"Details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"domNodeIndex","description":"The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.","type":"integer"},{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"layoutText","description":"Contents of the LayoutText, if any.","optional":true,"type":"string"},{"name":"inlineTextNodes","description":"The post-layout inline text nodes, if any.","optional":true,"type":"array","items":{"$ref":"InlineTextBox"}},{"name":"styleIndex","description":"Index into the `computedStyles` array returned by `getSnapshot`.","optional":true,"type":"integer"},{"name":"paintOrder","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ngetSnapshot was true.","optional":true,"type":"integer"},{"name":"isStackingContext","description":"Set to true to indicate the element begins a new stacking context.","optional":true,"type":"boolean"}]},{"id":"ComputedStyle","description":"A subset of the full ComputedStyle as defined by the request whitelist.","type":"object","properties":[{"name":"properties","description":"Name/value pairs of computed style properties.","type":"array","items":{"$ref":"NameValue"}}]},{"id":"NameValue","description":"A name/value pair.","type":"object","properties":[{"name":"name","description":"Attribute/property name.","type":"string"},{"name":"value","description":"Attribute/property value.","type":"string"}]},{"id":"StringIndex","description":"Index of the string in the strings table.","type":"integer"},{"id":"ArrayOfStrings","description":"Index of the string in the strings table.","type":"array","items":{"$ref":"StringIndex"}},{"id":"RareStringData","description":"Data that is only present on rare nodes.","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"$ref":"StringIndex"}}]},{"id":"RareBooleanData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}}]},{"id":"RareIntegerData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"type":"integer"}}]},{"id":"Rectangle","type":"array","items":{"type":"number"}},{"id":"DocumentSnapshot","description":"Document snapshot.","type":"object","properties":[{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","$ref":"StringIndex"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","$ref":"StringIndex"},{"name":"contentLanguage","description":"Contains the document\'s content language.","$ref":"StringIndex"},{"name":"encodingName","description":"Contains the document\'s character set encoding.","$ref":"StringIndex"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","$ref":"StringIndex"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","$ref":"StringIndex"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","$ref":"StringIndex"},{"name":"nodes","description":"A table with dom nodes.","$ref":"NodeTreeSnapshot"},{"name":"layout","description":"The nodes in the layout tree.","$ref":"LayoutTreeSnapshot"},{"name":"textBoxes","description":"The post-layout inline text nodes.","$ref":"TextBoxSnapshot"},{"name":"scrollOffsetX","description":"Horizontal scroll offset.","optional":true,"type":"number"},{"name":"scrollOffsetY","description":"Vertical scroll offset.","optional":true,"type":"number"}]},{"id":"NodeTreeSnapshot","description":"Table containing nodes.","type":"object","properties":[{"name":"parentIndex","description":"Parent node index.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeType","description":"`Node`\'s nodeType.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeName","description":"`Node`\'s nodeName.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"nodeValue","description":"`Node`\'s nodeValue.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","optional":true,"type":"array","items":{"$ref":"DOM.BackendNodeId"}},{"name":"attributes","description":"Attributes of an `Element` node. Flatten name, value pairs.","optional":true,"type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"$ref":"RareStringData"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"$ref":"RareStringData"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"$ref":"RareBooleanData"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"$ref":"RareBooleanData"},{"name":"contentDocumentIndex","description":"The index of the document in the list of the snapshot documents.","optional":true,"$ref":"RareIntegerData"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"RareStringData"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"$ref":"RareBooleanData"},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"$ref":"RareStringData"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"$ref":"RareStringData"}]},{"id":"LayoutTreeSnapshot","description":"Table of details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"nodeIndex","description":"Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.","type":"array","items":{"type":"integer"}},{"name":"styles","description":"Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.","type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"text","description":"Contents of the LayoutText, if any.","type":"array","items":{"$ref":"StringIndex"}},{"name":"stackingContexts","description":"Stacking context information.","$ref":"RareBooleanData"},{"name":"offsetRects","description":"The offset rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"scrollRects","description":"The scroll rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"clientRects","description":"The client rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}}]},{"id":"TextBoxSnapshot","description":"Table of details of the post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"layoutIndex","description":"Index of the layout tree node that owns this box collection.","type":"array","items":{"type":"integer"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"start","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}},{"name":"length","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}}]}],"commands":[{"name":"disable","description":"Disables DOM snapshot agent for the given page."},{"name":"enable","description":"Enables DOM snapshot agent for the given page."},{"name":"getSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","deprecated":true,"parameters":[{"name":"computedStyleWhitelist","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeEventListeners","description":"Whether or not to retrieve details of DOM listeners (default false).","optional":true,"type":"boolean"},{"name":"includePaintOrder","description":"Whether to determine and include the paint order index of LayoutTreeNodes (default false).","optional":true,"type":"boolean"},{"name":"includeUserAgentShadowTree","description":"Whether to include UA shadow tree in the snapshot (default false).","optional":true,"type":"boolean"}],"returns":[{"name":"domNodes","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DOMNode"}},{"name":"layoutTreeNodes","description":"The nodes in the layout tree.","type":"array","items":{"$ref":"LayoutTreeNode"}},{"name":"computedStyles","description":"Whitelisted ComputedStyle properties for each node in the layout tree.","type":"array","items":{"$ref":"ComputedStyle"}}]},{"name":"captureSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","parameters":[{"name":"computedStyles","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeDOMRects","description":"Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot","optional":true,"type":"boolean"}],"returns":[{"name":"documents","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DocumentSnapshot"}},{"name":"strings","description":"Shared string table that all string properties refer to with indexes.","type":"array","items":{"type":"string"}}]}]},{"domain":"DOMStorage","description":"Query and modify DOM storage.","experimental":true,"types":[{"id":"StorageId","description":"DOM Storage identifier.","type":"object","properties":[{"name":"securityOrigin","description":"Security origin for the storage.","type":"string"},{"name":"isLocalStorage","description":"Whether the storage is local storage (not session storage).","type":"boolean"}]},{"id":"Item","description":"DOM Storage item.","type":"array","items":{"type":"string"}}],"commands":[{"name":"clear","parameters":[{"name":"storageId","$ref":"StorageId"}]},{"name":"disable","description":"Disables storage tracking, prevents storage events from being sent to the client."},{"name":"enable","description":"Enables storage tracking, storage events will now be delivered to the client."},{"name":"getDOMStorageItems","parameters":[{"name":"storageId","$ref":"StorageId"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"Item"}}]},{"name":"removeDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"setDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"value","type":"string"}]}],"events":[{"name":"domStorageItemAdded","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemRemoved","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"domStorageItemUpdated","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"oldValue","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemsCleared","parameters":[{"name":"storageId","$ref":"StorageId"}]}]},{"domain":"Database","experimental":true,"types":[{"id":"DatabaseId","description":"Unique identifier of Database object.","type":"string"},{"id":"Database","description":"Database object.","type":"object","properties":[{"name":"id","description":"Database ID.","$ref":"DatabaseId"},{"name":"domain","description":"Database domain.","type":"string"},{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version.","type":"string"}]},{"id":"Error","description":"Database error.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"code","description":"Error code.","type":"integer"}]}],"commands":[{"name":"disable","description":"Disables database tracking, prevents database events from being sent to the client."},{"name":"enable","description":"Enables database tracking, database events will now be delivered to the client."},{"name":"executeSQL","parameters":[{"name":"databaseId","$ref":"DatabaseId"},{"name":"query","type":"string"}],"returns":[{"name":"columnNames","optional":true,"type":"array","items":{"type":"string"}},{"name":"values","optional":true,"type":"array","items":{"type":"any"}},{"name":"sqlError","optional":true,"$ref":"Error"}]},{"name":"getDatabaseTableNames","parameters":[{"name":"databaseId","$ref":"DatabaseId"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]}],"events":[{"name":"addDatabase","parameters":[{"name":"database","$ref":"Database"}]}]},{"domain":"DeviceOrientation","experimental":true,"commands":[{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation."},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]}]},{"domain":"Emulation","description":"This domain emulates different environments for the page.","dependencies":["DOM","Page","Runtime"],"types":[{"id":"ScreenOrientation","description":"Screen orientation.","type":"object","properties":[{"name":"type","description":"Orientation type.","type":"string","enum":["portraitPrimary","portraitSecondary","landscapePrimary","landscapeSecondary"]},{"name":"angle","description":"Orientation angle.","type":"integer"}]},{"id":"VirtualTimePolicy","description":"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\\nresource fetches.","experimental":true,"type":"string","enum":["advance","pause","pauseIfNetworkFetchesPending"]}],"commands":[{"name":"canEmulate","description":"Tells whether emulation is supported.","returns":[{"name":"result","description":"True if emulation is supported.","type":"boolean"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overriden device metrics."},{"name":"clearGeolocationOverride","description":"Clears the overriden Geolocation Position and Error."},{"name":"resetPageScaleFactor","description":"Requests that page scale factor is reset to initial values.","experimental":true},{"name":"setFocusEmulationEnabled","description":"Enables or disables simulating a focused and active page.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable to disable focus emulation.","type":"boolean"}]},{"name":"setCPUThrottlingRate","description":"Enables CPU throttling to emulate slow CPUs.","experimental":true,"parameters":[{"name":"rate","description":"Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).","type":"number"}]},{"name":"setDefaultBackgroundColorOverride","description":"Sets or clears an override of the default background color of the frame. This override is used\\nif the content does not specify one.","parameters":[{"name":"color","description":"RGBA of the default background color. If not specified, any existing override will be\\ncleared.","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","experimental":true,"optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","experimental":true,"optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"ScreenOrientation"},{"name":"viewport","description":"If set, the visible area of the page will be overridden to this viewport. This viewport\\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.","experimental":true,"optional":true,"$ref":"Page.Viewport"}]},{"name":"setScrollbarsHidden","experimental":true,"parameters":[{"name":"hidden","description":"Whether scrollbars should be always hidden.","type":"boolean"}]},{"name":"setDocumentCookieDisabled","experimental":true,"parameters":[{"name":"disabled","description":"Whether document.coookie API should be disabled.","type":"boolean"}]},{"name":"setEmitTouchEventsForMouse","experimental":true,"parameters":[{"name":"enabled","description":"Whether touch emulation based on mouse input should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"setEmulatedMedia","description":"Emulates the given media for CSS media queries.","parameters":[{"name":"media","description":"Media type to emulate. Empty string disables the override.","type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setNavigatorOverrides","description":"Overrides value returned by the javascript navigator object.","experimental":true,"deprecated":true,"parameters":[{"name":"platform","description":"The platform navigator.platform should return.","type":"string"}]},{"name":"setPageScaleFactor","description":"Sets a specified page scale factor.","experimental":true,"parameters":[{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"}]},{"name":"setScriptExecutionDisabled","description":"Switches script execution in the page.","parameters":[{"name":"value","description":"Whether script execution should be disabled in the page.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Enables touch on platforms which do not support them.","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"maxTouchPoints","description":"Maximum touch points supported. Defaults to one.","optional":true,"type":"integer"}]},{"name":"setVirtualTimePolicy","description":"Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\\nthe current virtual time policy. Note this supersedes any previous time budget.","experimental":true,"parameters":[{"name":"policy","$ref":"VirtualTimePolicy"},{"name":"budget","description":"If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\\nvirtualTimeBudgetExpired event is sent.","optional":true,"type":"number"},{"name":"maxVirtualTimeTaskStarvationCount","description":"If set this specifies the maximum number of tasks that can be run before virtual is forced\\nforwards to prevent deadlock.","optional":true,"type":"integer"},{"name":"waitForNavigation","description":"If set the virtual time policy change should be deferred until any frame starts navigating.\\nNote any previous deferred policy change is superseded.","optional":true,"type":"boolean"},{"name":"initialVirtualTime","description":"If set, base::Time::Now will be overriden to initially return this value.","optional":true,"$ref":"Network.TimeSinceEpoch"}],"returns":[{"name":"virtualTimeTicksBase","description":"Absolute timestamp at which virtual time was first enabled (up time in milliseconds).","type":"number"}]},{"name":"setTimezoneOverride","description":"Overrides default host system timezone with the specified one.","experimental":true,"parameters":[{"name":"timezoneId","description":"The timezone identifier. If empty, disables the override and\\nrestores default host system timezone.","type":"string"}]},{"name":"setVisibleSize","description":"Resizes the frame/viewport of the page. Note that this does not affect the frame\'s container\\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\\non Android.","experimental":true,"deprecated":true,"parameters":[{"name":"width","description":"Frame width (DIP).","type":"integer"},{"name":"height","description":"Frame height (DIP).","type":"integer"}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"}]}],"events":[{"name":"virtualTimeBudgetExpired","description":"Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.","experimental":true}]},{"domain":"HeadlessExperimental","description":"This domain provides experimental commands only supported in headless mode.","experimental":true,"dependencies":["Page","Runtime"],"types":[{"id":"ScreenshotParams","description":"Encoding options for a screenshot.","type":"object","properties":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"}]}],"commands":[{"name":"beginFrame","description":"Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\\nscreenshot from the resulting frame. Requires that the target was created with enabled\\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\\nhttps://goo.gl/3zHXhB for more background.","parameters":[{"name":"frameTimeTicks","description":"Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\\nthe current time will be used.","optional":true,"type":"number"},{"name":"interval","description":"The interval between BeginFrames that is reported to the compositor, in milliseconds.\\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.","optional":true,"type":"number"},{"name":"noDisplayUpdates","description":"Whether updates should not be committed and drawn onto the display. False by default. If\\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\\nany visual updates may not be visible on the display or in screenshots.","optional":true,"type":"boolean"},{"name":"screenshot","description":"If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\\nduring renderer initialization. In such a case, no screenshot data will be returned.","optional":true,"$ref":"ScreenshotParams"}],"returns":[{"name":"hasDamage","description":"Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\\ndisplay. Reported for diagnostic uses, may be removed in the future.","type":"boolean"},{"name":"screenshotData","description":"Base64-encoded image data of the screenshot, if one was requested and successfully taken.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables headless events for the target."},{"name":"enable","description":"Enables headless events for the target."}],"events":[{"name":"needsBeginFramesChanged","description":"Issued when the target starts or stops needing BeginFrames.","parameters":[{"name":"needsBeginFrames","description":"True if BeginFrames are needed, false otherwise.","type":"boolean"}]}]},{"domain":"IO","description":"Input/Output operations for streams produced by DevTools.","types":[{"id":"StreamHandle","description":"This is either obtained from another method or specifed as `blob:<uuid>` where\\n`<uuid>` is an UUID of a Blob.","type":"string"}],"commands":[{"name":"close","description":"Close the stream, discard any temporary backing storage.","parameters":[{"name":"handle","description":"Handle of the stream to close.","$ref":"StreamHandle"}]},{"name":"read","description":"Read a chunk of the stream","parameters":[{"name":"handle","description":"Handle of the stream to read.","$ref":"StreamHandle"},{"name":"offset","description":"Seek to the specified offset before reading (if not specificed, proceed with offset\\nfollowing the last read). Some types of streams may only support sequential reads.","optional":true,"type":"integer"},{"name":"size","description":"Maximum number of bytes to read (left upon the agent discretion if not specified).","optional":true,"type":"integer"}],"returns":[{"name":"base64Encoded","description":"Set if the data is base64-encoded","optional":true,"type":"boolean"},{"name":"data","description":"Data that were read.","type":"string"},{"name":"eof","description":"Set if the end-of-file condition occured while reading.","type":"boolean"}]},{"name":"resolveBlob","description":"Return UUID of Blob object specified by a remote object id.","parameters":[{"name":"objectId","description":"Object id of a Blob object wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"uuid","description":"UUID of the specified Blob.","type":"string"}]}]},{"domain":"IndexedDB","experimental":true,"dependencies":["Runtime"],"types":[{"id":"DatabaseWithObjectStores","description":"Database with an array of object stores.","type":"object","properties":[{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version (type is not \'integer\', as the standard\\nrequires the version number to be \'unsigned long long\')","type":"number"},{"name":"objectStores","description":"Object stores in this database.","type":"array","items":{"$ref":"ObjectStore"}}]},{"id":"ObjectStore","description":"Object store.","type":"object","properties":[{"name":"name","description":"Object store name.","type":"string"},{"name":"keyPath","description":"Object store key path.","$ref":"KeyPath"},{"name":"autoIncrement","description":"If true, object store has auto increment flag set.","type":"boolean"},{"name":"indexes","description":"Indexes in this object store.","type":"array","items":{"$ref":"ObjectStoreIndex"}}]},{"id":"ObjectStoreIndex","description":"Object store index.","type":"object","properties":[{"name":"name","description":"Index name.","type":"string"},{"name":"keyPath","description":"Index key path.","$ref":"KeyPath"},{"name":"unique","description":"If true, index is unique.","type":"boolean"},{"name":"multiEntry","description":"If true, index allows multiple entries for a key.","type":"boolean"}]},{"id":"Key","description":"Key.","type":"object","properties":[{"name":"type","description":"Key type.","type":"string","enum":["number","string","date","array"]},{"name":"number","description":"Number value.","optional":true,"type":"number"},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"date","description":"Date value.","optional":true,"type":"number"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"$ref":"Key"}}]},{"id":"KeyRange","description":"Key range.","type":"object","properties":[{"name":"lower","description":"Lower bound.","optional":true,"$ref":"Key"},{"name":"upper","description":"Upper bound.","optional":true,"$ref":"Key"},{"name":"lowerOpen","description":"If true lower bound is open.","type":"boolean"},{"name":"upperOpen","description":"If true upper bound is open.","type":"boolean"}]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"key","description":"Key object.","$ref":"Runtime.RemoteObject"},{"name":"primaryKey","description":"Primary key object.","$ref":"Runtime.RemoteObject"},{"name":"value","description":"Value object.","$ref":"Runtime.RemoteObject"}]},{"id":"KeyPath","description":"Key path.","type":"object","properties":[{"name":"type","description":"Key path type.","type":"string","enum":["null","string","array"]},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"type":"string"}}]}],"commands":[{"name":"clearObjectStore","description":"Clears all entries from an object store.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}]},{"name":"deleteDatabase","description":"Deletes a database.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"}]},{"name":"deleteObjectStoreEntries","description":"Delete a range of entries from an object store","parameters":[{"name":"securityOrigin","type":"string"},{"name":"databaseName","type":"string"},{"name":"objectStoreName","type":"string"},{"name":"keyRange","description":"Range of entry keys to delete","$ref":"KeyRange"}]},{"name":"disable","description":"Disables events from backend."},{"name":"enable","description":"Enables events from backend."},{"name":"requestData","description":"Requests data from object store or index.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"},{"name":"indexName","description":"Index name, empty string for object store data requests.","type":"string"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"keyRange","description":"Key range.","optional":true,"$ref":"KeyRange"}],"returns":[{"name":"objectStoreDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"hasMore","description":"If true, there are more entries to fetch in the given range.","type":"boolean"}]},{"name":"getMetadata","description":"Gets metadata of an object store","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}],"returns":[{"name":"entriesCount","description":"the entries count","type":"number"},{"name":"keyGeneratorValue","description":"the current value of key generator, to become the next inserted\\nkey into the object store. Valid if objectStore.autoIncrement\\nis true.","type":"number"}]},{"name":"requestDatabase","description":"Requests database with given name in given frame.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"}],"returns":[{"name":"databaseWithObjectStores","description":"Database with an array of object stores.","$ref":"DatabaseWithObjectStores"}]},{"name":"requestDatabaseNames","description":"Requests database names for given security origin.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"}],"returns":[{"name":"databaseNames","description":"Database names for origin.","type":"array","items":{"type":"string"}}]}]},{"domain":"Input","types":[{"id":"TouchPoint","type":"object","properties":[{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"radiusX","description":"X radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"radiusY","description":"Y radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"rotationAngle","description":"Rotation angle (default: 0.0).","optional":true,"type":"number"},{"name":"force","description":"Force (default: 1.0).","optional":true,"type":"number"},{"name":"id","description":"Identifier used to track touch sources between events, must be unique within an event.","optional":true,"type":"number"}]},{"id":"GestureSourceType","experimental":true,"type":"string","enum":["default","touch","mouse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"}],"commands":[{"name":"dispatchKeyEvent","description":"Dispatches a key event to the page.","parameters":[{"name":"type","description":"Type of the key event.","type":"string","enum":["keyDown","keyUp","rawKeyDown","char"]},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"text","description":"Text as generated by processing a virtual key code with a keyboard layout. Not needed for\\nfor `keyUp` and `rawKeyDown` events (default: \\"\\")","optional":true,"type":"string"},{"name":"unmodifiedText","description":"Text that would have been generated by the keyboard if no modifiers were pressed (except for\\nshift). Useful for shortcut (accelerator) key handling (default: \\"\\").","optional":true,"type":"string"},{"name":"keyIdentifier","description":"Unique key identifier (e.g., \'U+0041\') (default: \\"\\").","optional":true,"type":"string"},{"name":"code","description":"Unique DOM defined string value for each physical key (e.g., \'KeyA\') (default: \\"\\").","optional":true,"type":"string"},{"name":"key","description":"Unique DOM defined string value describing the meaning of the key in the context of active\\nmodifiers, keyboard layout, etc (e.g., \'AltGr\') (default: \\"\\").","optional":true,"type":"string"},{"name":"windowsVirtualKeyCode","description":"Windows virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"nativeVirtualKeyCode","description":"Native virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"autoRepeat","description":"Whether the event was generated from auto repeat (default: false).","optional":true,"type":"boolean"},{"name":"isKeypad","description":"Whether the event was generated from the keypad (default: false).","optional":true,"type":"boolean"},{"name":"isSystemKey","description":"Whether the event was a system key event (default: false).","optional":true,"type":"boolean"},{"name":"location","description":"Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\\n0).","optional":true,"type":"integer"}]},{"name":"insertText","description":"This method emulates inserting text that doesn\'t come from a key press,\\nfor example an emoji keyboard or an IME.","experimental":true,"parameters":[{"name":"text","description":"The text to insert.","type":"string"}]},{"name":"dispatchMouseEvent","description":"Dispatches a mouse event to the page.","parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"button","description":"Mouse button (default: \\"none\\").","optional":true,"type":"string","enum":["none","left","middle","right","back","forward"]},{"name":"buttons","description":"A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"},{"name":"deltaX","description":"X delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"pointerType","description":"Pointer type (default: \\"mouse\\").","optional":true,"type":"string","enum":["mouse","pen"]}]},{"name":"dispatchTouchEvent","description":"Dispatches a touch event to the page.","parameters":[{"name":"type","description":"Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\\nTouchStart and TouchMove must contains at least one.","type":"string","enum":["touchStart","touchEnd","touchMove","touchCancel"]},{"name":"touchPoints","description":"Active touch points on the touch device. One event per any changed point (compared to\\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\\none by one.","type":"array","items":{"$ref":"TouchPoint"}},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"}]},{"name":"emulateTouchFromMouseEvent","description":"Emulates touch event from the mouse event parameters.","experimental":true,"parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"y","description":"Y coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"button","description":"Mouse button.","type":"string","enum":["none","left","middle","right"]},{"name":"timestamp","description":"Time at which the event occurred (default: current time).","optional":true,"$ref":"TimeSinceEpoch"},{"name":"deltaX","description":"X delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"}]},{"name":"setIgnoreInputEvents","description":"Ignores input events (useful while auditing page).","parameters":[{"name":"ignore","description":"Ignores input events processing when set to true.","type":"boolean"}]},{"name":"synthesizePinchGesture","description":"Synthesizes a pinch gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"scaleFactor","description":"Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).","type":"number"},{"name":"relativeSpeed","description":"Relative pointer speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]},{"name":"synthesizeScrollGesture","description":"Synthesizes a scroll gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"xDistance","description":"The distance to scroll along the X axis (positive to scroll left).","optional":true,"type":"number"},{"name":"yDistance","description":"The distance to scroll along the Y axis (positive to scroll up).","optional":true,"type":"number"},{"name":"xOverscroll","description":"The number of additional pixels to scroll back along the X axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"yOverscroll","description":"The number of additional pixels to scroll back along the Y axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"preventFling","description":"Prevent fling (default: true).","optional":true,"type":"boolean"},{"name":"speed","description":"Swipe speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"},{"name":"repeatCount","description":"The number of times to repeat the gesture (default: 0).","optional":true,"type":"integer"},{"name":"repeatDelayMs","description":"The number of milliseconds delay between each repeat. (default: 250).","optional":true,"type":"integer"},{"name":"interactionMarkerName","description":"The name of the interaction markers to generate, if not empty (default: \\"\\").","optional":true,"type":"string"}]},{"name":"synthesizeTapGesture","description":"Synthesizes a tap gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"duration","description":"Duration between touchdown and touchup events in ms (default: 50).","optional":true,"type":"integer"},{"name":"tapCount","description":"Number of times to perform the tap (e.g. 2 for double tap, default: 1).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]}]},{"domain":"Inspector","experimental":true,"commands":[{"name":"disable","description":"Disables inspector domain notifications."},{"name":"enable","description":"Enables inspector domain notifications."}],"events":[{"name":"detached","description":"Fired when remote debugging connection is about to be terminated. Contains detach reason.","parameters":[{"name":"reason","description":"The reason why connection has been terminated.","type":"string"}]},{"name":"targetCrashed","description":"Fired when debugging target has crashed"},{"name":"targetReloadedAfterCrash","description":"Fired when debugging target has reloaded after crash"}]},{"domain":"LayerTree","experimental":true,"dependencies":["DOM"],"types":[{"id":"LayerId","description":"Unique Layer identifier.","type":"string"},{"id":"SnapshotId","description":"Unique snapshot identifier.","type":"string"},{"id":"ScrollRect","description":"Rectangle where scrolling happens on the main thread.","type":"object","properties":[{"name":"rect","description":"Rectangle itself.","$ref":"DOM.Rect"},{"name":"type","description":"Reason for rectangle to force scrolling on the main thread","type":"string","enum":["RepaintsOnScroll","TouchEventHandler","WheelEventHandler"]}]},{"id":"StickyPositionConstraint","description":"Sticky position constraints.","type":"object","properties":[{"name":"stickyBoxRect","description":"Layout rectangle of the sticky element before being shifted","$ref":"DOM.Rect"},{"name":"containingBlockRect","description":"Layout rectangle of the containing block of the sticky element","$ref":"DOM.Rect"},{"name":"nearestLayerShiftingStickyBox","description":"The nearest sticky layer that shifts the sticky box","optional":true,"$ref":"LayerId"},{"name":"nearestLayerShiftingContainingBlock","description":"The nearest sticky layer that shifts the containing block","optional":true,"$ref":"LayerId"}]},{"id":"PictureTile","description":"Serialized fragment of layer picture along with its offset within the layer.","type":"object","properties":[{"name":"x","description":"Offset from owning layer left boundary","type":"number"},{"name":"y","description":"Offset from owning layer top boundary","type":"number"},{"name":"picture","description":"Base64-encoded snapshot data.","type":"string"}]},{"id":"Layer","description":"Information about a compositing layer.","type":"object","properties":[{"name":"layerId","description":"The unique id for this layer.","$ref":"LayerId"},{"name":"parentLayerId","description":"The id of parent (not present for root).","optional":true,"$ref":"LayerId"},{"name":"backendNodeId","description":"The backend id for the node associated with this layer.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"offsetX","description":"Offset from parent layer, X coordinate.","type":"number"},{"name":"offsetY","description":"Offset from parent layer, Y coordinate.","type":"number"},{"name":"width","description":"Layer width.","type":"number"},{"name":"height","description":"Layer height.","type":"number"},{"name":"transform","description":"Transformation matrix for layer, default is identity matrix","optional":true,"type":"array","items":{"type":"number"}},{"name":"anchorX","description":"Transform anchor point X, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorY","description":"Transform anchor point Y, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorZ","description":"Transform anchor point Z, absent if no transform specified","optional":true,"type":"number"},{"name":"paintCount","description":"Indicates how many time this layer has painted.","type":"integer"},{"name":"drawsContent","description":"Indicates whether this layer hosts any content, rather than being used for\\ntransform/scrolling purposes only.","type":"boolean"},{"name":"invisible","description":"Set if layer is not visible.","optional":true,"type":"boolean"},{"name":"scrollRects","description":"Rectangles scrolling on main thread only.","optional":true,"type":"array","items":{"$ref":"ScrollRect"}},{"name":"stickyPositionConstraint","description":"Sticky position constraint information","optional":true,"$ref":"StickyPositionConstraint"}]},{"id":"PaintProfile","description":"Array of timings, one per paint step.","type":"array","items":{"type":"number"}}],"commands":[{"name":"compositingReasons","description":"Provides the reasons why the given layer was composited.","parameters":[{"name":"layerId","description":"The id of the layer for which we want to get the reasons it was composited.","$ref":"LayerId"}],"returns":[{"name":"compositingReasons","description":"A list of strings specifying reasons for the given layer to become composited.","type":"array","items":{"type":"string"}}]},{"name":"disable","description":"Disables compositing tree inspection."},{"name":"enable","description":"Enables compositing tree inspection."},{"name":"loadSnapshot","description":"Returns the snapshot identifier.","parameters":[{"name":"tiles","description":"An array of tiles composing the snapshot.","type":"array","items":{"$ref":"PictureTile"}}],"returns":[{"name":"snapshotId","description":"The id of the snapshot.","$ref":"SnapshotId"}]},{"name":"makeSnapshot","description":"Returns the layer snapshot identifier.","parameters":[{"name":"layerId","description":"The id of the layer.","$ref":"LayerId"}],"returns":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"profileSnapshot","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"minRepeatCount","description":"The maximum number of times to replay the snapshot (1, if not specified).","optional":true,"type":"integer"},{"name":"minDuration","description":"The minimum duration (in seconds) to replay the snapshot.","optional":true,"type":"number"},{"name":"clipRect","description":"The clip rectangle to apply when replaying the snapshot.","optional":true,"$ref":"DOM.Rect"}],"returns":[{"name":"timings","description":"The array of paint profiles, one per run.","type":"array","items":{"$ref":"PaintProfile"}}]},{"name":"releaseSnapshot","description":"Releases layer snapshot captured by the back-end.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"replaySnapshot","description":"Replays the layer snapshot and returns the resulting bitmap.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"fromStep","description":"The first step to replay from (replay from the very start if not specified).","optional":true,"type":"integer"},{"name":"toStep","description":"The last step to replay to (replay till the end if not specified).","optional":true,"type":"integer"},{"name":"scale","description":"The scale to apply while replaying (defaults to 1).","optional":true,"type":"number"}],"returns":[{"name":"dataURL","description":"A data: URL for resulting image.","type":"string"}]},{"name":"snapshotCommandLog","description":"Replays the layer snapshot and returns canvas log.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}],"returns":[{"name":"commandLog","description":"The array of canvas function calls.","type":"array","items":{"type":"object"}}]}],"events":[{"name":"layerPainted","parameters":[{"name":"layerId","description":"The id of the painted layer.","$ref":"LayerId"},{"name":"clip","description":"Clip rectangle.","$ref":"DOM.Rect"}]},{"name":"layerTreeDidChange","parameters":[{"name":"layers","description":"Layer tree, absent if not in the comspositing mode.","optional":true,"type":"array","items":{"$ref":"Layer"}}]}]},{"domain":"Log","description":"Provides access to log entries.","dependencies":["Runtime","Network"],"types":[{"id":"LogEntry","description":"Log entry.","type":"object","properties":[{"name":"source","description":"Log entry source.","type":"string","enum":["xml","javascript","network","storage","appcache","rendering","security","deprecation","worker","violation","intervention","recommendation","other"]},{"name":"level","description":"Log entry severity.","type":"string","enum":["verbose","info","warning","error"]},{"name":"text","description":"Logged text.","type":"string"},{"name":"timestamp","description":"Timestamp when this entry was added.","$ref":"Runtime.Timestamp"},{"name":"url","description":"URL of the resource if known.","optional":true,"type":"string"},{"name":"lineNumber","description":"Line number in the resource.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript stack trace.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"networkRequestId","description":"Identifier of the network request associated with this entry.","optional":true,"$ref":"Network.RequestId"},{"name":"workerId","description":"Identifier of the worker associated with this entry.","optional":true,"type":"string"},{"name":"args","description":"Call arguments.","optional":true,"type":"array","items":{"$ref":"Runtime.RemoteObject"}}]},{"id":"ViolationSetting","description":"Violation configuration setting.","type":"object","properties":[{"name":"name","description":"Violation type.","type":"string","enum":["longTask","longLayout","blockedEvent","blockedParser","discouragedAPIUse","handler","recurringHandler"]},{"name":"threshold","description":"Time threshold to trigger upon.","type":"number"}]}],"commands":[{"name":"clear","description":"Clears the log."},{"name":"disable","description":"Disables log domain, prevents further log entries from being reported to the client."},{"name":"enable","description":"Enables log domain, sends the entries collected so far to the client by means of the\\n`entryAdded` notification."},{"name":"startViolationsReport","description":"start violation reporting.","parameters":[{"name":"config","description":"Configuration for violations.","type":"array","items":{"$ref":"ViolationSetting"}}]},{"name":"stopViolationsReport","description":"Stop violation reporting."}],"events":[{"name":"entryAdded","description":"Issued when new message was logged.","parameters":[{"name":"entry","description":"The entry.","$ref":"LogEntry"}]}]},{"domain":"Memory","experimental":true,"types":[{"id":"PressureLevel","description":"Memory pressure level.","type":"string","enum":["moderate","critical"]},{"id":"SamplingProfileNode","description":"Heap profile sample.","type":"object","properties":[{"name":"size","description":"Size of the sampled allocation.","type":"number"},{"name":"total","description":"Total bytes attributed to this sample.","type":"number"},{"name":"stack","description":"Execution stack at the point of allocation.","type":"array","items":{"type":"string"}}]},{"id":"SamplingProfile","description":"Array of heap profile samples.","type":"object","properties":[{"name":"samples","type":"array","items":{"$ref":"SamplingProfileNode"}},{"name":"modules","type":"array","items":{"$ref":"Module"}}]},{"id":"Module","description":"Executable module information","type":"object","properties":[{"name":"name","description":"Name of the module.","type":"string"},{"name":"uuid","description":"UUID of the module.","type":"string"},{"name":"baseAddress","description":"Base address where the module is loaded into memory. Encoded as a decimal\\nor hexadecimal (0x prefixed) string.","type":"string"},{"name":"size","description":"Size of the module in bytes.","type":"number"}]}],"commands":[{"name":"getDOMCounters","returns":[{"name":"documents","type":"integer"},{"name":"nodes","type":"integer"},{"name":"jsEventListeners","type":"integer"}]},{"name":"prepareForLeakDetection"},{"name":"forciblyPurgeJavaScriptMemory","description":"Simulate OomIntervention by purging V8 memory."},{"name":"setPressureNotificationsSuppressed","description":"Enable/disable suppressing memory pressure notifications in all processes.","parameters":[{"name":"suppressed","description":"If true, memory pressure notifications will be suppressed.","type":"boolean"}]},{"name":"simulatePressureNotification","description":"Simulate a memory pressure notification in all processes.","parameters":[{"name":"level","description":"Memory pressure level of the notification.","$ref":"PressureLevel"}]},{"name":"startSampling","description":"Start collecting native memory profile.","parameters":[{"name":"samplingInterval","description":"Average number of bytes between samples.","optional":true,"type":"integer"},{"name":"suppressRandomness","description":"Do not randomize intervals between samples.","optional":true,"type":"boolean"}]},{"name":"stopSampling","description":"Stop collecting native memory profile."},{"name":"getAllTimeSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since renderer process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getBrowserSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since browser process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getSamplingProfile","description":"Retrieve native memory allocations profile collected since last\\n`startSampling` call.","returns":[{"name":"profile","$ref":"SamplingProfile"}]}]},{"domain":"Network","description":"Network domain allows tracking network activities of the page. It exposes information about http,\\nfile, data and other requests and responses, their headers, bodies, timing, etc.","dependencies":["Debugger","Runtime","Security"],"types":[{"id":"ResourceType","description":"Resource type as it was perceived by the rendering engine.","type":"string","enum":["Document","Stylesheet","Image","Media","Font","Script","TextTrack","XHR","Fetch","EventSource","WebSocket","Manifest","SignedExchange","Ping","CSPViolationReport","Other"]},{"id":"LoaderId","description":"Unique loader identifier.","type":"string"},{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"InterceptionId","description":"Unique intercepted request identifier.","type":"string"},{"id":"ErrorReason","description":"Network level fetch failure reason.","type":"string","enum":["Failed","Aborted","TimedOut","AccessDenied","ConnectionClosed","ConnectionReset","ConnectionRefused","ConnectionAborted","ConnectionFailed","NameNotResolved","InternetDisconnected","AddressUnreachable","BlockedByClient","BlockedByResponse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"MonotonicTime","description":"Monotonically increasing time in seconds since an arbitrary point in the past.","type":"number"},{"id":"Headers","description":"Request / response headers as keys / values of JSON object.","type":"object"},{"id":"ConnectionType","description":"The underlying connection technology that the browser is supposedly using.","type":"string","enum":["none","cellular2g","cellular3g","cellular4g","bluetooth","ethernet","wifi","wimax","other"]},{"id":"CookieSameSite","description":"Represents the cookie\'s \'SameSite\' status:\\nhttps://tools.ietf.org/html/draft-west-first-party-cookies","type":"string","enum":["Strict","Lax","Extended","None"]},{"id":"ResourceTiming","description":"Timing information for the request.","type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime.","type":"number"},{"name":"proxyStart","description":"Started resolving proxy.","type":"number"},{"name":"proxyEnd","description":"Finished resolving proxy.","type":"number"},{"name":"dnsStart","description":"Started DNS address resolve.","type":"number"},{"name":"dnsEnd","description":"Finished DNS address resolve.","type":"number"},{"name":"connectStart","description":"Started connecting to the remote host.","type":"number"},{"name":"connectEnd","description":"Connected to the remote host.","type":"number"},{"name":"sslStart","description":"Started SSL handshake.","type":"number"},{"name":"sslEnd","description":"Finished SSL handshake.","type":"number"},{"name":"workerStart","description":"Started running ServiceWorker.","experimental":true,"type":"number"},{"name":"workerReady","description":"Finished Starting ServiceWorker.","experimental":true,"type":"number"},{"name":"sendStart","description":"Started sending request.","type":"number"},{"name":"sendEnd","description":"Finished sending request.","type":"number"},{"name":"pushStart","description":"Time the server started pushing request.","experimental":true,"type":"number"},{"name":"pushEnd","description":"Time the server finished pushing request.","experimental":true,"type":"number"},{"name":"receiveHeadersEnd","description":"Finished receiving response headers.","type":"number"}]},{"id":"ResourcePriority","description":"Loading priority of a resource request.","type":"string","enum":["VeryLow","Low","Medium","High","VeryHigh"]},{"id":"Request","description":"HTTP request data.","type":"object","properties":[{"name":"url","description":"Request URL (without fragment).","type":"string"},{"name":"urlFragment","description":"Fragment of the requested URL starting with hash, if present.","optional":true,"type":"string"},{"name":"method","description":"HTTP request method.","type":"string"},{"name":"headers","description":"HTTP request headers.","$ref":"Headers"},{"name":"postData","description":"HTTP POST request data.","optional":true,"type":"string"},{"name":"hasPostData","description":"True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.","optional":true,"type":"boolean"},{"name":"mixedContentType","description":"The mixed content type of the request.","optional":true,"$ref":"Security.MixedContentType"},{"name":"initialPriority","description":"Priority of the resource request at the time request is sent.","$ref":"ResourcePriority"},{"name":"referrerPolicy","description":"The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/","type":"string","enum":["unsafe-url","no-referrer-when-downgrade","no-referrer","origin","origin-when-cross-origin","same-origin","strict-origin","strict-origin-when-cross-origin"]},{"name":"isLinkPreload","description":"Whether is loaded via link preload.","optional":true,"type":"boolean"}]},{"id":"SignedCertificateTimestamp","description":"Details of a signed certificate timestamp (SCT).","type":"object","properties":[{"name":"status","description":"Validation status.","type":"string"},{"name":"origin","description":"Origin.","type":"string"},{"name":"logDescription","description":"Log name / description.","type":"string"},{"name":"logId","description":"Log ID.","type":"string"},{"name":"timestamp","description":"Issuance date.","$ref":"TimeSinceEpoch"},{"name":"hashAlgorithm","description":"Hash algorithm.","type":"string"},{"name":"signatureAlgorithm","description":"Signature algorithm.","type":"string"},{"name":"signatureData","description":"Signature data.","type":"string"}]},{"id":"SecurityDetails","description":"Security details about a request.","type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificateId","description":"Certificate ID value.","$ref":"Security.CertificateId"},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"sanList","description":"Subject Alternative Name (SAN) DNS names and IP addresses.","type":"array","items":{"type":"string"}},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"TimeSinceEpoch"},{"name":"signedCertificateTimestampList","description":"List of signed certificate timestamps (SCTs).","type":"array","items":{"$ref":"SignedCertificateTimestamp"}},{"name":"certificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy","$ref":"CertificateTransparencyCompliance"}]},{"id":"CertificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy.","type":"string","enum":["unknown","not-compliant","compliant"]},{"id":"BlockedReason","description":"The reason why request was blocked.","type":"string","enum":["other","csp","mixed-content","origin","inspector","subresource-filter","content-type","collapsed-by-client"]},{"id":"Response","description":"HTTP response data.","type":"object","properties":[{"name":"url","description":"Response URL. This URL can be different from CachedResource.url in case of redirect.","type":"string"},{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"requestHeaders","description":"Refined HTTP request headers that were actually transmitted over the network.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"},{"name":"connectionReused","description":"Specifies whether physical connection was actually reused for this request.","type":"boolean"},{"name":"connectionId","description":"Physical connection id that was actually used for this request.","type":"number"},{"name":"remoteIPAddress","description":"Remote IP address.","optional":true,"type":"string"},{"name":"remotePort","description":"Remote port.","optional":true,"type":"integer"},{"name":"fromDiskCache","description":"Specifies that the request was served from the disk cache.","optional":true,"type":"boolean"},{"name":"fromServiceWorker","description":"Specifies that the request was served from the ServiceWorker.","optional":true,"type":"boolean"},{"name":"fromPrefetchCache","description":"Specifies that the request was served from the prefetch cache.","optional":true,"type":"boolean"},{"name":"encodedDataLength","description":"Total number of bytes received for this request so far.","type":"number"},{"name":"timing","description":"Timing information for the given request.","optional":true,"$ref":"ResourceTiming"},{"name":"protocol","description":"Protocol used to fetch this request.","optional":true,"type":"string"},{"name":"securityState","description":"Security state of the request resource.","$ref":"Security.SecurityState"},{"name":"securityDetails","description":"Security details for the request.","optional":true,"$ref":"SecurityDetails"}]},{"id":"WebSocketRequest","description":"WebSocket request data.","type":"object","properties":[{"name":"headers","description":"HTTP request headers.","$ref":"Headers"}]},{"id":"WebSocketResponse","description":"WebSocket response data.","type":"object","properties":[{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"requestHeaders","description":"HTTP request headers.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"}]},{"id":"WebSocketFrame","description":"WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.","type":"object","properties":[{"name":"opcode","description":"WebSocket message opcode.","type":"number"},{"name":"mask","description":"WebSocket message mask.","type":"boolean"},{"name":"payloadData","description":"WebSocket message payload data.\\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\\nIf the opcode isn\'t 1, then payloadData is a base64 encoded string representing binary data.","type":"string"}]},{"id":"CachedResource","description":"Information about the cached resource.","type":"object","properties":[{"name":"url","description":"Resource URL. This is the url of the original network request.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"ResourceType"},{"name":"response","description":"Cached response data.","optional":true,"$ref":"Response"},{"name":"bodySize","description":"Cached response body size.","type":"number"}]},{"id":"Initiator","description":"Information about the request initiator.","type":"object","properties":[{"name":"type","description":"Type of this initiator.","type":"string","enum":["parser","script","preload","SignedExchange","other"]},{"name":"stack","description":"Initiator JavaScript stack trace, set for Script only.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"url","description":"Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.","optional":true,"type":"string"},{"name":"lineNumber","description":"Initiator line number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"}]},{"id":"Cookie","description":"Cookie object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"domain","description":"Cookie domain.","type":"string"},{"name":"path","description":"Cookie path.","type":"string"},{"name":"expires","description":"Cookie expiration date as the number of seconds since the UNIX epoch.","type":"number"},{"name":"size","description":"Cookie size.","type":"integer"},{"name":"httpOnly","description":"True if cookie is http-only.","type":"boolean"},{"name":"secure","description":"True if cookie is secure.","type":"boolean"},{"name":"session","description":"True in case of session cookie.","type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"}]},{"id":"CookieParam","description":"Cookie parameter object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain and path values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]},{"id":"InterceptionStage","description":"Stages of the interception to begin intercepting. Request will intercept before the request is\\nsent. Response will intercept after the response is received.","experimental":true,"type":"string","enum":["Request","HeadersReceived"]},{"id":"RequestPattern","description":"Request pattern for interception.","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (\'*\' -> zero or more, \'?\' -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to \\"*\\".","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"ResourceType"},{"name":"interceptionStage","description":"Stage at wich to begin intercepting requests. Default is Request.","optional":true,"$ref":"InterceptionStage"}]},{"id":"SignedExchangeSignature","description":"Information about a signed exchange signature.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1","experimental":true,"type":"object","properties":[{"name":"label","description":"Signed exchange signature label.","type":"string"},{"name":"signature","description":"The hex string of signed exchange signature.","type":"string"},{"name":"integrity","description":"Signed exchange signature integrity.","type":"string"},{"name":"certUrl","description":"Signed exchange signature cert Url.","optional":true,"type":"string"},{"name":"certSha256","description":"The hex string of signed exchange signature cert sha256.","optional":true,"type":"string"},{"name":"validityUrl","description":"Signed exchange signature validity Url.","type":"string"},{"name":"date","description":"Signed exchange signature date.","type":"integer"},{"name":"expires","description":"Signed exchange signature expires.","type":"integer"},{"name":"certificates","description":"The encoded certificates.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"SignedExchangeHeader","description":"Information about a signed exchange header.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation","experimental":true,"type":"object","properties":[{"name":"requestUrl","description":"Signed exchange request URL.","type":"string"},{"name":"responseCode","description":"Signed exchange response code.","type":"integer"},{"name":"responseHeaders","description":"Signed exchange response headers.","$ref":"Headers"},{"name":"signatures","description":"Signed exchange response signature.","type":"array","items":{"$ref":"SignedExchangeSignature"}},{"name":"headerIntegrity","description":"Signed exchange header integrity hash in the form of \\"sha256-<base64-hash-value>\\".","type":"string"}]},{"id":"SignedExchangeErrorField","description":"Field type for a signed exchange related error.","experimental":true,"type":"string","enum":["signatureSig","signatureIntegrity","signatureCertUrl","signatureCertSha256","signatureValidityUrl","signatureTimestamps"]},{"id":"SignedExchangeError","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"signatureIndex","description":"The index of the signature which caused the error.","optional":true,"type":"integer"},{"name":"errorField","description":"The field which caused the error.","optional":true,"$ref":"SignedExchangeErrorField"}]},{"id":"SignedExchangeInfo","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"outerResponse","description":"The outer response of signed HTTP exchange which was received from network.","$ref":"Response"},{"name":"header","description":"Information about the signed exchange header.","optional":true,"$ref":"SignedExchangeHeader"},{"name":"securityDetails","description":"Security details for the signed exchange header.","optional":true,"$ref":"SecurityDetails"},{"name":"errors","description":"Errors occurred while handling the signed exchagne.","optional":true,"type":"array","items":{"$ref":"SignedExchangeError"}}]}],"commands":[{"name":"canClearBrowserCache","description":"Tells whether clearing browser cache is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cache can be cleared.","type":"boolean"}]},{"name":"canClearBrowserCookies","description":"Tells whether clearing browser cookies is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cookies can be cleared.","type":"boolean"}]},{"name":"canEmulateNetworkConditions","description":"Tells whether emulation of network conditions is supported.","deprecated":true,"returns":[{"name":"result","description":"True if emulation of network conditions is supported.","type":"boolean"}]},{"name":"clearBrowserCache","description":"Clears browser cache."},{"name":"clearBrowserCookies","description":"Clears browser cookies."},{"name":"continueInterceptedRequest","description":"Response to Network.requestIntercepted which either modifies the request to continue with any\\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\\nevent will be sent with the same InterceptionId.\\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"},{"name":"errorReason","description":"If set this causes the request to fail with the given reason. Passing `Aborted` for requests\\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\\nto an authChallenge.","optional":true,"$ref":"ErrorReason"},{"name":"rawResponse","description":"If set the requests completes using with the provided base64 encoded raw response, including\\nHTTP status line and headers etc... Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"url","description":"If set the request url will be modified in a way that\'s not observable by page. Must not be\\nset in response to an authChallenge.","optional":true,"type":"string"},{"name":"method","description":"If set this allows the request method to be overridden. Must not be set in response to an\\nauthChallenge.","optional":true,"type":"string"},{"name":"postData","description":"If set this allows postData to be set. Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"headers","description":"If set this allows the request headers to be changed. Must not be set in response to an\\nauthChallenge.","optional":true,"$ref":"Headers"},{"name":"authChallengeResponse","description":"Response to a requestIntercepted with an authChallenge. Must not be set otherwise.","optional":true,"$ref":"AuthChallengeResponse"}]},{"name":"deleteCookies","description":"Deletes browser cookies with matching name and url or domain/path pair.","parameters":[{"name":"name","description":"Name of the cookies to remove.","type":"string"},{"name":"url","description":"If specified, deletes all the cookies with the given name where domain and path match\\nprovided URL.","optional":true,"type":"string"},{"name":"domain","description":"If specified, deletes only cookies with the exact domain.","optional":true,"type":"string"},{"name":"path","description":"If specified, deletes only cookies with the exact path.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables network tracking, prevents network events from being sent to the client."},{"name":"emulateNetworkConditions","description":"Activates emulation of network conditions.","parameters":[{"name":"offline","description":"True to emulate internet disconnection.","type":"boolean"},{"name":"latency","description":"Minimum latency from request sent to response headers received (ms).","type":"number"},{"name":"downloadThroughput","description":"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.","type":"number"},{"name":"uploadThroughput","description":"Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.","type":"number"},{"name":"connectionType","description":"Connection type if known.","optional":true,"$ref":"ConnectionType"}]},{"name":"enable","description":"Enables network tracking, network events will now be delivered to the client.","parameters":[{"name":"maxTotalBufferSize","description":"Buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxResourceBufferSize","description":"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxPostDataSize","description":"Longest post body size (in bytes) that would be included in requestWillBeSent notification","optional":true,"type":"integer"}]},{"name":"getAllCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getCertificate","description":"Returns the DER-encoded certificate.","experimental":true,"parameters":[{"name":"origin","description":"Origin to get certificate for.","type":"string"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies for the current URL. Depending on the backend support, will return\\ndetailed cookie information in the `cookies` field.","parameters":[{"name":"urls","description":"The list of URLs for which applicable cookies will be fetched","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getResponseBody","description":"Returns content served for the given request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"getRequestPostData","description":"Returns post data sent with the request. Returns an error when no data was sent with the request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"postData","description":"Request body string, omitting files from multipart requests","type":"string"}]},{"name":"getResponseBodyForInterception","description":"Returns content served for the given currently intercepted request.","experimental":true,"parameters":[{"name":"interceptionId","description":"Identifier for the intercepted request to get body for.","$ref":"InterceptionId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyForInterceptionAsStream","description":"Returns a handle to the stream representing the response body. Note that after this command,\\nthe intercepted request can\'t be continued as is -- you either need to cancel it or to provide\\nthe response body. The stream only supports sequential read, IO.read will fail if the position\\nis specified.","experimental":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]},{"name":"replayXHR","description":"This method sends a new XMLHttpRequest which is identical to the original one. The following\\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\\nattribute, user, password.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of XHR to replay.","$ref":"RequestId"}]},{"name":"searchInResponseBody","description":"Searches for given string in response content.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of the network response to search.","$ref":"RequestId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setBlockedURLs","description":"Blocks URLs from loading.","experimental":true,"parameters":[{"name":"urls","description":"URL patterns to block. Wildcards (\'*\') are allowed.","type":"array","items":{"type":"string"}}]},{"name":"setBypassServiceWorker","description":"Toggles ignoring of service worker for each request.","experimental":true,"parameters":[{"name":"bypass","description":"Bypass service worker and load from network.","type":"boolean"}]},{"name":"setCacheDisabled","description":"Toggles ignoring cache for each request. If `true`, cache will not be used.","parameters":[{"name":"cacheDisabled","description":"Cache disabled state.","type":"boolean"}]},{"name":"setCookie","description":"Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.","parameters":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain and path values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"}],"returns":[{"name":"success","description":"True if successfully set cookie.","type":"boolean"}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"CookieParam"}}]},{"name":"setDataSizeLimitsForTest","description":"For testing.","experimental":true,"parameters":[{"name":"maxTotalSize","description":"Maximum total buffer size.","type":"integer"},{"name":"maxResourceSize","description":"Maximum per-resource size.","type":"integer"}]},{"name":"setExtraHTTPHeaders","description":"Specifies whether to always send extra HTTP headers with the requests from this page.","parameters":[{"name":"headers","description":"Map with extra HTTP headers.","$ref":"Headers"}]},{"name":"setRequestInterception","description":"Sets the requests to intercept that match the provided patterns and optionally resource types.\\nDeprecated, please use Fetch.enable instead.","experimental":true,"deprecated":true,"parameters":[{"name":"patterns","description":"Requests matching any of these patterns will be forwarded and wait for the corresponding\\ncontinueInterceptedRequest call.","type":"array","items":{"$ref":"RequestPattern"}}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","redirect":"Emulation","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"}]}],"events":[{"name":"dataReceived","description":"Fired when data chunk was received over the network.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"dataLength","description":"Data chunk length.","type":"integer"},{"name":"encodedDataLength","description":"Actual bytes received (might be less than dataLength for compressed encodings).","type":"integer"}]},{"name":"eventSourceMessageReceived","description":"Fired when EventSource message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"eventName","description":"Message type.","type":"string"},{"name":"eventId","description":"Message identifier.","type":"string"},{"name":"data","description":"Message content.","type":"string"}]},{"name":"loadingFailed","description":"Fired when HTTP request has failed to load.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"errorText","description":"User friendly error message.","type":"string"},{"name":"canceled","description":"True if loading was canceled.","optional":true,"type":"boolean"},{"name":"blockedReason","description":"The reason why loading was blocked, if any.","optional":true,"$ref":"BlockedReason"}]},{"name":"loadingFinished","description":"Fired when HTTP request has finished loading.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"encodedDataLength","description":"Total number of bytes received for this request.","type":"number"},{"name":"shouldReportCorbBlocking","description":"Set when 1) response was blocked by Cross-Origin Read Blocking and also\\n2) this needs to be reported to the DevTools console.","optional":true,"type":"boolean"}]},{"name":"requestIntercepted","description":"Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\\nmocked.\\nDeprecated, use Fetch.requestPaused instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","description":"Each request the page makes will have a unique id, however if any redirects are encountered\\nwhile processing that fetch, they will be reported with the same id as the original fetch.\\nLikewise if HTTP authentication is needed then the same fetch id will be used.","$ref":"InterceptionId"},{"name":"request","$ref":"Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"ResourceType"},{"name":"isNavigationRequest","description":"Whether this is a navigation request, which can abort the navigation completely.","type":"boolean"},{"name":"isDownload","description":"Set if the request is a navigation that will result in a download.\\nOnly present after response is received from the server (i.e. HeadersReceived stage).","optional":true,"type":"boolean"},{"name":"redirectUrl","description":"Redirect location, only sent if a redirect was intercepted.","optional":true,"type":"string"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered. If this is set then\\ncontinueInterceptedRequest must contain an authChallengeResponse.","optional":true,"$ref":"AuthChallenge"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage or if redirect occurred while intercepting\\nrequest.","optional":true,"$ref":"ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage or if redirect occurred while intercepting\\nrequest or auth retry occurred.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage or if redirect occurred while\\nintercepting request or auth retry occurred.","optional":true,"$ref":"Headers"},{"name":"requestId","description":"If the intercepted request had a corresponding requestWillBeSent event fired for it, then\\nthis requestId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"requestServedFromCache","description":"Fired if request ended up loading from cache.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"}]},{"name":"requestWillBeSent","description":"Fired when page is about to send HTTP request.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"documentURL","description":"URL of the document this request is loaded for.","type":"string"},{"name":"request","description":"Request data.","$ref":"Request"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"Timestamp.","$ref":"TimeSinceEpoch"},{"name":"initiator","description":"Request initiator.","$ref":"Initiator"},{"name":"redirectResponse","description":"Redirect response data.","optional":true,"$ref":"Response"},{"name":"type","description":"Type of this resource.","optional":true,"$ref":"ResourceType"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"},{"name":"hasUserGesture","description":"Whether the request is initiated by a user gesture. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"resourceChangedPriority","description":"Fired when resource loading priority is changed","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"newPriority","description":"New priority","$ref":"ResourcePriority"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"signedExchangeReceived","description":"Fired when a signed exchange was received over the network","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"info","description":"Information about the signed exchange response.","$ref":"SignedExchangeInfo"}]},{"name":"responseReceived","description":"Fired when HTTP response is available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"response","description":"Response data.","$ref":"Response"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"}]},{"name":"webSocketClosed","description":"Fired when WebSocket is closed.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webSocketCreated","description":"Fired upon WebSocket creation.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"url","description":"WebSocket request URL.","type":"string"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webSocketFrameError","description":"Fired when WebSocket message error occurs.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"errorMessage","description":"WebSocket error message.","type":"string"}]},{"name":"webSocketFrameReceived","description":"Fired when WebSocket message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketFrameSent","description":"Fired when WebSocket message is sent.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketHandshakeResponseReceived","description":"Fired when WebSocket handshake response becomes available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketResponse"}]},{"name":"webSocketWillSendHandshakeRequest","description":"Fired when WebSocket is about to initiate handshake.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"UTC Timestamp.","$ref":"TimeSinceEpoch"},{"name":"request","description":"WebSocket request data.","$ref":"WebSocketRequest"}]}]},{"domain":"Overlay","description":"This domain provides various functionality related to drawing atop the inspected page.","experimental":true,"dependencies":["DOM","Page","Runtime"],"types":[{"id":"HighlightConfig","description":"Configuration data for the highlighting of page elements.","type":"object","properties":[{"name":"showInfo","description":"Whether the node info tooltip should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showStyles","description":"Whether the node styles in the tooltip (default: false).","optional":true,"type":"boolean"},{"name":"showRulers","description":"Whether the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showExtensionLines","description":"Whether the extension lines from node to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"paddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"borderColor","description":"The border highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"marginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"eventTargetColor","description":"The event target element highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeColor","description":"The shape outside fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeMarginColor","description":"The shape margin fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cssGridColor","description":"The grid layout color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"InspectMode","type":"string","enum":["searchForNode","searchForUAShadowDOM","captureAreaScreenshot","showDistances","none"]}],"commands":[{"name":"disable","description":"Disables domain notifications."},{"name":"enable","description":"Enables domain notifications."},{"name":"getHighlightObjectForTest","description":"For testing.","parameters":[{"name":"nodeId","description":"Id of the node to get highlight object for.","$ref":"DOM.NodeId"},{"name":"includeDistance","description":"Whether to include distance info.","optional":true,"type":"boolean"},{"name":"includeStyle","description":"Whether to include style info.","optional":true,"type":"boolean"}],"returns":[{"name":"highlight","description":"Highlight data for the node.","type":"object"}]},{"name":"hideHighlight","description":"Hides any highlight."},{"name":"highlightFrame","description":"Highlights owner element of the frame with given id.","parameters":[{"name":"frameId","description":"Identifier of the frame to highlight.","$ref":"Page.FrameId"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"contentOutlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightNode","description":"Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\\nobjectId must be specified.","parameters":[{"name":"highlightConfig","description":"A descriptor for the highlight appearance.","$ref":"HighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"selector","description":"Selectors to highlight relevant nodes.","optional":true,"type":"string"}]},{"name":"highlightQuad","description":"Highlights given quad. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"quad","description":"Quad to highlight","$ref":"DOM.Quad"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightRect","description":"Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"x","description":"X coordinate","type":"integer"},{"name":"y","description":"Y coordinate","type":"integer"},{"name":"width","description":"Rectangle width","type":"integer"},{"name":"height","description":"Rectangle height","type":"integer"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setInspectMode","description":"Enters the \'inspect\' mode. In this mode, elements that user is hovering over are highlighted.\\nBackend then generates \'inspectNodeRequested\' event upon element selection.","parameters":[{"name":"mode","description":"Set an inspection mode.","$ref":"InspectMode"},{"name":"highlightConfig","description":"A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\\n== false`.","optional":true,"$ref":"HighlightConfig"}]},{"name":"setShowAdHighlights","description":"Highlights owner element of all frames detected to be ads.","parameters":[{"name":"show","description":"True for showing ad highlights","type":"boolean"}]},{"name":"setPausedInDebuggerMessage","parameters":[{"name":"message","description":"The message to display, also triggers resume and step over controls.","optional":true,"type":"string"}]},{"name":"setShowDebugBorders","description":"Requests that backend shows debug borders on layers","parameters":[{"name":"show","description":"True for showing debug borders","type":"boolean"}]},{"name":"setShowFPSCounter","description":"Requests that backend shows the FPS counter","parameters":[{"name":"show","description":"True for showing the FPS counter","type":"boolean"}]},{"name":"setShowPaintRects","description":"Requests that backend shows paint rectangles","parameters":[{"name":"result","description":"True for showing paint rectangles","type":"boolean"}]},{"name":"setShowLayoutShiftRegions","description":"Requests that backend shows layout shift regions","parameters":[{"name":"result","description":"True for showing layout shift regions","type":"boolean"}]},{"name":"setShowScrollBottleneckRects","description":"Requests that backend shows scroll bottleneck rects","parameters":[{"name":"show","description":"True for showing scroll bottleneck rects","type":"boolean"}]},{"name":"setShowHitTestBorders","description":"Requests that backend shows hit-test borders on layers","parameters":[{"name":"show","description":"True for showing hit-test borders","type":"boolean"}]},{"name":"setShowViewportSizeOnResize","description":"Paints viewport size upon main frame resize.","parameters":[{"name":"show","description":"Whether to paint size or not.","type":"boolean"}]}],"events":[{"name":"inspectNodeRequested","description":"Fired when the node should be inspected. This happens after call to `setInspectMode` or when\\nuser manually inspects an element.","parameters":[{"name":"backendNodeId","description":"Id of the node to inspect.","$ref":"DOM.BackendNodeId"}]},{"name":"nodeHighlightRequested","description":"Fired when the node should be highlighted. This happens after call to `setInspectMode`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}]},{"name":"screenshotRequested","description":"Fired when user asks to capture screenshot of some area on the page.","parameters":[{"name":"viewport","description":"Viewport to capture, in device independent pixels (dip).","$ref":"Page.Viewport"}]},{"name":"inspectModeCanceled","description":"Fired when user cancels the inspect mode."}]},{"domain":"Page","description":"Actions and events related to the inspected page belong to the page domain.","dependencies":["Debugger","DOM","IO","Network","Runtime"],"types":[{"id":"FrameId","description":"Unique frame identifier.","type":"string"},{"id":"Frame","description":"Information about the Frame on the page.","type":"object","properties":[{"name":"id","description":"Frame unique identifier.","type":"string"},{"name":"parentId","description":"Parent frame identifier.","optional":true,"type":"string"},{"name":"loaderId","description":"Identifier of the loader associated with this frame.","$ref":"Network.LoaderId"},{"name":"name","description":"Frame\'s name as specified in the tag.","optional":true,"type":"string"},{"name":"url","description":"Frame document\'s URL without fragment.","type":"string"},{"name":"urlFragment","description":"Frame document\'s URL fragment including the \'#\'.","experimental":true,"optional":true,"type":"string"},{"name":"securityOrigin","description":"Frame document\'s security origin.","type":"string"},{"name":"mimeType","description":"Frame document\'s mimeType as determined by the browser.","type":"string"},{"name":"unreachableUrl","description":"If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.","experimental":true,"optional":true,"type":"string"}]},{"id":"FrameResource","description":"Information about the Resource on the page.","experimental":true,"type":"object","properties":[{"name":"url","description":"Resource URL.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"Network.ResourceType"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"lastModified","description":"last-modified timestamp as reported by server.","optional":true,"$ref":"Network.TimeSinceEpoch"},{"name":"contentSize","description":"Resource content size.","optional":true,"type":"number"},{"name":"failed","description":"True if the resource failed to load.","optional":true,"type":"boolean"},{"name":"canceled","description":"True if the resource was canceled during loading.","optional":true,"type":"boolean"}]},{"id":"FrameResourceTree","description":"Information about the Frame hierarchy along with their cached resources.","experimental":true,"type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameResourceTree"}},{"name":"resources","description":"Information about frame resources.","type":"array","items":{"$ref":"FrameResource"}}]},{"id":"FrameTree","description":"Information about the Frame hierarchy.","type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameTree"}}]},{"id":"ScriptIdentifier","description":"Unique script identifier.","type":"string"},{"id":"TransitionType","description":"Transition type.","type":"string","enum":["link","typed","address_bar","auto_bookmark","auto_subframe","manual_subframe","generated","auto_toplevel","form_submit","reload","keyword","keyword_generated","other"]},{"id":"NavigationEntry","description":"Navigation history entry.","type":"object","properties":[{"name":"id","description":"Unique id of the navigation history entry.","type":"integer"},{"name":"url","description":"URL of the navigation history entry.","type":"string"},{"name":"userTypedURL","description":"URL that the user typed in the url bar.","type":"string"},{"name":"title","description":"Title of the navigation history entry.","type":"string"},{"name":"transitionType","description":"Transition type.","$ref":"TransitionType"}]},{"id":"ScreencastFrameMetadata","description":"Screencast frame metadata.","experimental":true,"type":"object","properties":[{"name":"offsetTop","description":"Top offset in DIP.","type":"number"},{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"},{"name":"deviceWidth","description":"Device screen width in DIP.","type":"number"},{"name":"deviceHeight","description":"Device screen height in DIP.","type":"number"},{"name":"scrollOffsetX","description":"Position of horizontal scroll in CSS pixels.","type":"number"},{"name":"scrollOffsetY","description":"Position of vertical scroll in CSS pixels.","type":"number"},{"name":"timestamp","description":"Frame swap timestamp.","optional":true,"$ref":"Network.TimeSinceEpoch"}]},{"id":"DialogType","description":"Javascript dialog type.","type":"string","enum":["alert","confirm","prompt","beforeunload"]},{"id":"AppManifestError","description":"Error while paring app manifest.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"critical","description":"If criticial, this is a non-recoverable parse error.","type":"integer"},{"name":"line","description":"Error line.","type":"integer"},{"name":"column","description":"Error column.","type":"integer"}]},{"id":"LayoutViewport","description":"Layout viewport position and dimensions.","type":"object","properties":[{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"integer"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"integer"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"integer"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"integer"}]},{"id":"VisualViewport","description":"Visual viewport position, dimensions, and scale.","type":"object","properties":[{"name":"offsetX","description":"Horizontal offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"offsetY","description":"Vertical offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"number"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"number"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"scale","description":"Scale relative to the ideal viewport (size at width=device-width).","type":"number"},{"name":"zoom","description":"Page zoom factor (CSS to device independent pixels ratio).","optional":true,"type":"number"}]},{"id":"Viewport","description":"Viewport for capturing screenshot.","type":"object","properties":[{"name":"x","description":"X offset in device independent pixels (dip).","type":"number"},{"name":"y","description":"Y offset in device independent pixels (dip).","type":"number"},{"name":"width","description":"Rectangle width in device independent pixels (dip).","type":"number"},{"name":"height","description":"Rectangle height in device independent pixels (dip).","type":"number"},{"name":"scale","description":"Page scale factor.","type":"number"}]},{"id":"FontFamilies","description":"Generic font families collection.","experimental":true,"type":"object","properties":[{"name":"standard","description":"The standard font-family.","optional":true,"type":"string"},{"name":"fixed","description":"The fixed font-family.","optional":true,"type":"string"},{"name":"serif","description":"The serif font-family.","optional":true,"type":"string"},{"name":"sansSerif","description":"The sansSerif font-family.","optional":true,"type":"string"},{"name":"cursive","description":"The cursive font-family.","optional":true,"type":"string"},{"name":"fantasy","description":"The fantasy font-family.","optional":true,"type":"string"},{"name":"pictograph","description":"The pictograph font-family.","optional":true,"type":"string"}]},{"id":"FontSizes","description":"Default font sizes.","experimental":true,"type":"object","properties":[{"name":"standard","description":"Default standard font size.","optional":true,"type":"integer"},{"name":"fixed","description":"Default fixed font size.","optional":true,"type":"integer"}]},{"id":"ClientNavigationReason","experimental":true,"type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload"]}],"commands":[{"name":"addScriptToEvaluateOnLoad","description":"Deprecated, please use addScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"scriptSource","type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"addScriptToEvaluateOnNewDocument","description":"Evaluates given script in every frame upon creation (before loading frame\'s scripts).","parameters":[{"name":"source","type":"string"},{"name":"worldName","description":"If specified, creates an isolated world with the given name and evaluates given script in it.\\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\\nevent is emitted.","experimental":true,"optional":true,"type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"bringToFront","description":"Brings page to front (activates tab)."},{"name":"captureScreenshot","description":"Capture page screenshot.","parameters":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"},{"name":"clip","description":"Capture the screenshot of a given region only.","optional":true,"$ref":"Viewport"},{"name":"fromSurface","description":"Capture the screenshot from the surface, rather than the view. Defaults to true.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"data","description":"Base64-encoded image data.","type":"string"}]},{"name":"captureSnapshot","description":"Returns a snapshot of the page as a string. For MHTML format, the serialization includes\\niframes, shadow DOM, external resources, and element-inline styles.","experimental":true,"parameters":[{"name":"format","description":"Format (defaults to mhtml).","optional":true,"type":"string","enum":["mhtml"]}],"returns":[{"name":"data","description":"Serialized page data.","type":"string"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overriden device metrics.","experimental":true,"deprecated":true,"redirect":"Emulation"},{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation"},{"name":"clearGeolocationOverride","description":"Clears the overriden Geolocation Position and Error.","deprecated":true,"redirect":"Emulation"},{"name":"createIsolatedWorld","description":"Creates an isolated world for the given frame.","parameters":[{"name":"frameId","description":"Id of the frame in which the isolated world should be created.","$ref":"FrameId"},{"name":"worldName","description":"An optional name which is reported in the Execution Context.","optional":true,"type":"string"},{"name":"grantUniveralAccess","description":"Whether or not universal access should be granted to the isolated world. This is a powerful\\noption, use with caution.","optional":true,"type":"boolean"}],"returns":[{"name":"executionContextId","description":"Execution context of the isolated world.","$ref":"Runtime.ExecutionContextId"}]},{"name":"deleteCookie","description":"Deletes browser cookie with given name, domain and path.","experimental":true,"deprecated":true,"redirect":"Network","parameters":[{"name":"cookieName","description":"Name of the cookie to remove.","type":"string"},{"name":"url","description":"URL to match cooke domain and path.","type":"string"}]},{"name":"disable","description":"Disables page domain notifications."},{"name":"enable","description":"Enables page domain notifications."},{"name":"getAppManifest","returns":[{"name":"url","description":"Manifest location.","type":"string"},{"name":"errors","type":"array","items":{"$ref":"AppManifestError"}},{"name":"data","description":"Manifest content.","optional":true,"type":"string"}]},{"name":"getInstallabilityErrors","experimental":true,"returns":[{"name":"errors","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.","experimental":true,"deprecated":true,"redirect":"Network","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"getFrameTree","description":"Returns present frame tree structure.","returns":[{"name":"frameTree","description":"Present frame tree structure.","$ref":"FrameTree"}]},{"name":"getLayoutMetrics","description":"Returns metrics relating to the layouting of the page, such as viewport bounds/scale.","returns":[{"name":"layoutViewport","description":"Metrics relating to the layout viewport.","$ref":"LayoutViewport"},{"name":"visualViewport","description":"Metrics relating to the visual viewport.","$ref":"VisualViewport"},{"name":"contentSize","description":"Size of scrollable area.","$ref":"DOM.Rect"}]},{"name":"getNavigationHistory","description":"Returns navigation history for the current page.","returns":[{"name":"currentIndex","description":"Index of the current navigation history entry.","type":"integer"},{"name":"entries","description":"Array of navigation history entries.","type":"array","items":{"$ref":"NavigationEntry"}}]},{"name":"resetNavigationHistory","description":"Resets navigation history for the current page."},{"name":"getResourceContent","description":"Returns content of the given resource.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get resource for.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"}],"returns":[{"name":"content","description":"Resource content.","type":"string"},{"name":"base64Encoded","description":"True, if content was served as base64.","type":"boolean"}]},{"name":"getResourceTree","description":"Returns present frame / resource tree structure.","experimental":true,"returns":[{"name":"frameTree","description":"Present frame / resource tree structure.","$ref":"FrameResourceTree"}]},{"name":"handleJavaScriptDialog","description":"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).","parameters":[{"name":"accept","description":"Whether to accept or dismiss the dialog.","type":"boolean"},{"name":"promptText","description":"The text to enter into the dialog prompt before accepting. Used only if this is a prompt\\ndialog.","optional":true,"type":"string"}]},{"name":"navigate","description":"Navigates current page to the given URL.","parameters":[{"name":"url","description":"URL to navigate the page to.","type":"string"},{"name":"referrer","description":"Referrer URL.","optional":true,"type":"string"},{"name":"transitionType","description":"Intended transition type.","optional":true,"$ref":"TransitionType"},{"name":"frameId","description":"Frame id to navigate, if not specified navigates the top frame.","optional":true,"$ref":"FrameId"}],"returns":[{"name":"frameId","description":"Frame id that has navigated (or failed to navigate)","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier.","optional":true,"$ref":"Network.LoaderId"},{"name":"errorText","description":"User friendly error message, present if and only if navigation has failed.","optional":true,"type":"string"}]},{"name":"navigateToHistoryEntry","description":"Navigates current page to the given history entry.","parameters":[{"name":"entryId","description":"Unique id of the entry to navigate to.","type":"integer"}]},{"name":"printToPDF","description":"Print page as PDF.","parameters":[{"name":"landscape","description":"Paper orientation. Defaults to false.","optional":true,"type":"boolean"},{"name":"displayHeaderFooter","description":"Display header and footer. Defaults to false.","optional":true,"type":"boolean"},{"name":"printBackground","description":"Print background graphics. Defaults to false.","optional":true,"type":"boolean"},{"name":"scale","description":"Scale of the webpage rendering. Defaults to 1.","optional":true,"type":"number"},{"name":"paperWidth","description":"Paper width in inches. Defaults to 8.5 inches.","optional":true,"type":"number"},{"name":"paperHeight","description":"Paper height in inches. Defaults to 11 inches.","optional":true,"type":"number"},{"name":"marginTop","description":"Top margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginBottom","description":"Bottom margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginLeft","description":"Left margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginRight","description":"Right margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"pageRanges","description":"Paper ranges to print, e.g., \'1-5, 8, 11-13\'. Defaults to the empty string, which means\\nprint all pages.","optional":true,"type":"string"},{"name":"ignoreInvalidPageRanges","description":"Whether to silently ignore invalid but successfully parsed page ranges, such as \'3-2\'.\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"headerTemplate","description":"HTML template for the print header. Should be valid HTML markup with following\\nclasses used to inject printing values into them:\\n- `date`: formatted print date\\n- `title`: document title\\n- `url`: document location\\n- `pageNumber`: current page number\\n- `totalPages`: total pages in the document\\n\\nFor example, `<span class=title></span>` would generate span containing the title.","optional":true,"type":"string"},{"name":"footerTemplate","description":"HTML template for the print footer. Should use the same format as the `headerTemplate`.","optional":true,"type":"string"},{"name":"preferCSSPageSize","description":"Whether or not to prefer page size as defined by css. Defaults to false,\\nin which case the content will be scaled to fit the paper size.","optional":true,"type":"boolean"},{"name":"transferMode","description":"return as stream","experimental":true,"optional":true,"type":"string","enum":["ReturnAsBase64","ReturnAsStream"]}],"returns":[{"name":"data","description":"Base64-encoded pdf data. Empty if |returnAsStream| is specified.","type":"string"},{"name":"stream","description":"A handle of the stream that holds resulting PDF data.","experimental":true,"optional":true,"$ref":"IO.StreamHandle"}]},{"name":"reload","description":"Reloads given page optionally ignoring the cache.","parameters":[{"name":"ignoreCache","description":"If true, browser cache is ignored (as if the user pressed Shift+refresh).","optional":true,"type":"boolean"},{"name":"scriptToEvaluateOnLoad","description":"If set, the script will be injected into all frames of the inspected page after reload.\\nArgument will be ignored if reloading dataURL origin.","optional":true,"type":"string"}]},{"name":"removeScriptToEvaluateOnLoad","description":"Deprecated, please use removeScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"removeScriptToEvaluateOnNewDocument","description":"Removes given script from the list.","parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"screencastFrameAck","description":"Acknowledges that a screencast frame has been received by the frontend.","experimental":true,"parameters":[{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"searchInResource","description":"Searches for given string in resource content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id for resource to search in.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to search in.","type":"string"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setAdBlockingEnabled","description":"Enable Chrome\'s experimental ad filter on all sites.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to block ads.","type":"boolean"}]},{"name":"setBypassCSP","description":"Enable page Content Security Policy by-passing.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to bypass page CSP.","type":"boolean"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"Emulation.ScreenOrientation"},{"name":"viewport","description":"The viewport dimensions and scale. If not set, the override is cleared.","optional":true,"$ref":"Viewport"}]},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]},{"name":"setFontFamilies","description":"Set generic font families.","experimental":true,"parameters":[{"name":"fontFamilies","description":"Specifies font families to set. If a font family is not specified, it won\'t be changed.","$ref":"FontFamilies"}]},{"name":"setFontSizes","description":"Set default font sizes.","experimental":true,"parameters":[{"name":"fontSizes","description":"Specifies font sizes to set. If a font size is not specified, it won\'t be changed.","$ref":"FontSizes"}]},{"name":"setDocumentContent","description":"Sets given markup as the document\'s HTML.","parameters":[{"name":"frameId","description":"Frame id to set HTML for.","$ref":"FrameId"},{"name":"html","description":"HTML content to set.","type":"string"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny).","type":"string","enum":["deny","allow","default"]},{"name":"downloadPath","description":"The default path to save downloaded files to. This is requred if behavior is set to \'allow\'","optional":true,"type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","deprecated":true,"redirect":"Emulation","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setLifecycleEventsEnabled","description":"Controls whether page will emit lifecycle events.","experimental":true,"parameters":[{"name":"enabled","description":"If true, starts emitting lifecycle events.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Toggles mouse event-based touch event emulation.","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"startScreencast","description":"Starts sending each frame using the `screencastFrame` event.","experimental":true,"parameters":[{"name":"format","description":"Image compression format.","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100].","optional":true,"type":"integer"},{"name":"maxWidth","description":"Maximum screenshot width.","optional":true,"type":"integer"},{"name":"maxHeight","description":"Maximum screenshot height.","optional":true,"type":"integer"},{"name":"everyNthFrame","description":"Send every n-th frame.","optional":true,"type":"integer"}]},{"name":"stopLoading","description":"Force the page stop all navigations and pending resource fetches."},{"name":"crash","description":"Crashes renderer on the IO thread, generates minidumps.","experimental":true},{"name":"close","description":"Tries to close page, running its beforeunload hooks, if any.","experimental":true},{"name":"setWebLifecycleState","description":"Tries to update the web lifecycle state of the page.\\nIt will transition the page to the given state according to:\\nhttps://github.com/WICG/web-lifecycle/","experimental":true,"parameters":[{"name":"state","description":"Target lifecycle state","type":"string","enum":["frozen","active"]}]},{"name":"stopScreencast","description":"Stops sending each frame in the `screencastFrame`.","experimental":true},{"name":"setProduceCompilationCache","description":"Forces compilation cache to be generated for every subresource script.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"addCompilationCache","description":"Seeds compilation cache for given url. Compilation cache does not survive\\ncross-process navigation.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data","type":"string"}]},{"name":"clearCompilationCache","description":"Clears seeded compilation cache.","experimental":true},{"name":"generateTestReport","description":"Generates a report for testing.","experimental":true,"parameters":[{"name":"message","description":"Message to be displayed in the report.","type":"string"},{"name":"group","description":"Specifies the endpoint group to deliver the report to.","optional":true,"type":"string"}]},{"name":"waitForDebugger","description":"Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.","experimental":true},{"name":"setInterceptFileChooserDialog","description":"Intercept file chooser requests and transfer control to protocol clients.\\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\\nInstead, a protocol event `Page.fileChooserOpened` is emitted.\\nFile chooser can be handled with `page.handleFileChooser` command.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"handleFileChooser","description":"Accepts or cancels an intercepted file chooser dialog.","experimental":true,"parameters":[{"name":"action","type":"string","enum":["accept","cancel","fallback"]},{"name":"files","description":"Array of absolute file paths to set, only respected with `accept` action.","optional":true,"type":"array","items":{"type":"string"}}]}],"events":[{"name":"domContentEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"fileChooserOpened","description":"Emitted only when `page.interceptFileChooser` is enabled.","parameters":[{"name":"mode","type":"string","enum":["selectSingle","selectMultiple"]}]},{"name":"frameAttached","description":"Fired when frame has been attached to its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been attached.","$ref":"FrameId"},{"name":"parentFrameId","description":"Parent frame identifier.","$ref":"FrameId"},{"name":"stack","description":"JavaScript stack trace of when frame was attached, only set if frame initiated from script.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"frameClearedScheduledNavigation","description":"Fired when frame no longer has a scheduled navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has cleared its scheduled navigation.","$ref":"FrameId"}]},{"name":"frameDetached","description":"Fired when frame has been detached from its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been detached.","$ref":"FrameId"}]},{"name":"frameNavigated","description":"Fired once navigation of the frame has completed. Frame is now associated with the new loader.","parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"}]},{"name":"frameResized","experimental":true},{"name":"frameRequestedNavigation","description":"Fired when a renderer-initiated navigation is requested.\\nNavigation may still be cancelled after the event is issued.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that is being navigated.","$ref":"FrameId"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the requested navigation.","type":"string"}]},{"name":"frameScheduledNavigation","description":"Fired when frame schedules a potential navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has scheduled a navigation.","$ref":"FrameId"},{"name":"delay","description":"Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\\nguaranteed to start.","type":"number"},{"name":"reason","description":"The reason for the navigation.","type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload"]},{"name":"url","description":"The destination URL for the scheduled navigation.","type":"string"}]},{"name":"frameStartedLoading","description":"Fired when frame has started loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has started loading.","$ref":"FrameId"}]},{"name":"frameStoppedLoading","description":"Fired when frame has stopped loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has stopped loading.","$ref":"FrameId"}]},{"name":"downloadWillBegin","description":"Fired when page is about to start a download.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused download to begin.","$ref":"FrameId"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"}]},{"name":"interstitialHidden","description":"Fired when interstitial page was hidden"},{"name":"interstitialShown","description":"Fired when interstitial page was shown"},{"name":"javascriptDialogClosed","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\\nclosed.","parameters":[{"name":"result","description":"Whether dialog was confirmed.","type":"boolean"},{"name":"userInput","description":"User input in case of prompt.","type":"string"}]},{"name":"javascriptDialogOpening","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\\nopen.","parameters":[{"name":"url","description":"Frame url.","type":"string"},{"name":"message","description":"Message that will be displayed by the dialog.","type":"string"},{"name":"type","description":"Dialog type.","$ref":"DialogType"},{"name":"hasBrowserHandler","description":"True iff browser is capable showing or acting on the given dialog. When browser has no\\ndialog handler for given target, calling alert while Page domain is engaged will stall\\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.","type":"boolean"},{"name":"defaultPrompt","description":"Default dialog prompt.","optional":true,"type":"string"}]},{"name":"lifecycleEvent","description":"Fired for top level page lifecycle events such as navigation, load, paint, etc.","parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"Network.LoaderId"},{"name":"name","type":"string"},{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"loadEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"navigatedWithinDocument","description":"Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"url","description":"Frame\'s new url.","type":"string"}]},{"name":"screencastFrame","description":"Compressed image data requested by the `startScreencast`.","experimental":true,"parameters":[{"name":"data","description":"Base64-encoded compressed image.","type":"string"},{"name":"metadata","description":"Screencast frame metadata.","$ref":"ScreencastFrameMetadata"},{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"screencastVisibilityChanged","description":"Fired when the page with currently enabled screencast was shown or hidden `.","experimental":true,"parameters":[{"name":"visible","description":"True if the page is visible.","type":"boolean"}]},{"name":"windowOpen","description":"Fired when a new window is going to be opened, via window.open(), link click, form submission,\\netc.","parameters":[{"name":"url","description":"The URL for the new window.","type":"string"},{"name":"windowName","description":"Window name.","type":"string"},{"name":"windowFeatures","description":"An array of enabled window features.","type":"array","items":{"type":"string"}},{"name":"userGesture","description":"Whether or not it was triggered by user gesture.","type":"boolean"}]},{"name":"compilationCacheProduced","description":"Issued for every compilation cache generated. Is only available\\nif Page.setGenerateCompilationCache is enabled.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data","type":"string"}]}]},{"domain":"Performance","types":[{"id":"Metric","description":"Run-time execution metric.","type":"object","properties":[{"name":"name","description":"Metric name.","type":"string"},{"name":"value","description":"Metric value.","type":"number"}]}],"commands":[{"name":"disable","description":"Disable collecting and reporting metrics."},{"name":"enable","description":"Enable collecting and reporting metrics."},{"name":"setTimeDomain","description":"Sets time domain to use for collecting and reporting duration metrics.\\nNote that this must be called before enabling metrics collection. Calling\\nthis method while metrics collection is enabled returns an error.","experimental":true,"parameters":[{"name":"timeDomain","description":"Time domain","type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"getMetrics","description":"Retrieve current values of run-time metrics.","returns":[{"name":"metrics","description":"Current values for run-time metrics.","type":"array","items":{"$ref":"Metric"}}]}],"events":[{"name":"metrics","description":"Current values of the metrics.","parameters":[{"name":"metrics","description":"Current values of the metrics.","type":"array","items":{"$ref":"Metric"}},{"name":"title","description":"Timestamp title.","type":"string"}]}]},{"domain":"Security","description":"Security","types":[{"id":"CertificateId","description":"An internal certificate ID value.","type":"integer"},{"id":"MixedContentType","description":"A description of mixed content (HTTP resources on HTTPS pages), as defined by\\nhttps://www.w3.org/TR/mixed-content/#categories","type":"string","enum":["blockable","optionally-blockable","none"]},{"id":"SecurityState","description":"The security level of a page or resource.","type":"string","enum":["unknown","neutral","insecure","secure","info"]},{"id":"SecurityStateExplanation","description":"An explanation of an factor contributing to the security state.","type":"object","properties":[{"name":"securityState","description":"Security state representing the severity of the factor being explained.","$ref":"SecurityState"},{"name":"title","description":"Title describing the type of factor.","type":"string"},{"name":"summary","description":"Short phrase describing the type of factor.","type":"string"},{"name":"description","description":"Full text explanation of the factor.","type":"string"},{"name":"mixedContentType","description":"The type of mixed content described by the explanation.","$ref":"MixedContentType"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"recommendations","description":"Recommendations to fix any issues.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"InsecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"type":"object","properties":[{"name":"ranMixedContent","description":"Always false.","type":"boolean"},{"name":"displayedMixedContent","description":"Always false.","type":"boolean"},{"name":"containedMixedForm","description":"Always false.","type":"boolean"},{"name":"ranContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"displayedContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"ranInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"},{"name":"displayedInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"}]},{"id":"CertificateErrorAction","description":"The action to take when a certificate error occurs. continue will continue processing the\\nrequest and cancel will cancel the request.","type":"string","enum":["continue","cancel"]}],"commands":[{"name":"disable","description":"Disables tracking security state changes."},{"name":"enable","description":"Enables tracking security state changes."},{"name":"setIgnoreCertificateErrors","description":"Enable/disable whether all certificate errors should be ignored.","experimental":true,"parameters":[{"name":"ignore","description":"If true, all certificate errors will be ignored.","type":"boolean"}]},{"name":"handleCertificateError","description":"Handles a certificate error that fired a certificateError event.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"action","description":"The action to take on the certificate error.","$ref":"CertificateErrorAction"}]},{"name":"setOverrideCertificateErrors","description":"Enable/disable overriding certificate errors. If enabled, all certificate error events need to\\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.","deprecated":true,"parameters":[{"name":"override","description":"If true, certificate errors will be overridden.","type":"boolean"}]}],"events":[{"name":"certificateError","description":"There is a certificate error. If overriding certificate errors is enabled, then it should be\\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\\ncertificate error has been allowed internally. Only one client per target should override\\ncertificate errors at the same time.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"errorType","description":"The type of the error.","type":"string"},{"name":"requestURL","description":"The url that was requested.","type":"string"}]},{"name":"securityStateChanged","description":"The security state of the page changed.","parameters":[{"name":"securityState","description":"Security state.","$ref":"SecurityState"},{"name":"schemeIsCryptographic","description":"True if the page was loaded over cryptographic transport such as HTTPS.","deprecated":true,"type":"boolean"},{"name":"explanations","description":"List of explanations for the security state. If the overall security state is `insecure` or\\n`warning`, at least one corresponding explanation should be included.","type":"array","items":{"$ref":"SecurityStateExplanation"}},{"name":"insecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"$ref":"InsecureContentStatus"},{"name":"summary","description":"Overrides user-visible description of the state.","optional":true,"type":"string"}]}]},{"domain":"ServiceWorker","experimental":true,"types":[{"id":"RegistrationID","type":"string"},{"id":"ServiceWorkerRegistration","description":"ServiceWorker registration.","type":"object","properties":[{"name":"registrationId","$ref":"RegistrationID"},{"name":"scopeURL","type":"string"},{"name":"isDeleted","type":"boolean"}]},{"id":"ServiceWorkerVersionRunningStatus","type":"string","enum":["stopped","starting","running","stopping"]},{"id":"ServiceWorkerVersionStatus","type":"string","enum":["new","installing","installed","activating","activated","redundant"]},{"id":"ServiceWorkerVersion","description":"ServiceWorker version.","type":"object","properties":[{"name":"versionId","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"scriptURL","type":"string"},{"name":"runningStatus","$ref":"ServiceWorkerVersionRunningStatus"},{"name":"status","$ref":"ServiceWorkerVersionStatus"},{"name":"scriptLastModified","description":"The Last-Modified header value of the main script.","optional":true,"type":"number"},{"name":"scriptResponseTime","description":"The time at which the response headers of the main script were received from the server.\\nFor cached script it is the last time the cache entry was validated.","optional":true,"type":"number"},{"name":"controlledClients","optional":true,"type":"array","items":{"$ref":"Target.TargetID"}},{"name":"targetId","optional":true,"$ref":"Target.TargetID"}]},{"id":"ServiceWorkerErrorMessage","description":"ServiceWorker error message.","type":"object","properties":[{"name":"errorMessage","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"versionId","type":"string"},{"name":"sourceURL","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]}],"commands":[{"name":"deliverPushMessage","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"data","type":"string"}]},{"name":"disable"},{"name":"dispatchSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"},{"name":"lastChance","type":"boolean"}]},{"name":"enable"},{"name":"inspectWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"setForceUpdateOnPageLoad","parameters":[{"name":"forceUpdateOnPageLoad","type":"boolean"}]},{"name":"skipWaiting","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"startWorker","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"stopAllWorkers"},{"name":"stopWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"unregister","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"updateRegistration","parameters":[{"name":"scopeURL","type":"string"}]}],"events":[{"name":"workerErrorReported","parameters":[{"name":"errorMessage","$ref":"ServiceWorkerErrorMessage"}]},{"name":"workerRegistrationUpdated","parameters":[{"name":"registrations","type":"array","items":{"$ref":"ServiceWorkerRegistration"}}]},{"name":"workerVersionUpdated","parameters":[{"name":"versions","type":"array","items":{"$ref":"ServiceWorkerVersion"}}]}]},{"domain":"Storage","experimental":true,"types":[{"id":"StorageType","description":"Enum of possible storage types.","type":"string","enum":["appcache","cookies","file_systems","indexeddb","local_storage","shader_cache","websql","service_workers","cache_storage","all","other"]},{"id":"UsageForType","description":"Usage for a storage type.","type":"object","properties":[{"name":"storageType","description":"Name of storage type.","$ref":"StorageType"},{"name":"usage","description":"Storage usage (bytes).","type":"number"}]}],"commands":[{"name":"clearDataForOrigin","description":"Clears storage for origin.","parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"getUsageAndQuota","description":"Returns usage and quota in bytes.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}],"returns":[{"name":"usage","description":"Storage usage (bytes).","type":"number"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"usageBreakdown","description":"Storage usage per type (bytes).","type":"array","items":{"$ref":"UsageForType"}}]},{"name":"trackCacheStorageForOrigin","description":"Registers origin to be notified when an update occurs to its cache storage list.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackIndexedDBForOrigin","description":"Registers origin to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackCacheStorageForOrigin","description":"Unregisters origin from receiving notifications for cache storage.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackIndexedDBForOrigin","description":"Unregisters origin from receiving notifications for IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]}],"events":[{"name":"cacheStorageContentUpdated","description":"A cache\'s contents have been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"cacheName","description":"Name of cache in origin.","type":"string"}]},{"name":"cacheStorageListUpdated","description":"A cache has been added/deleted.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"}]},{"name":"indexedDBContentUpdated","description":"The origin\'s IndexedDB object store has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"databaseName","description":"Database to update.","type":"string"},{"name":"objectStoreName","description":"ObjectStore to update.","type":"string"}]},{"name":"indexedDBListUpdated","description":"The origin\'s IndexedDB database list has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"}]}]},{"domain":"SystemInfo","description":"The SystemInfo domain defines methods and events for querying low-level system information.","experimental":true,"types":[{"id":"GPUDevice","description":"Describes a single graphics processor (GPU).","type":"object","properties":[{"name":"vendorId","description":"PCI ID of the GPU vendor, if available; 0 otherwise.","type":"number"},{"name":"deviceId","description":"PCI ID of the GPU device, if available; 0 otherwise.","type":"number"},{"name":"vendorString","description":"String description of the GPU vendor, if the PCI ID is not available.","type":"string"},{"name":"deviceString","description":"String description of the GPU device, if the PCI ID is not available.","type":"string"},{"name":"driverVendor","description":"String description of the GPU driver vendor.","type":"string"},{"name":"driverVersion","description":"String description of the GPU driver version.","type":"string"}]},{"id":"Size","description":"Describes the width and height dimensions of an entity.","type":"object","properties":[{"name":"width","description":"Width in pixels.","type":"integer"},{"name":"height","description":"Height in pixels.","type":"integer"}]},{"id":"VideoDecodeAcceleratorCapability","description":"Describes a supported video decoding profile with its associated minimum and\\nmaximum resolutions.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g. VP9 Profile 2.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"minResolution","description":"Minimum video dimensions in pixels supported for this |profile|.","$ref":"Size"}]},{"id":"VideoEncodeAcceleratorCapability","description":"Describes a supported video encoding profile with its associated maximum\\nresolution and maximum framerate.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g H264 Main.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"maxFramerateNumerator","description":"Maximum encoding framerate in frames per second supported for this\\n|profile|, as fraction\'s numerator and denominator, e.g. 24/1 fps,\\n24000/1001 fps, etc.","type":"integer"},{"name":"maxFramerateDenominator","type":"integer"}]},{"id":"SubsamplingFormat","description":"YUV subsampling type of the pixels of a given image.","type":"string","enum":["yuv420","yuv422","yuv444"]},{"id":"ImageDecodeAcceleratorCapability","description":"Describes a supported image decoding profile with its associated minimum and\\nmaximum resolutions and subsampling.","type":"object","properties":[{"name":"imageType","description":"Image coded, e.g. Jpeg.","type":"string"},{"name":"maxDimensions","description":"Maximum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"minDimensions","description":"Minimum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"subsamplings","description":"Optional array of supported subsampling formats, e.g. 4:2:0, if known.","type":"array","items":{"$ref":"SubsamplingFormat"}}]},{"id":"GPUInfo","description":"Provides information about the GPU(s) on the system.","type":"object","properties":[{"name":"devices","description":"The graphics devices on the system. Element 0 is the primary GPU.","type":"array","items":{"$ref":"GPUDevice"}},{"name":"auxAttributes","description":"An optional dictionary of additional GPU related attributes.","optional":true,"type":"object"},{"name":"featureStatus","description":"An optional dictionary of graphics features and their status.","optional":true,"type":"object"},{"name":"driverBugWorkarounds","description":"An optional array of GPU driver bug workarounds.","type":"array","items":{"type":"string"}},{"name":"videoDecoding","description":"Supported accelerated video decoding capabilities.","type":"array","items":{"$ref":"VideoDecodeAcceleratorCapability"}},{"name":"videoEncoding","description":"Supported accelerated video encoding capabilities.","type":"array","items":{"$ref":"VideoEncodeAcceleratorCapability"}},{"name":"imageDecoding","description":"Supported accelerated image decoding capabilities.","type":"array","items":{"$ref":"ImageDecodeAcceleratorCapability"}}]},{"id":"ProcessInfo","description":"Represents process info.","type":"object","properties":[{"name":"type","description":"Specifies process type.","type":"string"},{"name":"id","description":"Specifies process id.","type":"integer"},{"name":"cpuTime","description":"Specifies cumulative CPU usage in seconds across all threads of the\\nprocess since the process start.","type":"number"}]}],"commands":[{"name":"getInfo","description":"Returns information about the system.","returns":[{"name":"gpu","description":"Information about the GPUs on the system.","$ref":"GPUInfo"},{"name":"modelName","description":"A platform-dependent description of the model of the machine. On Mac OS, this is, for\\nexample, \'MacBookPro\'. Will be the empty string if not supported.","type":"string"},{"name":"modelVersion","description":"A platform-dependent description of the version of the machine. On Mac OS, this is, for\\nexample, \'10.1\'. Will be the empty string if not supported.","type":"string"},{"name":"commandLine","description":"The command line string used to launch the browser. Will be the empty string if not\\nsupported.","type":"string"}]},{"name":"getProcessInfo","description":"Returns information about all running processes.","returns":[{"name":"processInfo","description":"An array of process info blocks.","type":"array","items":{"$ref":"ProcessInfo"}}]}]},{"domain":"Target","description":"Supports additional targets discovery and allows to attach to them.","types":[{"id":"TargetID","type":"string"},{"id":"SessionID","description":"Unique identifier of attached debugging session.","type":"string"},{"id":"BrowserContextID","experimental":true,"type":"string"},{"id":"TargetInfo","type":"object","properties":[{"name":"targetId","$ref":"TargetID"},{"name":"type","type":"string"},{"name":"title","type":"string"},{"name":"url","type":"string"},{"name":"attached","description":"Whether the target has an attached client.","type":"boolean"},{"name":"openerId","description":"Opener target Id","optional":true,"$ref":"TargetID"},{"name":"browserContextId","experimental":true,"optional":true,"$ref":"BrowserContextID"}]},{"id":"RemoteLocation","experimental":true,"type":"object","properties":[{"name":"host","type":"string"},{"name":"port","type":"integer"}]}],"commands":[{"name":"activateTarget","description":"Activates (focuses) the target.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"attachToTarget","description":"Attaches to the target with given id.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"attachToBrowserTarget","description":"Attaches to the browser target, only uses flat sessionId mode.","experimental":true,"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"closeTarget","description":"Closes the target. If the target is a page that gets closed too.","parameters":[{"name":"targetId","$ref":"TargetID"}],"returns":[{"name":"success","type":"boolean"}]},{"name":"exposeDevToolsProtocol","description":"Inject object to the target\'s main frame that provides a communication\\nchannel with browser target.\\n\\nInjected object will be available as `window[bindingName]`.\\n\\nThe object has the follwing API:\\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"bindingName","description":"Binding name, \'cdp\' if not specified.","optional":true,"type":"string"}]},{"name":"createBrowserContext","description":"Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\\none.","experimental":true,"returns":[{"name":"browserContextId","description":"The id of the context created.","$ref":"BrowserContextID"}]},{"name":"getBrowserContexts","description":"Returns all browser contexts created with `Target.createBrowserContext` method.","experimental":true,"returns":[{"name":"browserContextIds","description":"An array of browser context ids.","type":"array","items":{"$ref":"BrowserContextID"}}]},{"name":"createTarget","description":"Creates a new page.","parameters":[{"name":"url","description":"The initial URL the page will be navigated to.","type":"string"},{"name":"width","description":"Frame width in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"height","description":"Frame height in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"browserContextId","description":"The browser context to create the page in.","optional":true,"$ref":"BrowserContextID"},{"name":"enableBeginFrameControl","description":"Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\\nnot supported on MacOS yet, false by default).","experimental":true,"optional":true,"type":"boolean"},{"name":"newWindow","description":"Whether to create a new Window or Tab (chrome-only, false by default).","optional":true,"type":"boolean"},{"name":"background","description":"Whether to create the target in background or foreground (chrome-only,\\nfalse by default).","optional":true,"type":"boolean"}],"returns":[{"name":"targetId","description":"The id of the page opened.","$ref":"TargetID"}]},{"name":"detachFromTarget","description":"Detaches session with given id.","parameters":[{"name":"sessionId","description":"Session to detach.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"disposeBrowserContext","description":"Deletes a BrowserContext. All the belonging pages will be closed without calling their\\nbeforeunload hooks.","experimental":true,"parameters":[{"name":"browserContextId","$ref":"BrowserContextID"}]},{"name":"getTargetInfo","description":"Returns information about a target.","experimental":true,"parameters":[{"name":"targetId","optional":true,"$ref":"TargetID"}],"returns":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"getTargets","description":"Retrieves a list of available targets.","returns":[{"name":"targetInfos","description":"The list of targets.","type":"array","items":{"$ref":"TargetInfo"}}]},{"name":"sendMessageToTarget","description":"Sends protocol message over session with given id.","parameters":[{"name":"message","type":"string"},{"name":"sessionId","description":"Identifier of the session.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"setAutoAttach","description":"Controls whether to automatically attach to new targets which are considered to be related to\\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\\nautomatically detaches from all currently attached targets.","experimental":true,"parameters":[{"name":"autoAttach","description":"Whether to auto-attach to related targets.","type":"boolean"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"setDiscoverTargets","description":"Controls whether to discover available targets and notify via\\n`targetCreated/targetInfoChanged/targetDestroyed` events.","parameters":[{"name":"discover","description":"Whether to discover available targets.","type":"boolean"}]},{"name":"setRemoteLocations","description":"Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\\n`true`.","experimental":true,"parameters":[{"name":"locations","description":"List of remote locations.","type":"array","items":{"$ref":"RemoteLocation"}}]}],"events":[{"name":"attachedToTarget","description":"Issued when attached to target because of auto-attach or `attachToTarget` command.","experimental":true,"parameters":[{"name":"sessionId","description":"Identifier assigned to the session used to send/receive messages.","$ref":"SessionID"},{"name":"targetInfo","$ref":"TargetInfo"},{"name":"waitingForDebugger","type":"boolean"}]},{"name":"detachedFromTarget","description":"Issued when detached from target for any reason (including `detachFromTarget` command). Can be\\nissued multiple times per target if multiple sessions have been attached to it.","experimental":true,"parameters":[{"name":"sessionId","description":"Detached session identifier.","$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"receivedMessageFromTarget","description":"Notifies about a new protocol message received from the session (as reported in\\n`attachedToTarget` event).","parameters":[{"name":"sessionId","description":"Identifier of a session which sends a message.","$ref":"SessionID"},{"name":"message","type":"string"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"targetCreated","description":"Issued when a possible inspection target is created.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"targetDestroyed","description":"Issued when a target is destroyed.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"targetCrashed","description":"Issued when a target has crashed.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"status","description":"Termination status type.","type":"string"},{"name":"errorCode","description":"Termination error code.","type":"integer"}]},{"name":"targetInfoChanged","description":"Issued when some information about a target has changed. This only happens between\\n`targetCreated` and `targetDestroyed`.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]}]},{"domain":"Tethering","description":"The Tethering domain defines methods and events for browser port binding.","experimental":true,"commands":[{"name":"bind","description":"Request browser port binding.","parameters":[{"name":"port","description":"Port number to bind.","type":"integer"}]},{"name":"unbind","description":"Request browser port unbinding.","parameters":[{"name":"port","description":"Port number to unbind.","type":"integer"}]}],"events":[{"name":"accepted","description":"Informs that port was successfully bound and got a specified connection id.","parameters":[{"name":"port","description":"Port number that was successfully bound.","type":"integer"},{"name":"connectionId","description":"Connection id to be used.","type":"string"}]}]},{"domain":"Tracing","experimental":true,"dependencies":["IO"],"types":[{"id":"MemoryDumpConfig","description":"Configuration for memory dump. Used only when \\"memory-infra\\" category is enabled.","type":"object"},{"id":"TraceConfig","type":"object","properties":[{"name":"recordMode","description":"Controls how the trace buffer stores data.","optional":true,"type":"string","enum":["recordUntilFull","recordContinuously","recordAsMuchAsPossible","echoToConsole"]},{"name":"enableSampling","description":"Turns on JavaScript stack sampling.","optional":true,"type":"boolean"},{"name":"enableSystrace","description":"Turns on system tracing.","optional":true,"type":"boolean"},{"name":"enableArgumentFilter","description":"Turns on argument filter.","optional":true,"type":"boolean"},{"name":"includedCategories","description":"Included category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"excludedCategories","description":"Excluded category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"syntheticDelays","description":"Configuration to synthesize the delays in tracing.","optional":true,"type":"array","items":{"type":"string"}},{"name":"memoryDumpConfig","description":"Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.","optional":true,"$ref":"MemoryDumpConfig"}]},{"id":"StreamFormat","description":"Data format of a trace. Can be either the legacy JSON format or the\\nprotocol buffer format. Note that the JSON format will be deprecated soon.","type":"string","enum":["json","proto"]},{"id":"StreamCompression","description":"Compression type to use for traces returned via streams.","type":"string","enum":["none","gzip"]}],"commands":[{"name":"end","description":"Stop trace events collection."},{"name":"getCategories","description":"Gets supported tracing categories.","returns":[{"name":"categories","description":"A list of supported tracing categories.","type":"array","items":{"type":"string"}}]},{"name":"recordClockSyncMarker","description":"Record a clock sync marker in the trace.","parameters":[{"name":"syncId","description":"The ID of this clock sync marker","type":"string"}]},{"name":"requestMemoryDump","description":"Request a global memory dump.","returns":[{"name":"dumpGuid","description":"GUID of the resulting global memory dump.","type":"string"},{"name":"success","description":"True iff the global memory dump succeeded.","type":"boolean"}]},{"name":"start","description":"Start trace events collection.","parameters":[{"name":"categories","description":"Category/tag filter","deprecated":true,"optional":true,"type":"string"},{"name":"options","description":"Tracing options","deprecated":true,"optional":true,"type":"string"},{"name":"bufferUsageReportingInterval","description":"If set, the agent will issue bufferUsage events at this interval, specified in milliseconds","optional":true,"type":"number"},{"name":"transferMode","description":"Whether to report trace events as series of dataCollected events or to save trace to a\\nstream (defaults to `ReportEvents`).","optional":true,"type":"string","enum":["ReportEvents","ReturnAsStream"]},{"name":"streamFormat","description":"Trace data format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `json`).","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `none`)","optional":true,"$ref":"StreamCompression"},{"name":"traceConfig","optional":true,"$ref":"TraceConfig"}]}],"events":[{"name":"bufferUsage","parameters":[{"name":"percentFull","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"},{"name":"eventCount","description":"An approximate number of events in the trace log.","optional":true,"type":"number"},{"name":"value","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"}]},{"name":"dataCollected","description":"Contains an bucket of collected trace events. When tracing is stopped collected events will be\\nsend as a sequence of dataCollected events followed by tracingComplete event.","parameters":[{"name":"value","type":"array","items":{"type":"object"}}]},{"name":"tracingComplete","description":"Signals that tracing is stopped and there is no trace buffers pending flush, all data were\\ndelivered via dataCollected events.","parameters":[{"name":"dataLossOccurred","description":"Indicates whether some trace data is known to have been lost, e.g. because the trace ring\\nbuffer wrapped around.","type":"boolean"},{"name":"stream","description":"A handle of the stream that holds resulting trace data.","optional":true,"$ref":"IO.StreamHandle"},{"name":"traceFormat","description":"Trace data format of returned stream.","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format of returned stream.","optional":true,"$ref":"StreamCompression"}]}]},{"domain":"Fetch","description":"A domain for letting clients substitute browser\'s network layer with client code.","experimental":true,"dependencies":["Network","IO","Page"],"types":[{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"RequestStage","description":"Stages of the request to handle. Request will intercept before the request is\\nsent. Response will intercept after the response is received (but before response\\nbody is received.","experimental":true,"type":"string","enum":["Request","Response"]},{"id":"RequestPattern","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (\'*\' -> zero or more, \'?\' -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to \\"*\\".","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"Network.ResourceType"},{"name":"requestStage","description":"Stage at wich to begin intercepting requests. Default is Request.","optional":true,"$ref":"RequestStage"}]},{"id":"HeaderEntry","description":"Response HTTP header entry","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]}],"commands":[{"name":"disable","description":"Disables the fetch domain."},{"name":"enable","description":"Enables issuing of requestPaused events. A request will be paused until client\\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.","parameters":[{"name":"patterns","description":"If specified, only requests matching any of these patterns will produce\\nfetchRequested event and will be paused until clients response. If not set,\\nall requests will be affected.","optional":true,"type":"array","items":{"$ref":"RequestPattern"}},{"name":"handleAuthRequests","description":"If true, authRequired events will be issued and requests will be paused\\nexpecting a call to continueWithAuth.","optional":true,"type":"boolean"}]},{"name":"failRequest","description":"Causes the request to fail with specified reason.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"errorReason","description":"Causes the request to fail with the given reason.","$ref":"Network.ErrorReason"}]},{"name":"fulfillRequest","description":"Provides response to the request.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code.","type":"integer"},{"name":"responseHeaders","description":"Response headers.","type":"array","items":{"$ref":"HeaderEntry"}},{"name":"body","description":"A response body.","optional":true,"type":"string"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase mathcing responseCode is used.","optional":true,"type":"string"}]},{"name":"continueRequest","description":"Continues the request, optionally modifying some of its parameters.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"url","description":"If set, the request url will be modified in a way that\'s not observable by page.","optional":true,"type":"string"},{"name":"method","description":"If set, the request method is overridden.","optional":true,"type":"string"},{"name":"postData","description":"If set, overrides the post data in the request.","optional":true,"type":"string"},{"name":"headers","description":"If set, overrides the request headrts.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}}]},{"name":"continueWithAuth","description":"Continues a request supplying authChallengeResponse following authRequired event.","parameters":[{"name":"requestId","description":"An id the client received in authRequired event.","$ref":"RequestId"},{"name":"authChallengeResponse","description":"Response to with an authChallenge.","$ref":"AuthChallengeResponse"}]},{"name":"getResponseBody","description":"Causes the body of the response to be received from the server and\\nreturned as a single string. May only be issued for a request that\\nis paused in the Response stage and is mutually exclusive with\\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\\naffect the request or disabling fetch domain before body is received\\nresults in an undefined behavior.","parameters":[{"name":"requestId","description":"Identifier for the intercepted request to get body for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyAsStream","description":"Returns a handle to the stream representing the response body.\\nThe request must be paused in the HeadersReceived stage.\\nNote that after this command the request can\'t be continued\\nas is -- client either needs to cancel it or to provide the\\nresponse body.\\nThe stream only supports sequential read, IO.read will fail if the position\\nis specified.\\nThis method is mutually exclusive with getResponseBody.\\nCalling other methods that affect the request or disabling fetch\\ndomain before body is received results in an undefined behavior.","parameters":[{"name":"requestId","$ref":"RequestId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]}],"events":[{"name":"requestPaused","description":"Issued when the domain is enabled and the request URL matches the\\nspecified filter. The request is paused until the client responds\\nwith one of continueRequest, failRequest or fulfillRequest.\\nThe stage of the request can be determined by presence of responseErrorReason\\nand responseStatusCode -- the request is at the response stage if either\\nof these fields is present and in the request stage otherwise.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage.","optional":true,"$ref":"Network.ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"networkId","description":"If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"authRequired","description":"Issued when the domain is enabled with handleAuthRequests set to true.\\nThe request is paused until client responds with continueWithAuth.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered.\\nIf this is set, client should respond with continueRequest that\\ncontains AuthChallengeResponse.","$ref":"AuthChallenge"}]}]},{"domain":"WebAudio","description":"This domain allows inspection of Web Audio API.\\nhttps://webaudio.github.io/web-audio-api/","experimental":true,"types":[{"id":"ContextId","description":"Context\'s UUID in string","type":"string"},{"id":"ContextType","description":"Enum of BaseAudioContext types","type":"string","enum":["realtime","offline"]},{"id":"ContextState","description":"Enum of AudioContextState from the spec","type":"string","enum":["suspended","running","closed"]},{"id":"ContextRealtimeData","description":"Fields in AudioContext that change in real-time.","type":"object","properties":[{"name":"currentTime","description":"The current context time in second in BaseAudioContext.","type":"number"},{"name":"renderCapacity","description":"The time spent on rendering graph divided by render qunatum duration,\\nand multiplied by 100. 100 means the audio renderer reached the full\\ncapacity and glitch may occur.","type":"number"},{"name":"callbackIntervalMean","description":"A running mean of callback interval.","type":"number"},{"name":"callbackIntervalVariance","description":"A running variance of callback interval.","type":"number"}]},{"id":"BaseAudioContext","description":"Protocol object for BaseAudioContext","type":"object","properties":[{"name":"contextId","$ref":"ContextId"},{"name":"contextType","$ref":"ContextType"},{"name":"contextState","$ref":"ContextState"},{"name":"realtimeData","optional":true,"$ref":"ContextRealtimeData"},{"name":"callbackBufferSize","description":"Platform-dependent callback buffer size.","type":"number"},{"name":"maxOutputChannelCount","description":"Number of output channels supported by audio hardware in use.","type":"number"},{"name":"sampleRate","description":"Context sample rate.","type":"number"}]}],"commands":[{"name":"enable","description":"Enables the WebAudio domain and starts sending context lifetime events."},{"name":"disable","description":"Disables the WebAudio domain."},{"name":"getRealtimeData","description":"Fetch the realtime data from the registered contexts.","parameters":[{"name":"contextId","$ref":"ContextId"}],"returns":[{"name":"realtimeData","$ref":"ContextRealtimeData"}]}],"events":[{"name":"contextCreated","description":"Notifies that a new BaseAudioContext has been created.","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"contextDestroyed","description":"Notifies that existing BaseAudioContext has been destroyed.","parameters":[{"name":"contextId","$ref":"ContextId"}]},{"name":"contextChanged","description":"Notifies that existing BaseAudioContext has changed some properties (id stays the same)..","parameters":[{"name":"context","$ref":"BaseAudioContext"}]}]},{"domain":"WebAuthn","description":"This domain allows configuring virtual authenticators to test the WebAuthn\\nAPI.","experimental":true,"types":[{"id":"AuthenticatorId","type":"string"},{"id":"AuthenticatorProtocol","type":"string","enum":["u2f","ctap2"]},{"id":"AuthenticatorTransport","type":"string","enum":["usb","nfc","ble","cable","internal"]},{"id":"VirtualAuthenticatorOptions","type":"object","properties":[{"name":"protocol","$ref":"AuthenticatorProtocol"},{"name":"transport","$ref":"AuthenticatorTransport"},{"name":"hasResidentKey","type":"boolean"},{"name":"hasUserVerification","type":"boolean"},{"name":"automaticPresenceSimulation","description":"If set to true, tests of user presence will succeed immediately.\\nOtherwise, they will not be resolved. Defaults to true.","optional":true,"type":"boolean"}]},{"id":"Credential","type":"object","properties":[{"name":"credentialId","type":"string"},{"name":"rpIdHash","description":"SHA-256 hash of the Relying Party ID the credential is scoped to. Must\\nbe 32 bytes long.\\nSee https://w3c.github.io/webauthn/#rpidhash","type":"string"},{"name":"privateKey","description":"The private key in PKCS#8 format.","type":"string"},{"name":"signCount","description":"Signature counter. This is incremented by one for each successful\\nassertion.\\nSee https://w3c.github.io/webauthn/#signature-counter","type":"integer"}]}],"commands":[{"name":"enable","description":"Enable the WebAuthn domain and start intercepting credential storage and\\nretrieval with a virtual authenticator."},{"name":"disable","description":"Disable the WebAuthn domain."},{"name":"addVirtualAuthenticator","description":"Creates and adds a virtual authenticator.","parameters":[{"name":"options","$ref":"VirtualAuthenticatorOptions"}],"returns":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"removeVirtualAuthenticator","description":"Removes the given authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"addCredential","description":"Adds the credential to the specified authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"getCredentials","description":"Returns all the credentials stored in the given virtual authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}],"returns":[{"name":"credentials","type":"array","items":{"$ref":"Credential"}}]},{"name":"clearCredentials","description":"Clears all the credentials from the specified device.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isUserVerified","type":"boolean"}]}]},{"domain":"Console","description":"This domain is deprecated - use Runtime or Log instead.","deprecated":true,"dependencies":["Runtime"],"types":[{"id":"ConsoleMessage","description":"Console message.","type":"object","properties":[{"name":"source","description":"Message source.","type":"string","enum":["xml","javascript","network","console-api","storage","appcache","rendering","security","other","deprecation","worker"]},{"name":"level","description":"Message severity.","type":"string","enum":["log","warning","error","debug","info"]},{"name":"text","description":"Message text.","type":"string"},{"name":"url","description":"URL of the message origin.","optional":true,"type":"string"},{"name":"line","description":"Line number in the resource that generated this message (1-based).","optional":true,"type":"integer"},{"name":"column","description":"Column number in the resource that generated this message (1-based).","optional":true,"type":"integer"}]}],"commands":[{"name":"clearMessages","description":"Does nothing."},{"name":"disable","description":"Disables console domain, prevents further console messages from being reported to the client."},{"name":"enable","description":"Enables console domain, sends the messages collected so far to the client by means of the\\n`messageAdded` notification."}],"events":[{"name":"messageAdded","description":"Issued when new console message is added.","parameters":[{"name":"message","description":"Console message that has been added.","$ref":"ConsoleMessage"}]}]},{"domain":"Debugger","description":"Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\\nbreakpoints, stepping through execution, exploring stack traces, etc.","dependencies":["Runtime"],"types":[{"id":"BreakpointId","description":"Breakpoint identifier.","type":"string"},{"id":"CallFrameId","description":"Call frame identifier.","type":"string"},{"id":"Location","description":"Location in the source code.","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"}]},{"id":"ScriptPosition","description":"Location in the source code.","experimental":true,"type":"object","properties":[{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"CallFrame","description":"JavaScript call frame. Array of call frames form the call stack.","type":"object","properties":[{"name":"callFrameId","description":"Call frame identifier. This identifier is only valid while the virtual machine is paused.","$ref":"CallFrameId"},{"name":"functionName","description":"Name of the JavaScript function called on this call frame.","type":"string"},{"name":"functionLocation","description":"Location in the source code.","optional":true,"$ref":"Location"},{"name":"location","description":"Location in the source code.","$ref":"Location"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"scopeChain","description":"Scope chain for this call frame.","type":"array","items":{"$ref":"Scope"}},{"name":"this","description":"`this` object for this call frame.","$ref":"Runtime.RemoteObject"},{"name":"returnValue","description":"The value being returned, if the function is at return point.","optional":true,"$ref":"Runtime.RemoteObject"}]},{"id":"Scope","description":"Scope description.","type":"object","properties":[{"name":"type","description":"Scope type.","type":"string","enum":["global","local","with","closure","catch","block","script","eval","module"]},{"name":"object","description":"Object representing the scope. For `global` and `with` scopes it represents the actual\\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\\nvariables as its properties.","$ref":"Runtime.RemoteObject"},{"name":"name","optional":true,"type":"string"},{"name":"startLocation","description":"Location in the source code where scope starts","optional":true,"$ref":"Location"},{"name":"endLocation","description":"Location in the source code where scope ends","optional":true,"$ref":"Location"}]},{"id":"SearchMatch","description":"Search match for resource.","type":"object","properties":[{"name":"lineNumber","description":"Line number in resource content.","type":"number"},{"name":"lineContent","description":"Line with match content.","type":"string"}]},{"id":"BreakLocation","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"},{"name":"type","optional":true,"type":"string","enum":["debuggerStatement","call","return"]}]}],"commands":[{"name":"continueToLocation","description":"Continues execution until specific location is reached.","parameters":[{"name":"location","description":"Location to continue to.","$ref":"Location"},{"name":"targetCallFrames","optional":true,"type":"string","enum":["any","current"]}]},{"name":"disable","description":"Disables debugger for given page."},{"name":"enable","description":"Enables debugger for the given page. Clients should not assume that the debugging has been\\nenabled until the result for this command is received.","parameters":[{"name":"maxScriptsCacheSize","description":"The maximum size in bytes of collected scripts (not referenced by other heap objects)\\nthe debugger can hold. Puts no limit if paramter is omitted.","experimental":true,"optional":true,"type":"number"}],"returns":[{"name":"debuggerId","description":"Unique identifier of the debugger.","experimental":true,"$ref":"Runtime.UniqueDebuggerId"}]},{"name":"evaluateOnCallFrame","description":"Evaluates expression on a given call frame.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"String object group name to put result into (allows rapid releasing resulting object handles\\nusing `releaseObjectGroup`).","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the evaluated expression, defaults\\nto false.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"Runtime.TimeDelta"}],"returns":[{"name":"result","description":"Object wrapper for the evaluation result.","$ref":"Runtime.RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"getPossibleBreakpoints","description":"Returns possible locations for breakpoint. scriptId in start and end range locations should be\\nthe same.","parameters":[{"name":"start","description":"Start of range to search possible breakpoint locations in.","$ref":"Location"},{"name":"end","description":"End of range to search possible breakpoint locations in (excluding). When not specified, end\\nof scripts is used as end of range.","optional":true,"$ref":"Location"},{"name":"restrictToFunction","description":"Only consider locations which are in the same (non-nested) function as start.","optional":true,"type":"boolean"}],"returns":[{"name":"locations","description":"List of the possible breakpoint locations.","type":"array","items":{"$ref":"BreakLocation"}}]},{"name":"getScriptSource","description":"Returns source for the script with given id.","parameters":[{"name":"scriptId","description":"Id of the script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"scriptSource","description":"Script source.","type":"string"}]},{"name":"getStackTrace","description":"Returns stack trace with given `stackTraceId`.","experimental":true,"parameters":[{"name":"stackTraceId","$ref":"Runtime.StackTraceId"}],"returns":[{"name":"stackTrace","$ref":"Runtime.StackTrace"}]},{"name":"pause","description":"Stops on the next JavaScript statement."},{"name":"pauseOnAsyncCall","experimental":true,"parameters":[{"name":"parentStackTraceId","description":"Debugger will pause when async call with given stack trace is started.","$ref":"Runtime.StackTraceId"}]},{"name":"removeBreakpoint","description":"Removes JavaScript breakpoint.","parameters":[{"name":"breakpointId","$ref":"BreakpointId"}]},{"name":"restartFrame","description":"Restarts particular call frame from the beginning.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"}],"returns":[{"name":"callFrames","description":"New stack trace.","type":"array","items":{"$ref":"CallFrame"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resume","description":"Resumes JavaScript execution."},{"name":"searchInContent","description":"Searches for given string in script content.","parameters":[{"name":"scriptId","description":"Id of the script to search in.","$ref":"Runtime.ScriptId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"SearchMatch"}}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setBlackboxPatterns","description":"Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\\nperforming \'step in\' several times, finally resorting to \'step out\' if unsuccessful.","experimental":true,"parameters":[{"name":"patterns","description":"Array of regexps that will be used to check script url for blackbox state.","type":"array","items":{"type":"string"}}]},{"name":"setBlackboxedRanges","description":"Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\\nscripts by performing \'step in\' several times, finally resorting to \'step out\' if unsuccessful.\\nPositions array contains positions where blackbox state is changed. First interval isn\'t\\nblackboxed. Array should be sorted.","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script.","$ref":"Runtime.ScriptId"},{"name":"positions","type":"array","items":{"$ref":"ScriptPosition"}}]},{"name":"setBreakpoint","description":"Sets JavaScript breakpoint at a given location.","parameters":[{"name":"location","description":"Location to set breakpoint in.","$ref":"Location"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"actualLocation","description":"Location this breakpoint resolved into.","$ref":"Location"}]},{"name":"setInstrumentationBreakpoint","description":"Sets instrumentation breakpoint.","parameters":[{"name":"instrumentation","description":"Instrumentation name.","type":"string","enum":["beforeScriptExecution","beforeScriptWithSourceMapExecution"]}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointByUrl","description":"Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\\n`locations` property. Further matching script parsing will result in subsequent\\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.","parameters":[{"name":"lineNumber","description":"Line number to set breakpoint at.","type":"integer"},{"name":"url","description":"URL of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"urlRegex","description":"Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\\n`urlRegex` must be specified.","optional":true,"type":"string"},{"name":"scriptHash","description":"Script hash of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"columnNumber","description":"Offset in the line to set breakpoint at.","optional":true,"type":"integer"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"locations","description":"List of the locations this breakpoint resolved into upon addition.","type":"array","items":{"$ref":"Location"}}]},{"name":"setBreakpointOnFunctionCall","description":"Sets JavaScript breakpoint before each call to the given function.\\nIf another function was created from the same source as a given one,\\ncalling it will also trigger the breakpoint.","experimental":true,"parameters":[{"name":"objectId","description":"Function object id.","$ref":"Runtime.RemoteObjectId"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will\\nstop on the breakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointsActive","description":"Activates / deactivates all breakpoints on the page.","parameters":[{"name":"active","description":"New value for breakpoints active state.","type":"boolean"}]},{"name":"setPauseOnExceptions","description":"Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or\\nno exceptions. Initial pause on exceptions state is `none`.","parameters":[{"name":"state","description":"Pause on exceptions mode.","type":"string","enum":["none","uncaught","all"]}]},{"name":"setReturnValue","description":"Changes return value in top frame. Available only at return break position.","experimental":true,"parameters":[{"name":"newValue","description":"New return value.","$ref":"Runtime.CallArgument"}]},{"name":"setScriptSource","description":"Edits JavaScript source live.","parameters":[{"name":"scriptId","description":"Id of the script to edit.","$ref":"Runtime.ScriptId"},{"name":"scriptSource","description":"New content of the script.","type":"string"},{"name":"dryRun","description":"If true the change will not actually be applied. Dry run may be used to get result\\ndescription without actually modifying the code.","optional":true,"type":"boolean"}],"returns":[{"name":"callFrames","description":"New stack trace in case editing has happened while VM was stopped.","optional":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"stackChanged","description":"Whether current call stack was modified after applying the changes.","optional":true,"type":"boolean"},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"exceptionDetails","description":"Exception details if any.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"setSkipAllPauses","description":"Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).","parameters":[{"name":"skip","description":"New value for skip pauses state.","type":"boolean"}]},{"name":"setVariableValue","description":"Changes value of variable in a callframe. Object-based scopes are not supported and must be\\nmutated manually.","parameters":[{"name":"scopeNumber","description":"0-based number of scope as was listed in scope chain. Only \'local\', \'closure\' and \'catch\'\\nscope types are allowed. Other scopes could be manipulated manually.","type":"integer"},{"name":"variableName","description":"Variable name.","type":"string"},{"name":"newValue","description":"New variable value.","$ref":"Runtime.CallArgument"},{"name":"callFrameId","description":"Id of callframe that holds variable.","$ref":"CallFrameId"}]},{"name":"stepInto","description":"Steps into the function call.","parameters":[{"name":"breakOnAsyncCall","description":"Debugger will issue additional Debugger.paused notification if any async task is scheduled\\nbefore next pause.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"stepOut","description":"Steps out of the function call."},{"name":"stepOver","description":"Steps over the statement."}],"events":[{"name":"breakpointResolved","description":"Fired when breakpoint is resolved to an actual script and location.","parameters":[{"name":"breakpointId","description":"Breakpoint unique identifier.","$ref":"BreakpointId"},{"name":"location","description":"Actual breakpoint location.","$ref":"Location"}]},{"name":"paused","description":"Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.","parameters":[{"name":"callFrames","description":"Call stack the virtual machine stopped on.","type":"array","items":{"$ref":"CallFrame"}},{"name":"reason","description":"Pause reason.","type":"string","enum":["ambiguous","assert","debugCommand","DOM","EventListener","exception","instrumentation","OOM","other","promiseRejection","XHR"]},{"name":"data","description":"Object containing break-specific auxiliary properties.","optional":true,"type":"object"},{"name":"hitBreakpoints","description":"Hit breakpoints IDs","optional":true,"type":"array","items":{"type":"string"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"asyncCallStackTraceId","description":"Just scheduled async call will have this stack trace as parent stack during async execution.\\nThis field is available only after `Debugger.stepInto` call with `breakOnAsynCall` flag.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resumed","description":"Fired when the virtual machine resumed execution."},{"name":"scriptFailedToParse","description":"Fired when virtual machine fails to parse the script.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"scriptParsed","description":"Fired when virtual machine parses script. This event is also fired for all known and uncollected\\nscripts upon enabling debugger.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"},{"name":"isLiveEdit","description":"True, if this script is generated as a result of the live edit operation.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"}]}]},{"domain":"HeapProfiler","experimental":true,"dependencies":["Runtime"],"types":[{"id":"HeapSnapshotObjectId","description":"Heap snapshot object id.","type":"string"},{"id":"SamplingHeapProfileNode","description":"Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.","type":"object","properties":[{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"selfSize","description":"Allocations size in bytes for the node excluding children.","type":"number"},{"name":"id","description":"Node id. Ids are unique across all profiles collected between startSampling and stopSampling.","type":"integer"},{"name":"children","description":"Child nodes.","type":"array","items":{"$ref":"SamplingHeapProfileNode"}}]},{"id":"SamplingHeapProfileSample","description":"A single sample from a sampling profile.","type":"object","properties":[{"name":"size","description":"Allocation size in bytes attributed to the sample.","type":"number"},{"name":"nodeId","description":"Id of the corresponding profile tree node.","type":"integer"},{"name":"ordinal","description":"Time-ordered sample ordinal number. It is unique across all profiles retrieved\\nbetween startSampling and stopSampling.","type":"number"}]},{"id":"SamplingHeapProfile","description":"Sampling profile.","type":"object","properties":[{"name":"head","$ref":"SamplingHeapProfileNode"},{"name":"samples","type":"array","items":{"$ref":"SamplingHeapProfileSample"}}]}],"commands":[{"name":"addInspectedHeapObject","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","parameters":[{"name":"heapObjectId","description":"Heap snapshot object id to be accessible by means of $x command line API.","$ref":"HeapSnapshotObjectId"}]},{"name":"collectGarbage"},{"name":"disable"},{"name":"enable"},{"name":"getHeapObjectId","parameters":[{"name":"objectId","description":"Identifier of the object to get heap object id for.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"heapSnapshotObjectId","description":"Id of the heap snapshot object corresponding to the passed remote object id.","$ref":"HeapSnapshotObjectId"}]},{"name":"getObjectByHeapObjectId","parameters":[{"name":"objectId","$ref":"HeapSnapshotObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"Runtime.RemoteObject"}]},{"name":"getSamplingProfile","returns":[{"name":"profile","description":"Return the sampling profile being collected.","$ref":"SamplingHeapProfile"}]},{"name":"startSampling","parameters":[{"name":"samplingInterval","description":"Average sample interval in bytes. Poisson distribution is used for the intervals. The\\ndefault value is 32768 bytes.","optional":true,"type":"number"}]},{"name":"startTrackingHeapObjects","parameters":[{"name":"trackAllocations","optional":true,"type":"boolean"}]},{"name":"stopSampling","returns":[{"name":"profile","description":"Recorded sampling heap profile.","$ref":"SamplingHeapProfile"}]},{"name":"stopTrackingHeapObjects","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken\\nwhen the tracking is stopped.","optional":true,"type":"boolean"}]},{"name":"takeHeapSnapshot","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken.","optional":true,"type":"boolean"}]}],"events":[{"name":"addHeapSnapshotChunk","parameters":[{"name":"chunk","type":"string"}]},{"name":"heapStatsUpdate","description":"If heap objects tracking has been started then backend may send update for one or more fragments","parameters":[{"name":"statsUpdate","description":"An array of triplets. Each triplet describes a fragment. The first integer is the fragment\\nindex, the second integer is a total count of objects for the fragment, the third integer is\\na total size of the objects for the fragment.","type":"array","items":{"type":"integer"}}]},{"name":"lastSeenObjectId","description":"If heap objects tracking has been started then backend regularly sends a current value for last\\nseen object id and corresponding timestamp. If the were changes in the heap since last event\\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.","parameters":[{"name":"lastSeenObjectId","type":"integer"},{"name":"timestamp","type":"number"}]},{"name":"reportHeapSnapshotProgress","parameters":[{"name":"done","type":"integer"},{"name":"total","type":"integer"},{"name":"finished","optional":true,"type":"boolean"}]},{"name":"resetProfiles"}]},{"domain":"Profiler","dependencies":["Runtime","Debugger"],"types":[{"id":"ProfileNode","description":"Profile node. Holds callsite information, execution statistics and child nodes.","type":"object","properties":[{"name":"id","description":"Unique id of the node.","type":"integer"},{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"hitCount","description":"Number of samples where this node was on top of the call stack.","optional":true,"type":"integer"},{"name":"children","description":"Child node ids.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"deoptReason","description":"The reason of being not optimized. The function may be deoptimized or marked as don\'t\\noptimize.","optional":true,"type":"string"},{"name":"positionTicks","description":"An array of source position ticks.","optional":true,"type":"array","items":{"$ref":"PositionTickInfo"}}]},{"id":"Profile","description":"Profile.","type":"object","properties":[{"name":"nodes","description":"The list of profile nodes. First item is the root node.","type":"array","items":{"$ref":"ProfileNode"}},{"name":"startTime","description":"Profiling start timestamp in microseconds.","type":"number"},{"name":"endTime","description":"Profiling end timestamp in microseconds.","type":"number"},{"name":"samples","description":"Ids of samples top nodes.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"timeDeltas","description":"Time intervals between adjacent samples in microseconds. The first delta is relative to the\\nprofile startTime.","optional":true,"type":"array","items":{"type":"integer"}}]},{"id":"PositionTickInfo","description":"Specifies a number of samples attributed to a certain source position.","type":"object","properties":[{"name":"line","description":"Source line number (1-based).","type":"integer"},{"name":"ticks","description":"Number of samples attributed to the source line.","type":"integer"}]},{"id":"CoverageRange","description":"Coverage data for a source range.","type":"object","properties":[{"name":"startOffset","description":"JavaScript script source offset for the range start.","type":"integer"},{"name":"endOffset","description":"JavaScript script source offset for the range end.","type":"integer"},{"name":"count","description":"Collected execution count of the source range.","type":"integer"}]},{"id":"FunctionCoverage","description":"Coverage data for a JavaScript function.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"ranges","description":"Source ranges inside the function with coverage data.","type":"array","items":{"$ref":"CoverageRange"}},{"name":"isBlockCoverage","description":"Whether coverage data for this function has block granularity.","type":"boolean"}]},{"id":"ScriptCoverage","description":"Coverage data for a JavaScript script.","type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"functions","description":"Functions contained in the script that has coverage data.","type":"array","items":{"$ref":"FunctionCoverage"}}]},{"id":"TypeObject","description":"Describes a type collected during runtime.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name of a type collected with type profiling.","type":"string"}]},{"id":"TypeProfileEntry","description":"Source offset and types for a parameter or return value.","experimental":true,"type":"object","properties":[{"name":"offset","description":"Source offset of the parameter or end of function for return values.","type":"integer"},{"name":"types","description":"The types for this parameter or return value.","type":"array","items":{"$ref":"TypeObject"}}]},{"id":"ScriptTypeProfile","description":"Type profile data collected during runtime for a JavaScript script.","experimental":true,"type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"entries","description":"Type profile entries for parameters and return values of the functions in the script.","type":"array","items":{"$ref":"TypeProfileEntry"}}]}],"commands":[{"name":"disable"},{"name":"enable"},{"name":"getBestEffortCoverage","description":"Collect coverage data for the current isolate. The coverage data may be incomplete due to\\ngarbage collection.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"setSamplingInterval","description":"Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.","parameters":[{"name":"interval","description":"New sampling interval in microseconds.","type":"integer"}]},{"name":"start"},{"name":"startPreciseCoverage","description":"Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\\ncounters.","parameters":[{"name":"callCount","description":"Collect accurate call counts beyond simple \'covered\' or \'not covered\'.","optional":true,"type":"boolean"},{"name":"detailed","description":"Collect block-based coverage.","optional":true,"type":"boolean"}]},{"name":"startTypeProfile","description":"Enable type profile.","experimental":true},{"name":"stop","returns":[{"name":"profile","description":"Recorded profile.","$ref":"Profile"}]},{"name":"stopPreciseCoverage","description":"Disable precise code coverage. Disabling releases unnecessary execution count records and allows\\nexecuting optimized code."},{"name":"stopTypeProfile","description":"Disable type profile. Disabling releases type profile data collected so far.","experimental":true},{"name":"takePreciseCoverage","description":"Collect coverage data for the current isolate, and resets execution counters. Precise code\\ncoverage needs to have started.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"takeTypeProfile","description":"Collect type profile.","experimental":true,"returns":[{"name":"result","description":"Type profile for all scripts since startTypeProfile() was turned on.","type":"array","items":{"$ref":"ScriptTypeProfile"}}]}],"events":[{"name":"consoleProfileFinished","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profileEnd().","$ref":"Debugger.Location"},{"name":"profile","$ref":"Profile"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"consoleProfileStarted","description":"Sent when new profile recording is started using console.profile() call.","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profile().","$ref":"Debugger.Location"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]}]},{"domain":"Runtime","description":"Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\\nEvaluation results are returned as mirror object that expose object type, string representation\\nand unique identifier that can be used for further object reference. Original objects are\\nmaintained in memory unless they are either explicitly released or are released along with the\\nother objects in their object group.","types":[{"id":"ScriptId","description":"Unique script identifier.","type":"string"},{"id":"RemoteObjectId","description":"Unique object identifier.","type":"string"},{"id":"UnserializableValue","description":"Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\\n`-Infinity`, and bigint literals.","type":"string"},{"id":"RemoteObject","description":"Mirror object referencing original JavaScript object.","type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview"]},{"name":"className","description":"Object class (constructor) name. Specified for `object` type values only.","optional":true,"type":"string"},{"name":"value","description":"Remote object value in case of primitive values or JSON values (if it was requested).","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified does not have `value`, but gets this\\nproperty.","optional":true,"$ref":"UnserializableValue"},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"objectId","description":"Unique object identifier (for non-primitive values).","optional":true,"$ref":"RemoteObjectId"},{"name":"preview","description":"Preview containing abbreviated property values. Specified for `object` type values only.","experimental":true,"optional":true,"$ref":"ObjectPreview"},{"name":"customPreview","experimental":true,"optional":true,"$ref":"CustomPreview"}]},{"id":"CustomPreview","experimental":true,"type":"object","properties":[{"name":"header","description":"The JSON-stringified result of formatter.header(object, config) call.\\nIt contains json ML array that represents RemoteObject.","type":"string"},{"name":"bodyGetterId","description":"If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\\nThe result value is json ML array.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ObjectPreview","description":"Object containing abbreviated remote object value.","experimental":true,"type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error"]},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"overflow","description":"True iff some of the properties or entries of the original object did not fit.","type":"boolean"},{"name":"properties","description":"List of the properties.","type":"array","items":{"$ref":"PropertyPreview"}},{"name":"entries","description":"List of the entries. Specified for `map` and `set` subtype values only.","optional":true,"type":"array","items":{"$ref":"EntryPreview"}}]},{"id":"PropertyPreview","experimental":true,"type":"object","properties":[{"name":"name","description":"Property name.","type":"string"},{"name":"type","description":"Object type. Accessor means that the property itself is an accessor property.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","accessor","bigint"]},{"name":"value","description":"User-friendly property value string.","optional":true,"type":"string"},{"name":"valuePreview","description":"Nested value preview.","optional":true,"$ref":"ObjectPreview"},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error"]}]},{"id":"EntryPreview","experimental":true,"type":"object","properties":[{"name":"key","description":"Preview of the key. Specified for map-like collection entries.","optional":true,"$ref":"ObjectPreview"},{"name":"value","description":"Preview of the value.","$ref":"ObjectPreview"}]},{"id":"PropertyDescriptor","description":"Object property descriptor.","type":"object","properties":[{"name":"name","description":"Property name or symbol description.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"},{"name":"writable","description":"True if the value associated with the property may be changed (data descriptors only).","optional":true,"type":"boolean"},{"name":"get","description":"A function which serves as a getter for the property, or `undefined` if there is no getter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the property, or `undefined` if there is no setter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"configurable","description":"True if the type of this property descriptor may be changed and if the property may be\\ndeleted from the corresponding object.","type":"boolean"},{"name":"enumerable","description":"True if this property shows up during enumeration of the properties on the corresponding\\nobject.","type":"boolean"},{"name":"wasThrown","description":"True if the result was thrown during the evaluation.","optional":true,"type":"boolean"},{"name":"isOwn","description":"True if the property is owned for the object.","optional":true,"type":"boolean"},{"name":"symbol","description":"Property symbol object, if the property is of the `symbol` type.","optional":true,"$ref":"RemoteObject"}]},{"id":"InternalPropertyDescriptor","description":"Object internal property descriptor. This property isn\'t normally visible in JavaScript code.","type":"object","properties":[{"name":"name","description":"Conventional property name.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"}]},{"id":"PrivatePropertyDescriptor","description":"Object private field descriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Private property name.","type":"string"},{"name":"value","description":"The value associated with the private property.","$ref":"RemoteObject"}]},{"id":"CallArgument","description":"Represents function call argument. Either remote object id `objectId`, primitive `value`,\\nunserializable primitive value or neither of (for undefined) them should be specified.","type":"object","properties":[{"name":"value","description":"Primitive value or serializable javascript object.","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified.","optional":true,"$ref":"UnserializableValue"},{"name":"objectId","description":"Remote object handle.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ExecutionContextId","description":"Id of an execution context.","type":"integer"},{"id":"ExecutionContextDescription","description":"Description of an isolated world.","type":"object","properties":[{"name":"id","description":"Unique id of the execution context. It can be used to specify in which execution context\\nscript evaluation should be performed.","$ref":"ExecutionContextId"},{"name":"origin","description":"Execution context origin.","type":"string"},{"name":"name","description":"Human readable name describing given context.","type":"string"},{"name":"auxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"}]},{"id":"ExceptionDetails","description":"Detailed information about exception (or error) that was thrown during script compilation or\\nexecution.","type":"object","properties":[{"name":"exceptionId","description":"Exception id.","type":"integer"},{"name":"text","description":"Exception text, which should be used together with exception object when available.","type":"string"},{"name":"lineNumber","description":"Line number of the exception location (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number of the exception location (0-based).","type":"integer"},{"name":"scriptId","description":"Script ID of the exception location.","optional":true,"$ref":"ScriptId"},{"name":"url","description":"URL of the exception location, to be used when the script was not reported.","optional":true,"type":"string"},{"name":"stackTrace","description":"JavaScript stack trace if available.","optional":true,"$ref":"StackTrace"},{"name":"exception","description":"Exception object if available.","optional":true,"$ref":"RemoteObject"},{"name":"executionContextId","description":"Identifier of the context where exception happened.","optional":true,"$ref":"ExecutionContextId"}]},{"id":"Timestamp","description":"Number of milliseconds since epoch.","type":"number"},{"id":"TimeDelta","description":"Number of milliseconds.","type":"number"},{"id":"CallFrame","description":"Stack entry for runtime errors and assertions.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"scriptId","description":"JavaScript script id.","$ref":"ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"lineNumber","description":"JavaScript script line number (0-based).","type":"integer"},{"name":"columnNumber","description":"JavaScript script column number (0-based).","type":"integer"}]},{"id":"StackTrace","description":"Call frames for assertions or error messages.","type":"object","properties":[{"name":"description","description":"String label of this stack trace. For async traces this may be a name of the function that\\ninitiated the async call.","optional":true,"type":"string"},{"name":"callFrames","description":"JavaScript function name.","type":"array","items":{"$ref":"CallFrame"}},{"name":"parent","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","optional":true,"$ref":"StackTrace"},{"name":"parentId","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","experimental":true,"optional":true,"$ref":"StackTraceId"}]},{"id":"UniqueDebuggerId","description":"Unique identifier of current debugger.","experimental":true,"type":"string"},{"id":"StackTraceId","description":"If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.","experimental":true,"type":"object","properties":[{"name":"id","type":"string"},{"name":"debuggerId","optional":true,"$ref":"UniqueDebuggerId"}]}],"commands":[{"name":"awaitPromise","description":"Add handler to promise with given promise object id.","parameters":[{"name":"promiseObjectId","description":"Identifier of the promise.","$ref":"RemoteObjectId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Promise result. Will contain rejected value if promise was rejected.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details if stack strace is available.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"callFunctionOn","description":"Calls function with given declaration on the given object. Object group of the result is\\ninherited from the target object.","parameters":[{"name":"functionDeclaration","description":"Declaration of the function to call.","type":"string"},{"name":"objectId","description":"Identifier of the object to call function on. Either objectId or executionContextId should\\nbe specified.","optional":true,"$ref":"RemoteObjectId"},{"name":"arguments","description":"Call arguments. All call arguments must belong to the same JavaScript world as the target\\nobject.","optional":true,"type":"array","items":{"$ref":"CallArgument"}},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"executionContextId","description":"Specifies execution context which global object will be used to call function on. Either\\nexecutionContextId or objectId should be specified.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects. If objectGroup is not\\nspecified and objectId is, objectGroup will be inherited from object.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Call result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"compileScript","description":"Compiles expression.","parameters":[{"name":"expression","description":"Expression to compile.","type":"string"},{"name":"sourceURL","description":"Source url to be set for the script.","type":"string"},{"name":"persistScript","description":"Specifies whether the compiled script should be persisted.","type":"boolean"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"scriptId","description":"Id of the script.","optional":true,"$ref":"ScriptId"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"disable","description":"Disables reporting of execution contexts creation."},{"name":"discardConsoleEntries","description":"Discards collected exceptions and console API calls."},{"name":"enable","description":"Enables reporting of execution contexts creation by means of `executionContextCreated` event.\\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\\ncontext."},{"name":"evaluate","description":"Evaluates expression on global object.","parameters":[{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"contextId","description":"Specifies in which execution context to perform evaluation. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","experimental":true,"optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"TimeDelta"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"getIsolateId","description":"Returns the isolate id.","experimental":true,"returns":[{"name":"id","description":"The isolate id.","type":"string"}]},{"name":"getHeapUsage","description":"Returns the JavaScript heap usage.\\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.","experimental":true,"returns":[{"name":"usedSize","description":"Used heap size in bytes.","type":"number"},{"name":"totalSize","description":"Allocated heap size in bytes.","type":"number"}]},{"name":"getProperties","description":"Returns properties of a given object. Object group of the result is inherited from the target\\nobject.","parameters":[{"name":"objectId","description":"Identifier of the object to return properties for.","$ref":"RemoteObjectId"},{"name":"ownProperties","description":"If true, returns properties belonging only to the element itself, not to its prototype\\nchain.","optional":true,"type":"boolean"},{"name":"accessorPropertiesOnly","description":"If true, returns accessor properties (with getter/setter) only; internal properties are not\\nreturned either.","experimental":true,"optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the results.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Object properties.","type":"array","items":{"$ref":"PropertyDescriptor"}},{"name":"internalProperties","description":"Internal object properties (only of the element itself).","optional":true,"type":"array","items":{"$ref":"InternalPropertyDescriptor"}},{"name":"privateProperties","description":"Object private properties.","experimental":true,"optional":true,"type":"array","items":{"$ref":"PrivatePropertyDescriptor"}},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"globalLexicalScopeNames","description":"Returns all let, const and class variables from global scope.","parameters":[{"name":"executionContextId","description":"Specifies in which execution context to lookup global scope variables.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"names","type":"array","items":{"type":"string"}}]},{"name":"queryObjects","parameters":[{"name":"prototypeObjectId","description":"Identifier of the prototype to return objects for.","$ref":"RemoteObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release the results.","optional":true,"type":"string"}],"returns":[{"name":"objects","description":"Array with objects.","$ref":"RemoteObject"}]},{"name":"releaseObject","description":"Releases remote object with given id.","parameters":[{"name":"objectId","description":"Identifier of the object to release.","$ref":"RemoteObjectId"}]},{"name":"releaseObjectGroup","description":"Releases all remote objects that belong to a given group.","parameters":[{"name":"objectGroup","description":"Symbolic object group name.","type":"string"}]},{"name":"runIfWaitingForDebugger","description":"Tells inspected instance to run if it was waiting for debugger to attach."},{"name":"runScript","description":"Runs script with given id in a given context.","parameters":[{"name":"scriptId","description":"Id of the script to run.","$ref":"ScriptId"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Run result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","redirect":"Debugger","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setCustomObjectFormatterEnabled","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setMaxCallStackSizeToCapture","experimental":true,"parameters":[{"name":"size","type":"integer"}]},{"name":"terminateExecution","description":"Terminate current or next JavaScript execution.\\nWill cancel the termination when the outer-most script execution ends.","experimental":true},{"name":"addBinding","description":"If executionContextId is empty, adds binding with the given name on the\\nglobal objects of all inspected contexts, including those created later,\\nbindings survive reloads.\\nIf executionContextId is specified, adds binding only on global object of\\ngiven execution context.\\nBinding function takes exactly one argument, this argument should be string,\\nin case of any other input, function throws an exception.\\nEach binding function call produces Runtime.bindingCalled notification.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"executionContextId","optional":true,"$ref":"ExecutionContextId"}]},{"name":"removeBinding","description":"This method does not remove binding function from global object but\\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.","experimental":true,"parameters":[{"name":"name","type":"string"}]}],"events":[{"name":"bindingCalled","description":"Notification is issued every time when binding is called.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"payload","type":"string"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"}]},{"name":"consoleAPICalled","description":"Issued when console API was called.","parameters":[{"name":"type","description":"Type of the call.","type":"string","enum":["log","debug","info","error","warning","dir","dirxml","table","trace","clear","startGroup","startGroupCollapsed","endGroup","assert","profile","profileEnd","count","timeEnd"]},{"name":"args","description":"Call arguments.","type":"array","items":{"$ref":"RemoteObject"}},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"},{"name":"timestamp","description":"Call timestamp.","$ref":"Timestamp"},{"name":"stackTrace","description":"Stack trace captured when the call was made. The async stack chain is automatically reported for\\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.","optional":true,"$ref":"StackTrace"},{"name":"context","description":"Console context descriptor for calls on non-default console context (not console.*):\\n\'anonymous#unique-logger-id\' for call on unnamed context, \'name#unique-logger-id\' for call\\non named context.","experimental":true,"optional":true,"type":"string"}]},{"name":"exceptionRevoked","description":"Issued when unhandled exception was revoked.","parameters":[{"name":"reason","description":"Reason describing why exception was revoked.","type":"string"},{"name":"exceptionId","description":"The id of revoked exception, as reported in `exceptionThrown`.","type":"integer"}]},{"name":"exceptionThrown","description":"Issued when exception was thrown and unhandled.","parameters":[{"name":"timestamp","description":"Timestamp of the exception.","$ref":"Timestamp"},{"name":"exceptionDetails","$ref":"ExceptionDetails"}]},{"name":"executionContextCreated","description":"Issued when new execution context is created.","parameters":[{"name":"context","description":"A newly created execution context.","$ref":"ExecutionContextDescription"}]},{"name":"executionContextDestroyed","description":"Issued when execution context is destroyed.","parameters":[{"name":"executionContextId","description":"Id of the destroyed context","$ref":"ExecutionContextId"}]},{"name":"executionContextsCleared","description":"Issued when all executionContexts were cleared in browser"},{"name":"inspectRequested","description":"Issued when object should be inspected (for example, as a result of inspect() command line API\\ncall).","parameters":[{"name":"object","$ref":"RemoteObject"},{"name":"hints","type":"object"}]}]},{"domain":"Schema","description":"This domain is deprecated.","deprecated":true,"types":[{"id":"Domain","description":"Description of the protocol domain.","type":"object","properties":[{"name":"name","description":"Domain name.","type":"string"},{"name":"version","description":"Domain version.","type":"string"}]}],"commands":[{"name":"getDomains","description":"Returns supported domains.","returns":[{"name":"domains","description":"List of supported domains.","type":"array","items":{"$ref":"Domain"}}]}]}]}')},function(e,t,n){"use strict";(function(t){const r=n(56),i=n(379),o=n(75).format,a=n(75).parse,s=n(381),c=n(382),p=n(151),d=n(139);class u extends Error{constructor(e,t){let{message:n}=t;t.data&&(n+=` (${t.data})`),super(n),this.request=e,this.response=t}}e.exports=class extends r{constructor(e,t){super(),e=e||{},this.host=e.host||p.HOST,this.port=e.port||p.PORT,this.secure=!!e.secure,this.useHostName=!!e.useHostName,this.alterPath=e.alterPath||(e=>e),this.protocol=e.protocol,this.local=!!e.local,this.target=e.target||(e=>{let t,n=e.find(e=>!!e.webSocketDebuggerUrl&&(t=t||e,"page"===e.type));if(n=n||t)return n;throw new Error("No inspectable targets")}),this._notifier=t,this._callbacks={},this._nextCommandId=1,this.webSocketUrl=void 0,this._start()}inspect(e,t){return t.customInspect=!1,i.inspect(this,t)}send(e,t,n){return"function"==typeof t&&(n=t,t=void 0),"function"==typeof n?void this._enqueueCommand(e,t,n):new Promise((n,r)=>{this._enqueueCommand(e,t,(i,o)=>{if(i){const n={method:e,params:t};r(i instanceof Error?i:new u(n,o))}else n(o)})})}close(e){const t=e=>{3===this._ws.readyState?e():(this._ws.removeAllListeners("close"),this._ws.once("close",()=>{this._ws.removeAllListeners(),e()}),this._ws.close())};return"function"==typeof e?void t(e):new Promise((e,n)=>{t(e)})}async _start(){const e={host:this.host,port:this.port,secure:this.secure,useHostName:this.useHostName,alterPath:this.alterPath};try{const n=await this._fetchDebuggerURL(e),r=a(n);r.pathname=e.alterPath(r.pathname),this.webSocketUrl=o(r),e.host=r.hostname,e.port=r.port||e.port;const i=await this._fetchProtocol(e);c.prepare(this,i),await this._connectToWebSocket(),t.nextTick(()=>{this._notifier.emit("connect",this)})}catch(e){this._notifier.emit("error",e)}}async _fetchDebuggerURL(e){const t=this.target;switch(typeof t){case"string":{let n=t;return n.startsWith("/")&&(n=`ws://${this.host}:${this.port}${n}`),n.match(/^wss?:/i)?n:(await d.List(e)).find(e=>e.id===n).webSocketDebuggerUrl}case"object":return t.webSocketDebuggerUrl;case"function":{const n=t,r=await d.List(e),i=n(r);return("number"==typeof i?r[i]:i).webSocketDebuggerUrl}default:throw new Error(`Invalid target argument "${this.target}"`)}}async _fetchProtocol(e){return this.protocol?this.protocol:(e.local=this.local,await d.Protocol(e))}_connectToWebSocket(){return new Promise((e,t)=>{try{this.secure&&(this.webSocketUrl=this.webSocketUrl.replace(/^ws:/i,"wss:")),this._ws=new s(this.webSocketUrl)}catch(e){return void t(e)}this._ws.on("open",()=>{e()}),this._ws.on("message",e=>{const t=JSON.parse(e);this._handleMessage(t)}),this._ws.on("close",e=>{this.emit("disconnect")}),this._ws.on("error",e=>{t(e)})})}_handleMessage(e){if(e.id){const t=this._callbacks[e.id];if(!t)return;e.error?t(!0,e.error):t(!1,e.result||{}),delete this._callbacks[e.id],0===Object.keys(this._callbacks).length&&this.emit("ready")}else e.method&&(this.emit("event",e),this.emit(e.method,e.params))}_enqueueCommand(e,t,n){const r=this._nextCommandId++,i={id:r,method:e,params:t||{}};this._ws.send(JSON.stringify(i),e=>{e?"function"==typeof n&&n(e):this._callbacks[r]=n})}}}).call(this,n(30))},function(e,t,n){(function(e){var r=Object.getOwnPropertyDescriptors||function(e){for(var t=Object.keys(e),n={},r=0;r<t.length;r++)n[t[r]]=Object.getOwnPropertyDescriptor(e,t[r]);return n},i=/%[sdj%]/g;t.format=function(e){if(!y(e)){for(var t=[],n=0;n<arguments.length;n++)t.push(s(arguments[n]));return t.join(" ")}n=1;for(var r=arguments,o=r.length,a=String(e).replace(i,function(e){if("%%"===e)return"%";if(n>=o)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n<o;c=r[++n])h(c)||!w(c)?a+=" "+c:a+=" "+s(c);return a},t.deprecate=function(n,r){if(void 0!==e&&!0===e.noDeprecation)return n;if(void 0===e)return function(){return t.deprecate(n,r).apply(this,arguments)};var i=!1;return function(){if(!i){if(e.throwDeprecation)throw new Error(r);e.traceDeprecation?console.trace(r):console.error(r),i=!0}return n.apply(this,arguments)}};var o,a={};function s(e,n){var r={seen:[],stylize:p};return arguments.length>=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),f(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),d(r,e,r.depth)}function c(e,t){var n=s.styles[t];return n?"["+s.colors[n][0]+"m"+e+"["+s.colors[n][1]+"m":e}function p(e,t){return e}function d(e,n,r){if(e.customInspect&&n&&I(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var i=n.inspect(r,e);return y(i)||(i=d(e,i,r)),i}var o=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(y(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(g(t))return e.stylize(""+t,"number");if(f(t))return e.stylize(""+t,"boolean");if(h(t))return e.stylize("null","null")}(e,n);if(o)return o;var a=Object.keys(n),s=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(n)),x(n)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return u(n);if(0===a.length){if(I(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(v(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(S(n))return e.stylize(Date.prototype.toString.call(n),"date");if(x(n))return u(n)}var p,w="",T=!1,R=["{","}"];(m(n)&&(T=!0,R=["[","]"]),I(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return v(n)&&(w=" "+RegExp.prototype.toString.call(n)),S(n)&&(w=" "+Date.prototype.toUTCString.call(n)),x(n)&&(w=" "+u(n)),0!==a.length||T&&0!=n.length?r<0?v(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),p=T?function(e,t,n,r,i){for(var o=[],a=0,s=t.length;a<s;++a)O(t,String(a))?o.push(l(e,t,n,r,String(a),!0)):o.push("");return i.forEach(function(i){i.match(/^\d+$/)||o.push(l(e,t,n,r,i,!0))}),o}(e,n,r,s,a):a.map(function(t){return l(e,n,r,s,t,T)}),e.seen.pop(),function(e,t,n){if(e.reduce(function(e,t){return 0,t.indexOf("\n")>=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(p,w,R)):R[0]+w+R[1]}function u(e){return"["+Error.prototype.toString.call(e)+"]"}function l(e,t,n,r,i,o){var a,s,c;if((c=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]}).get?s=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(s=e.stylize("[Setter]","special")),O(r,i)||(a="["+i+"]"),s||(e.seen.indexOf(c.value)<0?(s=h(n)?d(e,c.value,null):d(e,c.value,n-1)).indexOf("\n")>-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n")):s=e.stylize("[Circular]","special")),b(a)){if(o&&i.match(/^\d+$/))return s;(a=JSON.stringify(""+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function m(e){return Array.isArray(e)}function f(e){return"boolean"==typeof e}function h(e){return null===e}function g(e){return"number"==typeof e}function y(e){return"string"==typeof e}function b(e){return void 0===e}function v(e){return w(e)&&"[object RegExp]"===T(e)}function w(e){return"object"==typeof e&&null!==e}function S(e){return w(e)&&"[object Date]"===T(e)}function x(e){return w(e)&&("[object Error]"===T(e)||e instanceof Error)}function I(e){return"function"==typeof e}function T(e){return Object.prototype.toString.call(e)}function R(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(o)&&(o=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!a[n])if(new RegExp("\\b"+n+"\\b","i").test(o)){var r=e.pid;a[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else a[n]=function(){};return a[n]},t.inspect=s,s.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},s.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=m,t.isBoolean=f,t.isNull=h,t.isNullOrUndefined=function(e){return null==e},t.isNumber=g,t.isString=y,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=v,t.isObject=w,t.isDate=S,t.isError=x,t.isFunction=I,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(380);var k=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function C(){var e=new Date,t=[R(e.getHours()),R(e.getMinutes()),R(e.getSeconds())].join(":");return[e.getDate(),k[e.getMonth()],t].join(" ")}function O(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){console.log("%s - %s",C(),t.format.apply(t,arguments))},t.inherits=n(34),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var $="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function E(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if($&&e[$]){var t;if("function"!=typeof(t=e[$]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,$,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),i=[],o=0;o<arguments.length;o++)i.push(arguments[o]);i.push(function(e,r){e?n(e):t(r)});try{e.apply(this,i)}catch(e){n(e)}return r}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),$&&Object.defineProperty(t,$,{value:t,enumerable:!1,writable:!1,configurable:!0}),Object.defineProperties(t,r(e))},t.promisify.custom=$,t.callbackify=function(t){if("function"!=typeof t)throw new TypeError('The "original" argument must be of type Function');function n(){for(var n=[],r=0;r<arguments.length;r++)n.push(arguments[r]);var i=n.pop();if("function"!=typeof i)throw new TypeError("The last argument must be of type Function");var o=this,a=function(){return i.apply(o,arguments)};t.apply(this,n).then(function(t){e.nextTick(a,null,t)},function(t){e.nextTick(E,t,a)})}return Object.setPrototypeOf(n,Object.getPrototypeOf(t)),Object.defineProperties(n,r(t)),n}}).call(this,n(30))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t,n){"use strict";const r=n(56);e.exports=class extends r{constructor(e){super(),this._ws=new WebSocket(e),this._ws.onopen=()=>{this.emit("open")},this._ws.onclose=()=>{this.emit("close")},this._ws.onmessage=e=>{this.emit("message",e.data)},this._ws.onerror=()=>{this.emit("error",new Error("WebSocket error"))}}close(){this._ws.close()}send(e,t){try{this._ws.send(e),t()}catch(e){t(e)}}}},function(e,t,n){"use strict";function r(e,t,n){e.category=t,Object.keys(n).forEach(r=>{"name"!==r&&(e[r]="type"===t&&"properties"===r||"parameters"===r?function(e){const t={};return e.forEach(e=>{const n=e.name;delete e.name,t[n]=e}),t}(n[r]):n[r])})}e.exports.prepare=function(e,t){e.protocol=t,t.domains.forEach(t=>{const n=t.domain;e[n]={},(t.commands||[]).forEach(t=>{!function(e,t,n){const i=(r,i)=>e.send(`${t}.${n.name}`,r,i);r(i,"command",n),e[t][n.name]=i}(e,n,t)}),(t.events||[]).forEach(t=>{!function(e,t,n){const i=`${t}.${n.name}`,o=t=>"function"==typeof t?(e.on(i,t),()=>e.removeListener(i,t)):new Promise((t,n)=>{e.once(i,t)});r(o,"event",n),e[t][n.name]=o}(e,n,t)}),(t.types||[]).forEach(t=>{!function(e,t,n){const i={};r(i,"type",n),e[t][n.id]=i}(e,n,t)})})}}]);
\ No newline at end of file diff --git a/remote/cdp/test/browser/dom/browser.toml b/remote/cdp/test/browser/dom/browser.toml new file mode 100644 index 0000000000..0815ae681f --- /dev/null +++ b/remote/cdp/test/browser/dom/browser.toml @@ -0,0 +1,23 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_describeNode.js"] + +["browser_resolveNode.js"] diff --git a/remote/cdp/test/browser/dom/browser_describeNode.js b/remote/cdp/test/browser/dom/browser_describeNode.js new file mode 100644 index 0000000000..8db9a85685 --- /dev/null +++ b/remote/cdp/test/browser/dom/browser_describeNode.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div id='content'><p>foo</p><p>bar</p></div>"); +const DOC_FRAME = toDataURL(`<iframe src="${DOC}"></iframe>`); + +add_task(async function objectIdInvalidTypes({ client }) { + const { DOM } = client; + + for (const objectId of [null, true, 1, [], {}]) { + await Assert.rejects( + DOM.describeNode({ objectId }), + /objectId: string value expected/, + `Fails with invalid type: ${objectId}` + ); + } +}); + +add_task(async function objectIdUnknownValue({ client }) { + const { DOM } = client; + + await Assert.rejects( + DOM.describeNode({ objectId: "foo" }), + /Could not find object with given id/, + `Fails with unknown objectId` + ); +}); + +add_task(async function objectIdIsNotANode({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "[42]", + }); + + await Assert.rejects( + DOM.describeNode({ objectId: result.objectId }), + /Object id doesn't reference a Node/, + `Fails if objectId doesn't reference a DOM node` + ); +}); + +add_task(async function objectIdAllProperties({ client }) { + const { DOM, Page, Runtime } = client; + + await Page.enable(); + const { frameId } = await Page.navigate({ url: DOC }); + await Page.loadEventFired(); + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: `document.getElementById('content')`, + }); + const { node } = await DOM.describeNode({ + objectId: result.objectId, + }); + + ok(!!node.nodeId, "The node has a node id"); + ok(!!node.backendNodeId, "The node has a backend node id"); + is(node.nodeName, "DIV", "Found expected node name"); + is(node.localName, "div", "Found expected local name"); + is(node.nodeType, 1, "Found expected node type"); + is(node.nodeValue, "", "Found expected node value"); + is(node.childNodeCount, 2, "Expected number of child nodes found"); + is(node.attributes.length, 2, "Found expected attribute's name and value"); + is(node.attributes[0], "id", "Found expected attribute name"); + is(node.attributes[1], "content", "Found expected attribute value"); + is(node.frameId, frameId, "Found expected frame id"); +}); + +add_task(async function objectIdNoAttributes({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "document", + }); + const { node } = await DOM.describeNode({ + objectId: result.objectId, + }); + + is(node.attributes, undefined, "No attributes returned"); +}); + +add_task(async function objectIdDiffersForDifferentNodes({ client }) { + const { DOM, Runtime } = client; + + await loadURL(DOC); + + await Runtime.enable(); + const { result: doc } = await Runtime.evaluate({ + expression: "document", + }); + const { node: node1 } = await DOM.describeNode({ + objectId: doc.objectId, + }); + + const { result: body } = await Runtime.evaluate({ + expression: `document.getElementById('content')`, + }); + const { node: node2 } = await DOM.describeNode({ + objectId: body.objectId, + }); + + for (const prop in node1) { + if (["nodeValue", "frameId"].includes(prop)) { + is(node1[prop], node2[prop], `Values of ${prop} are equal`); + } else { + isnot(node1[prop], node2[prop], `Values of ${prop} are different`); + } + } +}); + +add_task(async function objectIdDoesNotChangeForTheSameNode({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "document", + }); + const { node: node1 } = await DOM.describeNode({ + objectId: result.objectId, + }); + const { node: node2 } = await DOM.describeNode({ + objectId: result.objectId, + }); + + for (const prop in node1) { + is(node1[prop], node2[prop], `Values of ${prop} are equal`); + } +}); + +add_task(async function frameIdForFrameElement({ client }) { + const { DOM, Page, Runtime } = client; + + await Page.enable(); + + const frameAttached = Page.frameAttached(); + await loadURL(DOC_FRAME); + const { frameId, parentFrameId } = await frameAttached; + + await Runtime.enable(); + + const { result: frameObj } = await Runtime.evaluate({ + expression: "document.getElementsByTagName('iframe')[0]", + }); + const { node: frame } = await DOM.describeNode({ + objectId: frameObj.objectId, + }); + + is(frame.frameId, frameId, "Reported frameId is from the frame itself"); + isnot( + frame.frameId, + parentFrameId, + "Reported frameId is not the parentFrameId" + ); +}); diff --git a/remote/cdp/test/browser/dom/browser_resolveNode.js b/remote/cdp/test/browser/dom/browser_resolveNode.js new file mode 100644 index 0000000000..759c4ee76c --- /dev/null +++ b/remote/cdp/test/browser/dom/browser_resolveNode.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function backendNodeIdInvalidTypes({ client }) { + const { DOM } = client; + + for (const backendNodeId of [null, true, "foo", [], {}]) { + await Assert.rejects( + DOM.resolveNode({ backendNodeId }), + /backendNodeId: number value expected/, + `Fails for invalid type: ${backendNodeId}` + ); + } +}); + +add_task(async function backendNodeIdInvalidValue({ client }) { + const { DOM } = client; + + await Assert.rejects( + DOM.resolveNode({ backendNodeId: -1 }), + /No node with given id found/, + "Fails for unknown backendNodeId" + ); +}); + +add_task(async function backendNodeIdResultProperties({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + const { object } = await DOM.resolveNode({ + backendNodeId: node.backendNodeId, + }); + + ok(!!object, "Javascript node object returned"); + is(object.type, result.type, "Expected type returned"); + is(object.subtype, result.subtype, "Expected subtype returned"); + isnot(object.objectId, result.objectId, "Object has been duplicated"); +}); + +add_task(async function executionContextIdInvalidTypes({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + for (const executionContextId of [null, true, "foo", [], {}]) { + await Assert.rejects( + DOM.resolveNode({ + backendNodeId: node.backendNodeId, + executionContextId, + }), + /executionContextId: integer value expected/, + `Fails for invalid type: ${executionContextId}` + ); + } +}); + +add_task(async function executionContextIdInvalidValue({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + await Assert.rejects( + DOM.resolveNode({ + backendNodeId: node.backendNodeId, + executionContextId: -1, + }), + /Node with given id does not belong to the document/, + "Fails for unknown executionContextId" + ); +}); diff --git a/remote/cdp/test/browser/dom/head.js b/remote/cdp/test/browser/dom/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/dom/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/emulation/browser.toml b/remote/cdp/test/browser/emulation/browser.toml new file mode 100644 index 0000000000..e5051abace --- /dev/null +++ b/remote/cdp/test/browser/emulation/browser.toml @@ -0,0 +1,25 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_setDeviceMetricsOverride.js"] + +["browser_setTouchEmulationEnabled.js"] + +["browser_setUserAgentOverride.js"] diff --git a/remote/cdp/test/browser/emulation/browser_setDeviceMetricsOverride.js b/remote/cdp/test/browser/emulation/browser_setDeviceMetricsOverride.js new file mode 100644 index 0000000000..eb8c314161 --- /dev/null +++ b/remote/cdp/test/browser/emulation/browser_setDeviceMetricsOverride.js @@ -0,0 +1,403 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC_SMALL = toDataURL("<div>Hello world"); +const DOC_LARGE = toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world"); + +const MAX_WINDOW_SIZE = 10000000; + +function getContentDPR() { + info(`Retrieve device pixel ratio in content`); + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + _ => content.browsingContext.overrideDPPX || content.devicePixelRatio + ); +} + +add_task(async function dimensionsSmallerThanWindow({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height, + "Expected layout viewport height set" + ); + is( + updatedLayoutMetrics.contentSize.width, + overrideSettings.width, + "Expected content size width set" + ); + is( + updatedLayoutMetrics.contentSize.height, + overrideSettings.height, + "Expected content size height set" + ); +}); + +add_task(async function dimensionsLargerThanWindow({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_LARGE); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: layoutViewport.clientWidth * 2, + height: layoutViewport.clientHeight * 2, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_LARGE); + + const scrollbarSize = await getScrollbarSize(); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width - scrollbarSize.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height - scrollbarSize.height, + "Expected layout viewport height set" + ); +}); + +add_task(async function noSizeChangeForSameWidthAndHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: layoutViewport.clientWidth, + height: layoutViewport.clientHeight, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function noWidthChangeWithZeroWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: 0, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height, + "Expected layout viewport height set" + ); +}); + +add_task(async function noHeightChangeWithZeroHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: 0, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function nosizeChangeWithZeroWidthAndHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: 0, + height: 0, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function failsWithNegativeWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentDPR(); + + const overrideSettings = { + width: -1, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Assert.rejects( + Emulation.setDeviceMetricsOverride(overrideSettings), + err => + err.message.includes( + "Width and height values must be positive, not greater than 10000000" + ), + "Negative width raised error" + ); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is(await getContentDPR(), ratio, "Device pixel ratio hasn't been changed"); +}); + +add_task(async function failsWithTooLargeWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentDPR(); + + const overrideSettings = { + width: MAX_WINDOW_SIZE + 1, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Assert.rejects( + Emulation.setDeviceMetricsOverride(overrideSettings), + err => + err.message.includes( + "Width and height values must be positive, not greater than 10000000" + ), + "Too large width raised error" + ); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is(await getContentDPR(), ratio, "Device pixel ratio hasn't been changed"); +}); + +add_task(async function failsWithNegativeHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentDPR(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: -1, + deviceScaleFactor: 1.0, + }; + + await Assert.rejects( + Emulation.setDeviceMetricsOverride(overrideSettings), + err => + err.message.includes( + "Width and height values must be positive, not greater than 10000000" + ), + "Negative height raised error" + ); + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is(await getContentDPR(), ratio, "Device pixel ratio hasn't been changed"); +}); + +add_task(async function failsWithTooLargeHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentDPR(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: MAX_WINDOW_SIZE + 1, + deviceScaleFactor: 1.0, + }; + await Assert.rejects( + Emulation.setDeviceMetricsOverride(overrideSettings), + err => + err.message.includes( + "Width and height values must be positive, not greater than 10000000" + ), + "Too large height raised error" + ); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is(await getContentDPR(), ratio, "Device pixel ratio hasn't been changed"); +}); + +add_task(async function setDevicePixelRatio({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio_orig = await getContentDPR(); + + const overrideSettings = { + width: layoutViewport.clientWidth, + height: layoutViewport.clientHeight, + deviceScaleFactor: ratio_orig * 2, + }; + + // Set a custom pixel ratio + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + is(await getContentDPR(), ratio_orig * 2, "Expected device pixel ratio set"); +}); + +add_task(async function failsWithNegativeRatio({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentDPR(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientHeight / 2), + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: -1, + }; + + await Assert.rejects( + Emulation.setDeviceMetricsOverride(overrideSettings), + err => err.message.includes("deviceScaleFactor: must be positive"), + "Negative device scale factor raised error" + ); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is(await getContentDPR(), ratio, "Device pixel ratio hasn't been changed"); +}); diff --git a/remote/cdp/test/browser/emulation/browser_setTouchEmulationEnabled.js b/remote/cdp/test/browser/emulation/browser_setTouchEmulationEnabled.js new file mode 100644 index 0000000000..1d4efacff1 --- /dev/null +++ b/remote/cdp/test/browser/emulation/browser_setTouchEmulationEnabled.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC_TOUCH = toDataURL("<div>Hello world"); + +add_task(async function invalidEnabledType({ client }) { + const { Emulation } = client; + + for (const enabled of [null, "", 1, [], {}]) { + await Assert.rejects( + Emulation.setTouchEmulationEnabled({ enabled }), + /enabled: boolean value expected/, + `Fails with invalid type: ${enabled}` + ); + } +}); + +add_task(async function enableAndDisable({ client }) { + const { Emulation, Runtime } = client; + const url = toDataURL("<p>foo"); + + await enableRuntime(client); + + let contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + let result = await contextCreated; + await assertTouchEnabled(client, result.context, false); + + await Emulation.setTouchEmulationEnabled({ enabled: true }); + contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + result = await contextCreated; + await assertTouchEnabled(client, result.context, true); + + await Emulation.setTouchEmulationEnabled({ enabled: false }); + contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + result = await contextCreated; + await assertTouchEnabled(client, result.context, false); +}); + +add_task(async function receiveTouchEventsWhenEnabled({ client }) { + const { Emulation, Runtime } = client; + + await enableRuntime(client); + + await Emulation.setTouchEmulationEnabled({ enabled: true }); + const contextCreated = Runtime.executionContextCreated(); + await loadURL(DOC_TOUCH); + const { context } = await contextCreated; + + await assertTouchEnabled(client, context, true); + + const { result } = await evaluate(client, context.id, () => { + return new Promise(resolve => { + window.ontouchstart = () => { + resolve(true); + }; + window.dispatchEvent(new Event("touchstart")); + resolve(false); + }); + }); + is(result.value, true, "Received touch event"); +}); + +async function assertTouchEnabled(client, context, expectedStatus) { + const { result } = await evaluate(client, context.id, () => { + return "ontouchstart" in window; + }); + + if (expectedStatus) { + ok(result.value, "Touch emulation enabled"); + } else { + ok(!result.value, "Touch emulation disabled"); + } +} diff --git a/remote/cdp/test/browser/emulation/browser_setUserAgentOverride.js b/remote/cdp/test/browser/emulation/browser_setUserAgentOverride.js new file mode 100644 index 0000000000..a0f292435d --- /dev/null +++ b/remote/cdp/test/browser/emulation/browser_setUserAgentOverride.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL(`<script>document.write(navigator.userAgent);</script>`); + +add_task(async function invalidPlatform({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0\n"; + + for (const platform of [null, true, 1, [], {}]) { + await Assert.rejects( + Emulation.setUserAgentOverride({ userAgent, platform }), + /platform: string value expected/, + `Fails with invalid type: ${platform}` + ); + } +}); + +add_task(async function setAndResetUserAgent({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + + isnot(originalUserAgent, userAgent, "Custom user agent hasn't been set"); + + await Emulation.setUserAgentOverride({ userAgent }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + + await Emulation.setUserAgentOverride({ userAgent: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); +}); + +add_task(async function invalidUserAgent({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0\n"; + + await loadURL(DOC); + isnot( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent hasn't been set" + ); + + await Assert.rejects( + Emulation.setUserAgentOverride({ userAgent }), + err => err.message.includes("Invalid characters found in userAgent"), + "Invalid user agent format raised error" + ); +}); + +add_task(async function setAndResetPlatform({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + const originalPlatform = await getNavigatorProperty("platform"); + + isnot(userAgent, originalUserAgent, "Custom user agent hasn't been set"); + isnot(platform, originalPlatform, "Custom platform hasn't been set"); + + await Emulation.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + await Emulation.setUserAgentOverride({ userAgent: "", platform: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); + is( + await getNavigatorProperty("platform"), + originalPlatform, + "Custom platform has been reset" + ); +}); + +add_task(async function notSetForNewContext({ client }) { + const { Emulation, Target } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await Emulation.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + await openTab(Target, { activate: true }); + + isnot( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has not been set" + ); + isnot( + await getNavigatorProperty("platform"), + platform, + "Custom platform has not been set" + ); +}); + +async function getNavigatorProperty(prop) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [prop], _prop => { + return content.navigator[_prop]; + }); +} diff --git a/remote/cdp/test/browser/emulation/head.js b/remote/cdp/test/browser/emulation/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/emulation/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/fetch/browser.toml b/remote/cdp/test/browser/fetch/browser.toml new file mode 100644 index 0000000000..bf26c824a5 --- /dev/null +++ b/remote/cdp/test/browser/fetch/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_disable.js"] diff --git a/remote/cdp/test/browser/fetch/browser_disable.js b/remote/cdp/test/browser/fetch/browser_disable.js new file mode 100644 index 0000000000..01c65fb855 --- /dev/null +++ b/remote/cdp/test/browser/fetch/browser_disable.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function fetchDomainDisabled({ client }) { + const { Fetch } = client; + + await Fetch.disable(); + ok("Disabling Fetch domain successful"); +}); diff --git a/remote/cdp/test/browser/fetch/head.js b/remote/cdp/test/browser/fetch/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/fetch/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/head.js b/remote/cdp/test/browser/head.js new file mode 100644 index 0000000000..acf2216194 --- /dev/null +++ b/remote/cdp/test/browser/head.js @@ -0,0 +1,629 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// window.RemoteAgent is a simple object set in browser.js, and importing +// RemoteAgent conflicts with that. +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +const { RemoteAgent } = ChromeUtils.importESModule( + "chrome://remote/content/components/RemoteAgent.sys.mjs" +); +const { RemoteAgentError } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/Error.sys.mjs" +); +const { TabManager } = ChromeUtils.importESModule( + "chrome://remote/content/shared/TabManager.sys.mjs" +); +const { Stream } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/StreamRegistry.sys.mjs" +); + +const { getTimeoutMultiplier } = ChromeUtils.importESModule( + "chrome://remote/content/shared/AppInfo.sys.mjs" +); + +const TIMEOUT_MULTIPLIER = getTimeoutMultiplier(); +const TIMEOUT_EVENTS = 1000 * TIMEOUT_MULTIPLIER; + +/* +add_task() is overriden to setup and teardown a test environment +making it easier to write browser-chrome tests for the remote +debugger. + +Before the task is run, the nsIRemoteAgent listener is started and +a CDP client is connected to it. A new tab is also added. These +three things are exposed to the provided task like this: + + add_task(async function testName(client, CDP, tab) { + // client is an instance of the CDP class + // CDP is ./chrome-remote-interface.js + // tab is a fresh tab, destroyed after the test + }); + +Also target discovery is getting enabled, which means that targetCreated, +targetDestroyed, and targetInfoChanged events will be received by the client. + +add_plain_task() may be used to write test tasks without the implicit +setup and teardown described above. +*/ + +const add_plain_task = add_task.bind(this); + +this.add_task = function (taskFn, opts = {}) { + const { + createTab = true, // By default run each test in its own tab + } = opts; + + const fn = async function () { + let client, tab, target; + + try { + const CDP = await getCDP(); + + if (createTab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const tabId = TabManager.getIdForBrowser(tab.linkedBrowser); + + const targets = await CDP.List(); + target = targets.find(target => target.id === tabId); + } + + client = await CDP({ target }); + info("CDP client instantiated"); + + // Bug 1605722 - Workaround to not hang when waiting for Target events + await getDiscoveredTargets(client.Target); + + await taskFn({ client, CDP, tab }); + + if (createTab) { + // taskFn may resolve within a tick after opening a new tab. + // We shouldn't remove the newly opened tab in the same tick. + // Wait for the next tick here. + await TestUtils.waitForTick(); + BrowserTestUtils.removeTab(tab); + } + } catch (e) { + // Display better error message with the server side stacktrace + // if an error happened on the server side: + if (e.response) { + throw RemoteAgentError.fromJSON(e.response); + } else { + throw e; + } + } finally { + if (client) { + await client.close(); + info("CDP client closed"); + } + + // Close any additional tabs, so that only a single tab remains open + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } + } + }; + + Object.defineProperty(fn, "name", { value: taskFn.name, writable: false }); + add_plain_task(fn); +}; + +/** + * Create a test document in an invisible window. + * This window will be automatically closed on test teardown. + */ +function createTestDocument() { + const browser = Services.appShell.createWindowlessBrowser(true); + registerCleanupFunction(() => browser.close()); + + // Create a system principal content viewer to ensure there is a valid + // empty document using system principal and avoid any wrapper issues + // when using document's JS Objects. + const webNavigation = browser.docShell.QueryInterface(Ci.nsIWebNavigation); + const system = Services.scriptSecurityManager.getSystemPrincipal(); + webNavigation.createAboutBlankDocumentViewer(system, system); + + return webNavigation.document; +} + +/** + * Retrieve an intance of CDP object from chrome-remote-interface library + */ +async function getCDP() { + // Instantiate a background test document in order to load the library + // as in a web page + const document = createTestDocument(); + + const window = document.defaultView.wrappedJSObject; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/chrome-remote-interface.js", + window + ); + + // Implements `criRequest` to be called by chrome-remote-interface + // library in order to do the cross-domain http request, which, + // in a regular Web page, is impossible. + window.criRequest = (options, callback) => { + const { path } = options; + const url = `http://${RemoteAgent.host}:${RemoteAgent.port}${path}`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + + // Prevent "XML Parsing Error: syntax error" error messages + xhr.overrideMimeType("text/plain"); + + xhr.send(null); + xhr.onload = () => callback(null, xhr.responseText); + xhr.onerror = e => callback(e, null); + }; + + return window.CDP; +} + +async function getScrollbarSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const scrollbarHeight = {}; + const scrollbarWidth = {}; + + content.windowUtils.getScrollbarSize( + false, + scrollbarWidth, + scrollbarHeight + ); + return { + width: scrollbarWidth.value, + height: scrollbarHeight.value, + }; + }); +} + +function getTargets(CDP) { + return new Promise((resolve, reject) => { + CDP.List(null, (err, targets) => { + if (err) { + reject(err); + return; + } + resolve(targets); + }); + }); +} + +// Wait for all Target.targetCreated events. One for each tab. +async function getDiscoveredTargets(Target, options = {}) { + const { discover = true, filter } = options; + + const targets = []; + const unsubscribe = Target.targetCreated(target => { + targets.push(target.targetInfo); + }); + + await Target.setDiscoverTargets({ + discover, + filter, + }).finally(() => unsubscribe()); + + return targets; +} + +async function openTab(Target, options = {}) { + const { activate = false } = options; + + info("Create a new tab and wait for the target to be created"); + const targetCreated = Target.targetCreated(); + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const { targetInfo } = await targetCreated; + + is(targetInfo.type, "page"); + + if (activate) { + await Target.activateTarget({ + targetId: targetInfo.targetId, + }); + info(`New tab with target id ${targetInfo.targetId} created and activated`); + } else { + info(`New tab with target id ${targetInfo.targetId} created`); + } + + return { targetInfo, newTab }; +} + +async function openWindow(Target, options = {}) { + const { activate = false } = options; + + info("Create a new window and wait for the target to be created"); + const targetCreated = Target.targetCreated(); + const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + const newTab = newWindow.gBrowser.selectedTab; + const { targetInfo } = await targetCreated; + is(targetInfo.type, "page"); + + if (activate) { + await Target.activateTarget({ + targetId: targetInfo.targetId, + }); + info( + `New window with target id ${targetInfo.targetId} created and activated` + ); + } else { + info(`New window with target id ${targetInfo.targetId} created`); + } + + return { targetInfo, newWindow, newTab }; +} + +/** Creates a data URL for the given source document. */ +function toDataURL(src, doctype = "html") { + let doc, mime; + switch (doctype) { + case "html": + mime = "text/html;charset=utf-8"; + doc = `<!doctype html>\n<meta charset=utf-8>\n${src}`; + break; + default: + throw new Error("Unexpected doctype: " + doctype); + } + + return `data:${mime},${encodeURIComponent(doc)}`; +} + +function convertArgument(arg) { + if (typeof arg === "bigint") { + return { unserializableValue: `${arg.toString()}n` }; + } + if (Object.is(arg, -0)) { + return { unserializableValue: "-0" }; + } + if (Object.is(arg, Infinity)) { + return { unserializableValue: "Infinity" }; + } + if (Object.is(arg, -Infinity)) { + return { unserializableValue: "-Infinity" }; + } + if (Object.is(arg, NaN)) { + return { unserializableValue: "NaN" }; + } + + return { value: arg }; +} + +async function evaluate(client, contextId, pageFunction, ...args) { + const { Runtime } = client; + + if (typeof pageFunction === "string") { + return Runtime.evaluate({ + expression: pageFunction, + contextId, + returnByValue: true, + awaitPromise: true, + }); + } else if (typeof pageFunction === "function") { + return Runtime.callFunctionOn({ + functionDeclaration: pageFunction.toString(), + executionContextId: contextId, + arguments: args.map(convertArgument), + returnByValue: true, + awaitPromise: true, + }); + } + throw new Error("pageFunction: expected 'string' or 'function'"); +} + +/** + * Load a given URL in the currently selected tab + */ +async function loadURL(url, expectedURL = undefined) { + expectedURL = expectedURL || url; + + const browser = gBrowser.selectedTab.linkedBrowser; + const loaded = BrowserTestUtils.browserLoaded(browser, true, expectedURL); + + BrowserTestUtils.startLoadingURIString(browser, url); + await loaded; +} + +/** + * Enable the Runtime domain + */ +async function enableRuntime(client) { + const { Runtime } = client; + + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +/** + * Retrieve the value of a property on the content window. + */ +function getContentProperty(prop) { + info(`Retrieve ${prop} on the content window`); + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [prop], + _prop => content[_prop] + ); +} + +/** + * Retrieve all frames for the current tab as flattened list. + * + * @returns {Map<number, Frame>} + * Flattened list of frames as Map + */ +async function getFlattenedFrameTree(client) { + const { Page } = client; + + function flatten(frames) { + return frames.reduce((result, current) => { + result.set(current.frame.id, current.frame); + if (current.childFrames) { + const frames = flatten(current.childFrames); + result = new Map([...result, ...frames]); + } + return result; + }, new Map()); + } + + const { frameTree } = await Page.getFrameTree(); + return flatten(Array(frameTree)); +} + +/** + * Return a new promise, which resolves after ms have been elapsed + */ +function timeoutPromise(ms) { + return new Promise(resolve => { + window.setTimeout(resolve, ms); + }); +} + +/** Fail a test. */ +function fail(message) { + ok(false, message); +} + +/** + * Create a stream with the specified contents. + * + * @param {string} contents + * Contents of the file. + * @param {object} options + * @param {string=} options.path + * Path of the file. Defaults to the temporary directory. + * @param {boolean=} options.remove + * If true, automatically remove the file after the test. Defaults to true. + * + * @returns {Promise<Stream>} + */ +async function createFileStream(contents, options = {}) { + let { path = null, remove = true } = options; + + if (!path) { + path = await IOUtils.createUniqueFile( + PathUtils.tempDir, + "remote-agent.txt" + ); + } + + await IOUtils.writeUTF8(path, contents); + + const stream = new Stream(path); + if (remove) { + registerCleanupFunction(() => stream.destroy()); + } + + return stream; +} + +async function throwScriptError(options = {}) { + const { inContent = true } = options; + + const addScriptErrorInternal = options => { + const { + flag = Ci.nsIScriptError.errorFlag, + innerWindowId = content.windowGlobalChild.innerWindowId, + } = options; + + const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + scriptError.initWithWindowID( + options.text, + options.sourceName || "sourceName", + null, + options.lineNumber || 0, + options.columnNumber || 0, + flag, + options.category || "javascript", + innerWindowId + ); + Services.console.logMessage(scriptError); + }; + + if (inContent) { + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [options], + addScriptErrorInternal + ); + } else { + options.innerWindowId = window.windowGlobalChild.innerWindowId; + addScriptErrorInternal(options); + } +} + +class RecordEvents { + /** + * A timeline of events chosen by calls to `addRecorder`. + * Call `configure`` for each client event you want to record. + * Then `await record(someTimeout)` to record a timeline that you + * can make assertions about. + * + * const history = new RecordEvents(expectedNumberOfEvents); + * + * history.addRecorder({ + * event: Runtime.executionContextDestroyed, + * eventName: "Runtime.executionContextDestroyed", + * messageFn: payload => { + * return `Received Runtime.executionContextDestroyed for id ${payload.executionContextId}`; + * }, + * }); + * + * + * @param {number} total + * Number of expected events. Stop recording when this number is exceeded. + * + */ + constructor(total) { + this.events = []; + this.promises = new Set(); + this.subscriptions = new Set(); + this.total = total; + } + + /** + * Configure an event to be recorded and logged. + * The recording stops once we accumulate more than the expected + * total of all configured events. + * + * @param {object} options + * @param {CDPEvent} options.event + * https://github.com/cyrus-and/chrome-remote-interface#clientdomaineventcallback + * @param {string} options.eventName + * Name to use for reporting. + * @param {Function=} options.callback + * ({ eventName, payload }) => {} to be called when each event is received + * @param {function(payload):string=} options.messageFn + */ + addRecorder(options = {}) { + const { + event, + eventName, + messageFn = () => `Recorded ${eventName}`, + callback, + } = options; + + const promise = new Promise(resolve => { + const unsubscribe = event(payload => { + info(messageFn(payload)); + this.events.push({ eventName, payload, index: this.events.length }); + callback?.({ eventName, payload, index: this.events.length - 1 }); + if (this.events.length > this.total) { + this.subscriptions.delete(unsubscribe); + unsubscribe(); + resolve(this.events); + } + }); + this.subscriptions.add(unsubscribe); + }); + + this.promises.add(promise); + } + + /** + * Register a promise to await while recording the timeline. The returned + * callback resolves the registered promise and adds `step` + * to the timeline, along with an associated payload, if provided. + * + * @param {string} step + * @returns {Function} callback + */ + addPromise(step) { + let callback; + const promise = new Promise(resolve => { + callback = value => { + resolve(); + info(`Recorded ${step}`); + this.events.push({ + eventName: step, + payload: value, + index: this.events.length, + }); + return value; + }; + }); + + this.promises.add(promise); + return callback; + } + + /** + * Record events until we hit the timeout or the expected total is exceeded. + * + * @param {number=} timeout + * Timeout in milliseconds. Defaults to 1000. + * + * @returns {Array<{ eventName, payload, index }>} Recorded events + */ + async record(timeout = TIMEOUT_EVENTS) { + await Promise.race([Promise.all(this.promises), timeoutPromise(timeout)]); + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } + return this.events; + } + + /** + * Filter events based on predicate + * + * @param {Function} predicate + * + * @returns {Array<{ eventName, payload, index }>} + * The list of events matching the filter. + */ + filter(predicate) { + return this.events.filter(predicate); + } + + /** + * Find first occurrence of the given event. + * + * @param {string} eventName + * + * @returns {{ eventName, payload, index }} The event, if any. + */ + findEvent(eventName) { + const event = this.events.find(el => el.eventName == eventName); + if (event) { + return event; + } + return {}; + } + + /** + * Find given events. + * + * @param {string} eventName + * + * @returns {Array<{ eventName, payload, index }>} + * The events, if any. + */ + findEvents(eventName) { + return this.events.filter(event => event.eventName == eventName); + } + + /** + * Find index of first occurrence of the given event. + * + * @param {string} eventName + * + * @returns {number} The event index, -1 if not found. + */ + indexOf(eventName) { + const event = this.events.find(el => el.eventName == eventName); + if (event) { + return event.index; + } + return -1; + } +} diff --git a/remote/cdp/test/browser/input/browser.toml b/remote/cdp/test/browser/input/browser.toml new file mode 100644 index 0000000000..b103734fc0 --- /dev/null +++ b/remote/cdp/test/browser/input/browser.toml @@ -0,0 +1,31 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", + "doc_events.html", + "doc_dispatchKeyEvent_race.html", +] + +["browser_dispatchKeyEvent.js"] + +["browser_dispatchKeyEvent_events.js"] +https_first_disabled = true + +["browser_dispatchKeyEvent_race.js"] +https_first_disabled = true + +["browser_dispatchMouseEvent.js"] diff --git a/remote/cdp/test/browser/input/browser_dispatchKeyEvent.js b/remote/cdp/test/browser/input/browser_dispatchKeyEvent.js new file mode 100644 index 0000000000..e2b214503b --- /dev/null +++ b/remote/cdp/test/browser/input/browser_dispatchKeyEvent.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testTypingPrintableCharacters({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + info("Write 'h'"); + await sendTextKey(Input, "h"); + await checkInputContent("h", 1); + + info("Write 'H'"); + await sendTextKey(Input, "H"); + await checkInputContent("hH", 2); + + info("Send char type event for char [’]"); + await Input.dispatchKeyEvent({ + type: "char", + modifiers: 0, + key: "’", + }); + await checkInputContent("hH’", 3); +}); + +add_task(async function testArrowKeys({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + await sendText(Input, "hH’"); + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hH’", 2); + + info("Write 'a'"); + await sendTextKey(Input, "a"); + await checkInputContent("hHa’", 3); + + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hHa’", 2); + + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hHa’", 1); + + info("Write 'a'"); + await sendTextKey(Input, "a"); + await checkInputContent("haHa’", 2); + + info("Send ALT/CONTROL + Right"); + const modCode = AppInfo.isMac ? alt : ctrl; + const modKey = AppInfo.isMac ? "Alt" : "Control"; + await dispatchKeyEvent(Input, modKey, "rawKeyDown", modCode); + await dispatchKeyEvent(Input, "ArrowRight", "rawKeyDown", modCode); + await dispatchKeyEvent(Input, "ArrowRight", "keyUp"); + await dispatchKeyEvent(Input, modKey, "keyUp"); + await checkInputContent("haHa’", 5); +}); + +add_task(async function testBackspace({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + await sendText(Input, "haHa’"); + + info("Delete every character in the input"); + await checkBackspace(Input, "haHa"); + await checkBackspace(Input, "haH"); + await checkBackspace(Input, "ha"); + await checkBackspace(Input, "h"); + await checkBackspace(Input, ""); +}); + +add_task(async function testShiftSelect({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Shift + Left (select one char to the left)"); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + info("(deleteContentBackward)"); + await checkBackspace(Input, "word 2 wo"); + await dispatchKeyEvent(Input, "Shift", "keyUp"); + + await resetInput("word 2 wo"); + info("Send Shift + Left (select one char to the left)"); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendTextKey(Input, "H"); + await checkInputContent("word 2 H", 8); + await dispatchKeyEvent(Input, "Shift", "keyUp"); +}); + +add_task(async function testSelectWord({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Shift + Ctrl/Alt + Left (select one word to the left)"); + const { primary, primaryKey } = keyForPlatform(); + const combined = shift | primary; + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", combined); + await sendRawKey(Input, "ArrowLeft", combined); + await sendRawKey(Input, "ArrowLeft", combined); + await dispatchKeyEvent(Input, "Shift", "keyUp", primary); + await dispatchKeyEvent(Input, primaryKey, "keyUp"); + info("(deleteContentBackward)"); + await checkBackspace(Input, "word "); +}); + +add_task(async function testSelectDelete({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Ctrl/Alt + Backspace (deleteWordBackward)"); + const { primary, primaryKey } = keyForPlatform(); + await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", primary); + await checkBackspace(Input, "word 2 ", primary); + await dispatchKeyEvent(Input, primaryKey, "keyUp"); + + await resetInput("word 2 "); + await sendText(Input, "word4"); + await sendRawKey(Input, "ArrowLeft"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("word 2 word4", 10); + + if (AppInfo.isMac) { + info("Send Meta + Backspace (deleteSoftLineBackward)"); + await dispatchKeyEvent(Input, "Meta", "rawKeyDown", meta); + await sendRawKey(Input, "Backspace", meta); + await dispatchKeyEvent(Input, "Meta", "keyUp"); + await checkInputContent("d4", 0); + } +}); + +add_task(async function testCtrlShiftArrows({ client }) { + await loadURL( + toDataURL('<select multiple size="3"><option>a<option>b<option>c</select>') + ); + const { Input } = client; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const select = content.document.querySelector("select"); + select.selectedIndex = 0; + select.focus(); + }); + + const combined = shift | ctrl; + await dispatchKeyEvent(Input, "Control", "rawKeyDown", shift); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", combined); + await sendRawKey(Input, "ArrowDown", combined); + await dispatchKeyEvent(Input, "Control", "keyUp", shift); + await dispatchKeyEvent(Input, "Shift", "keyUp"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const select = content.document.querySelector("select"); + ok(select[0].selected, "First option should be selected"); + ok(select[1].selected, "Second option should be selected"); + ok(!select[2].selected, "Third option should not be selected"); + }); +}); diff --git a/remote/cdp/test/browser/input/browser_dispatchKeyEvent_events.js b/remote/cdp/test/browser/input/browser_dispatchKeyEvent_events.js new file mode 100644 index 0000000000..195043019e --- /dev/null +++ b/remote/cdp/test/browser/input/browser_dispatchKeyEvent_events.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_URL = + "https://example.com/browser/remote/cdp/test/browser/input/doc_events.html"; + +add_task(async function testShiftEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "A"); + await checkInputContent("A", 1); + let events = await getEvents(); + checkEvent(events[0], "keydown", "Shift", "shift", true); + checkEvent(events[1], "keydown", "A", "shift", true); + checkEvent(events[2], "keypress", "A", "shift", true); + checkProperties({ data: "A", inputType: "insertText" }, events[3]); + checkEvent(events[4], "keyup", "A", "shift", true); + checkEvent(events[5], "keyup", "Shift", "shift", false); + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "Enter"); + events = await getEvents(); + checkEvent(events[2], "keypress", "Enter", "shift", true); + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "Tab"); + events = await getEvents(); + checkEvent(events[1], "keydown", "Tab", "shift", true); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const input = content.document.querySelector("input"); + isnot(input, content.document.activeElement, "input should lose focus"); + }); +}); + +add_task(async function testAltEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Alt", "alt", "a"); + if (AppInfo.isMac) { + await checkInputContent("a", 1); + } else { + await checkInputContent("", 0); + } + let events = await getEvents(); + checkEvent(events[1], "keydown", "a", "alt", true); + checkEvent(events[events.length - 1], "keyup", "Alt", "alt", false); +}); + +add_task(async function testControlEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Control", "ctrl", "`"); + let events = await getEvents(); + // no keypress or input event + checkEvent(events[1], "keydown", "`", "ctrl", true); + checkEvent(events[events.length - 1], "keyup", "Control", "ctrl", false); +}); + +add_task(async function testMetaEvents({ client }) { + if (!AppInfo.isMac) { + return; + } + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Meta", "meta", "a"); + let events = await getEvents(); + // no keypress or input event + checkEvent(events[1], "keydown", "a", "meta", true); + checkEvent(events[events.length - 1], "keyup", "Meta", "meta", false); +}); diff --git a/remote/cdp/test/browser/input/browser_dispatchKeyEvent_race.js b/remote/cdp/test/browser/input/browser_dispatchKeyEvent_race.js new file mode 100644 index 0000000000..ee1cd5be39 --- /dev/null +++ b/remote/cdp/test/browser/input/browser_dispatchKeyEvent_race.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Here we test that the `dispatchKeyEvent` API resolves after all the synchronous event +// handlers from the content page have been flushed. +// +// Say the content page has an event handler such as: +// +// el.addEventListener("keyup", () => { +// doSomeVeryLongProcessing(); // <- takes a long time but is synchronous! +// window.myVariable = "newValue"; +// }); +// +// And imagine this is tested via: +// +// await Input.dispatchKeyEvent(...); +// const myVariable = await Runtime.evaluate({ expression: "window.myVariable" }); +// equals(myVariable, "newValue"); +// +// In order for this to work, we need to be sure that `await Input.dispatchKeyEvent` +// resolves only after the content page flushed the event handlers (and +// `window.myVariable = "newValue"` was executed). +// +// This can be racy because Input.dispatchKeyEvent and window.myVariable = "newValue" run +// in different processes. + +const PAGE_URL = + "https://example.com/browser/remote/cdp/test/browser/input/doc_dispatchKeyEvent_race.html"; + +add_task(async function ({ client }) { + await loadURL(PAGE_URL); + + const { Input, Runtime } = client; + + // Need an enabled Runtime domain to run evaluate. + info("Enable the Runtime domain"); + await Runtime.enable(); + const { context } = await Runtime.executionContextCreated(); + + info("Focus the input on the page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const input = content.document.querySelector("input"); + input.focus(); + is(input, content.document.activeElement, "Input should be focused"); + }); + + // See doc_input_dispatchKeyEvent_race.html + // The page listens to `input` events to update a property on window. + // We will check that the value is updated as soon dispatchKeyEvent has resolved. + await checkWindowTestValue("initial-value", context.id, Runtime); + + info("Write 'hhhhhh' ('h' times 6)"); + for (let i = 0; i < 6; i++) { + await dispatchKeyEvent(Input, "h", 72, "keyDown"); + await dispatchKeyEvent(Input, "h", 72, "keyUp"); + } + await checkWindowTestValue("hhhhhh", context.id, Runtime); + + info("Write 'aaaaaa' with 6 consecutive keydown and one keyup"); + await Promise.all([ + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + ]); + await dispatchKeyEvent(Input, "a", 65, "keyUp"); + await checkWindowTestValue("hhhhhhaaaaaa", context.id, Runtime); +}); + +function dispatchKeyEvent(Input, key, keyCode, type, modifiers = 0) { + info(`Send ${type} for key ${key}`); + return Input.dispatchKeyEvent({ + type, + modifiers, + windowsVirtualKeyCode: keyCode, + key, + }); +} + +async function checkWindowTestValue(expected, contextId, Runtime) { + info("Retrieve the value of `window.testValue` in the test page"); + const { result } = await Runtime.evaluate({ + contextId, + expression: "window.testValue", + }); + + is(result.value, expected, "Content window test value is correct"); +} diff --git a/remote/cdp/test/browser/input/browser_dispatchMouseEvent.js b/remote/cdp/test/browser/input/browser_dispatchMouseEvent.js new file mode 100644 index 0000000000..9ab2b74a67 --- /dev/null +++ b/remote/cdp/test/browser/input/browser_dispatchMouseEvent.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_URL = + "https://example.com/browser/remote/cdp/test/browser/input/doc_events.html"; + +add_task(async function testPressedReleasedAndClick({ client }) { + const { Input } = client; + + await loadURL(PAGE_URL); + await resetEvents(); + + info("Click the 'pointers' div."); + await Input.dispatchMouseEvent({ + type: "mousePressed", + x: 80, + y: 180, + }); + await Input.dispatchMouseEvent({ + type: "mouseReleased", + x: 80, + y: 180, + }); + + const events = await getEvents(); + ["mousedown", "mouseup", "click"].forEach((type, index) => { + info(`Checking properties for event ${type}`); + checkProperties({ type, button: 0 }, events[index]); + }); +}); + +add_task(async function testModifiers({ client }) { + const { Input } = client; + + await loadURL(PAGE_URL); + + const testable_modifiers = [ + [alt], + [ctrl], + [meta], + [shift], + [alt, meta], + [ctrl, shift], + [alt, ctrl, meta, shift], + ]; + + for (let modifiers of testable_modifiers) { + info(`MousePressed with modifier ${modifiers} on the 'pointers' div.`); + + await resetEvents(); + await Input.dispatchMouseEvent({ + type: "mousePressed", + x: 80, + y: 180, + modifiers: modifiers.reduce((previous, current) => previous | current), + }); + + const events = await getEvents(); + const expectedEvent = { + type: "mousedown", + button: 0, + altKey: modifiers.includes(alt), + ctrlKey: modifiers.includes(ctrl), + metaKey: modifiers.includes(meta), + shiftKey: modifiers.includes(shift), + }; + + checkProperties(expectedEvent, events[0]); + } +}); + +add_task(async function testClickCount({ client }) { + const { Input } = client; + + await loadURL(PAGE_URL); + + const testable_clickCounts = [ + { type: "click", clickCount: 1 }, + { type: "dblclick", clickCount: 2 }, + ]; + + for (const { clickCount, type } of testable_clickCounts) { + info(`MousePressed with clickCount ${clickCount} on the 'pointers' div.`); + + await resetEvents(); + await Input.dispatchMouseEvent({ + type: "mousePressed", + x: 80, + y: 180, + clickCount, + }); + await Input.dispatchMouseEvent({ + type: "mouseReleased", + x: 80, + y: 180, + clickCount, + }); + + const events = await getEvents(); + checkProperties({ type, button: 0 }, events[events.length - 1]); + } +}); + +add_task(async function testDispatchMouseEventAwaitClick({ client }) { + const { Input } = client; + + await setupForInput(PAGE_URL); + await loadURL( + toDataURL(` + <div onclick="setTimeout(() => result = 'clicked', 0)">foo</div> + `) + ); + + const { x, y } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const div = content.document.querySelector("div"); + return div.getBoundingClientRect(); + } + ); + + await Input.dispatchMouseEvent({ type: "mousePressed", x, y }); + await Input.dispatchMouseEvent({ type: "mouseReleased", x, y }); + + const context = await enableRuntime(client); + const { result } = await evaluate( + client, + context.id, + () => globalThis.result + ); + + is(result.value, "clicked", "Awaited click event handler"); +}); diff --git a/remote/cdp/test/browser/input/doc_dispatchKeyEvent_race.html b/remote/cdp/test/browser/input/doc_dispatchKeyEvent_race.html new file mode 100644 index 0000000000..c761a224e6 --- /dev/null +++ b/remote/cdp/test/browser/input/doc_dispatchKeyEvent_race.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test page for dispatchKeyEvent race test</title> +</head> +<body> + <input type="text"> + <script type="text/javascript"> + window.testValue = "initial-value"; + + // Small helper to synchronously pause for a given delay, in ms. + function sleep(delay) { + const start = Date.now(); + while (Date.now() - start < delay) { + // Wait for the condition to fail. + } + } + + document.querySelector("input").addEventListener("input", () => { + // Block the execution synchronously for one second. + sleep(1000); + // Update the value that will be checked by the test. + window.testValue = document.querySelector("input").value; + }); + </script> +</body> +</html> diff --git a/remote/cdp/test/browser/input/doc_events.html b/remote/cdp/test/browser/input/doc_events.html new file mode 100644 index 0000000000..74df931233 --- /dev/null +++ b/remote/cdp/test/browser/input/doc_events.html @@ -0,0 +1,148 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Input Events</title> + <style> + div { padding:0px; margin: 0px; } + #trackPointer { position: fixed; } + #resultContainer { width: 600px; height: 60px; } + .area { width: 100px; height: 50px; background-color: #ccc; } + .block { width: 5px; height: 5px; border: solid 1px red; } + #dragArea { position: relative; } + #dragTarget { position: absolute; top:22px; left:47px;} + </style> + <script> + "use strict"; + var allEvents = []; + function makeParagraph(message) { + let paragraph = document.createElement("p"); + paragraph.textContent = message; + return paragraph; + } + function displayMessage(message) { + let eventNode = document.getElementById("events"); + eventNode.innerHTML = "" + eventNode.appendChild(makeParagraph(message)); + } + + function appendMessage(message) { + document.getElementById("events").appendChild(makeParagraph(message)); + } + + /** + * Escape |key| if it's in a surrogate-half character range. + * + * Example: given "\ud83d" return "U+d83d". + * + * Otherwise JSON.stringify will convert it to U+FFFD (REPLACEMENT CHARACTER) + * when returning a value from executeScript, for example. + */ + function escapeSurrogateHalf(key) { + if (typeof key !== "undefined" && key.length === 1) { + var charCode = key.charCodeAt(0); + var highSurrogate = charCode >= 0xD800 && charCode <= 0xDBFF; + var surrogate = highSurrogate || (charCode >= 0xDC00 && charCode <= 0xDFFF); + if (surrogate) { + key = "U+" + charCode.toString(16); + } + } + return key; + } + + function recordKeyboardEvent(event) { + var key = escapeSurrogateHalf(event.key); + allEvents.push({ + "code": event.code, + "key": key, + "which": event.which, + "location": event.location, + "alt": event.altKey, + "ctrl": event.ctrlKey, + "meta": event.metaKey, + "shift": event.shiftKey, + "repeat": event.repeat, + "type": event.type + }); + appendMessage(event.type + " " + + "code: " + event.code + ", " + + "key: " + key + ", " + + "which: " + event.which + ", " + + "keyCode: " + event.keyCode); + } + function recordInputEvent(event) { + allEvents.push({ + "data": event.data, + "inputType": event.inputType, + "isComposing": event.isComposing, + }); + appendMessage("InputEvent " + + "data: " + event.data + ", " + + "inputType: " + event.inputType + ", " + + "isComposing: " + event.isComposing); + } + function recordPointerEvent(event) { + if (event.type === "contextmenu") { + event.preventDefault(); + } + allEvents.push({ + "type": event.type, + "button": event.button, + "buttons": event.buttons, + "pageX": event.pageX, + "pageY": event.pageY, + "ctrlKey": event.ctrlKey, + "metaKey": event.metaKey, + "altKey": event.altKey, + "shiftKey": event.shiftKey, + "target": event.target.id + }); + appendMessage(event.type + " " + + "pageX: " + event.pageX + ", " + + "pageY: " + event.pageY + ", " + + "button: " + event.button + ", " + + "buttons: " + event.buttons + ", " + + "ctrlKey: " + event.ctrlKey + ", " + + "altKey: " + event.altKey + ", " + + "metaKey: " + event.metaKey + ", " + + "shiftKey: " + event.shiftKey + ", " + + "target id: " + event.target.id); + } + function resetEvents() { + allEvents.length = 0; + displayMessage(""); + } + + document.addEventListener("DOMContentLoaded", function() { + let keyReporter = document.getElementById("keys"); + keyReporter.addEventListener("keyup", recordKeyboardEvent); + keyReporter.addEventListener("keypress", recordKeyboardEvent); + keyReporter.addEventListener("keydown", recordKeyboardEvent); + keyReporter.addEventListener("input", recordInputEvent); + + let mouseReporter = document.getElementById("pointers"); + mouseReporter.addEventListener("click", recordPointerEvent); + mouseReporter.addEventListener("dblclick", recordPointerEvent); + mouseReporter.addEventListener("mousedown", recordPointerEvent); + mouseReporter.addEventListener("mouseup", recordPointerEvent); + mouseReporter.addEventListener("contextmenu", recordPointerEvent); + }); + </script> +</head> +<body> + <div id="trackPointer" class="block"></div> + <div> + <h2>KeyReporter</h2> + <input type="text" id="keys" size="80"> + </div> + <div> + <h2>ClickReporter</h2> + <div id="pointers" class="area"> + </div> + </div> + <div id="resultContainer"> + <h2>Events</h2> + <div id="events"></div> + </div> +</body> +</html> diff --git a/remote/cdp/test/browser/input/head.js b/remote/cdp/test/browser/input/head.js new file mode 100644 index 0000000000..4076935802 --- /dev/null +++ b/remote/cdp/test/browser/input/head.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); + +const { Input: I } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/domains/parent/Input.sys.mjs" +); +const { AppInfo } = ChromeUtils.importESModule( + "chrome://remote/content/shared/AppInfo.sys.mjs" +); + +const { alt, ctrl, meta, shift } = I.Modifier; + +// Map of key codes used in Input tests. +const KEYCODES = { + a: KeyboardEvent.DOM_VK_A, + A: KeyboardEvent.DOM_VK_A, + b: KeyboardEvent.DOM_VK_B, + B: KeyboardEvent.DOM_VK_B, + c: KeyboardEvent.DOM_VK_C, + C: KeyboardEvent.DOM_VK_C, + h: KeyboardEvent.DOM_VK_H, + H: KeyboardEvent.DOM_VK_H, + Alt: KeyboardEvent.DOM_VK_ALT, + ArrowLeft: KeyboardEvent.DOM_VK_LEFT, + ArrowRight: KeyboardEvent.DOM_VK_RIGHT, + ArrowDown: KeyboardEvent.DOM_VK_DOWN, + Backspace: KeyboardEvent.DOM_VK_BACK_SPACE, + Control: KeyboardEvent.DOM_VK_CONTROL, + Meta: KeyboardEvent.DM_VK_META, + Shift: KeyboardEvent.DOM_VK_SHIFT, + Tab: KeyboardEvent.DOM_VK_TAB, +}; + +async function setupForInput(url) { + await loadURL(url); + info("Focus the input on the page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const input = content.document.querySelector("input"); + input.focus(); + is(input, content.document.activeElement, "Input should be focused"); + is(input.value, "", "Check input content"); + is(input.selectionStart, 0, "Check position of input caret"); + }); +} + +async function withModifier(Input, modKey, mod, key) { + await dispatchKeyEvent(Input, modKey, "rawKeyDown", I.Modifier[mod]); + await dispatchKeyEvent(Input, key, "keyDown", I.Modifier[mod]); + await dispatchKeyEvent(Input, key, "keyUp", I.Modifier[mod]); + await dispatchKeyEvent(Input, modKey, "keyUp"); +} + +function dispatchKeyEvent(Input, key, type, modifiers = 0) { + info(`Send ${type} for key ${key}`); + return Input.dispatchKeyEvent({ + type, + modifiers, + windowsVirtualKeyCode: KEYCODES[key], + key, + }); +} + +async function getEvents() { + const events = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.allEvents; + }); + info(`Events: ${JSON.stringify(events)}`); + return events; +} + +function getInputContent() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const input = content.document.querySelector("input"); + return { value: input.value, caret: input.selectionStart }; + }); +} + +function checkEvent(event, type, key, property, expectedValue) { + let expected = { type, key }; + expected[property] = expectedValue; + checkProperties(expected, event, "Event"); +} + +async function checkInputContent(expectedValue, expectedCaret) { + const { value, caret } = await getInputContent(); + is(value, expectedValue, "Check input content"); + is(caret, expectedCaret, "Check position of input caret"); +} + +function checkProperties(expectedObj, targetObj, message = "Compare objects") { + for (const prop in expectedObj) { + is(targetObj[prop], expectedObj[prop], message + `: check ${prop}`); + } +} + +function keyForPlatform() { + // TODO add cases for other key-combinations as the need arises + let primary = ctrl; + let primaryKey = "Control"; + if (AppInfo.isMac) { + primary = alt; + primaryKey = "Alt"; + } + return { primary, primaryKey }; +} + +async function sendTextKey(Input, key, modifiers = 0) { + await dispatchKeyEvent(Input, key, "keyDown", modifiers); + await dispatchKeyEvent(Input, key, "keyUp", modifiers); +} + +async function sendText(Input, text) { + for (const sym of text) { + await sendTextKey(Input, sym); + } +} + +async function sendRawKey(Input, key, modifiers = 0) { + await dispatchKeyEvent(Input, key, "rawKeyDown", modifiers); + await dispatchKeyEvent(Input, key, "keyUp", modifiers); +} + +async function checkBackspace(Input, expected, modifiers = 0) { + info("Send Backspace"); + await sendRawKey(Input, "Backspace", modifiers); + await checkInputContent(expected, expected.length); +} + +async function resetEvents() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.resetEvents(); + const events = content.wrappedJSObject.allEvents; + is(events.length, 0, "List of events should be empty"); + }); +} + +function resetInput(value = "") { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [value], function (arg) { + const input = content.document.querySelector("input"); + input.value = arg; + input.focus(); + }); +} diff --git a/remote/cdp/test/browser/io/browser.toml b/remote/cdp/test/browser/io/browser.toml new file mode 100644 index 0000000000..5b43f8b9fa --- /dev/null +++ b/remote/cdp/test/browser/io/browser.toml @@ -0,0 +1,23 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_close.js"] + +["browser_read.js"] diff --git a/remote/cdp/test/browser/io/browser_close.js b/remote/cdp/test/browser/io/browser_close.js new file mode 100644 index 0000000000..37a10b4963 --- /dev/null +++ b/remote/cdp/test/browser/io/browser_close.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function fileRemovedAfterClose({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle, stream } = await registerFileStream(contents); + + await IO.close({ handle }); + ok( + !(await IOUtils.exists(stream.path)), + "Discarded the temporary backing storage" + ); +}); + +add_task(async function unknownHandle({ client }) { + const { IO } = client; + const handle = "1000000"; + + await Assert.rejects( + IO.close({ handle }), + err => err.message.includes(`Invalid stream handle`), + "Error contains expected message" + ); +}); + +add_task(async function invalidHandleTypes({ client }) { + const { IO } = client; + for (const handle of [null, true, 1, [], {}]) { + await Assert.rejects( + IO.close({ handle }), + err => err.message.includes(`handle: string value expected`), + "Error contains expected message" + ); + } +}); diff --git a/remote/cdp/test/browser/io/browser_read.js b/remote/cdp/test/browser/io/browser_read.js new file mode 100644 index 0000000000..4af05e4ba2 --- /dev/null +++ b/remote/cdp/test/browser/io/browser_read.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function seekByOffsets({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const offset of [0, 5, 10, 100, 0, -1]) { + const result = await IO.read({ handle, offset }); + ok(result.base64Encoded, `Data for offset ${offset} is base64 encoded`); + ok(result.eof, `All data has been read for offset ${offset}`); + is( + atob(result.data), + contents.substring(offset >= 0 ? offset : 0), + `Found expected data for offset ${offset}` + ); + } +}); + +add_task(async function remembersOffsetAfterRead({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + let expectedOffset = 0; + const size = 3; + do { + const result = await IO.read({ handle, size }); + is( + atob(result.data), + contents.substring(expectedOffset, expectedOffset + size), + `Found expected data for expectedOffset ${expectedOffset}` + ); + ok( + result.base64Encoded, + `Data up to expected offset ${expectedOffset} is base64 encoded` + ); + + is( + result.eof, + expectedOffset + size >= contents.length, + `All data has been read up to expected offset ${expectedOffset}` + ); + + expectedOffset = Math.min(expectedOffset + size, contents.length); + } while (expectedOffset < contents.length); +}); + +add_task(async function readBySize({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const size of [0, 5, 10, 100, 0, -1]) { + const result = await IO.read({ handle, offset: 0, size }); + ok(result.base64Encoded, `Data for size ${size} is base64 encoded`); + is( + result.eof, + size >= contents.length, + `All data has been read for size ${size}` + ); + is( + atob(result.data), + contents.substring(0, size), + `Found expected data for size ${size}` + ); + } +}); + +add_task(async function readAfterClose({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + + // If we omit remove: false, then by the time the registered cleanup function + // runs we will have deleted our temp file (in the following call to IO.close) + // *but* another test will have created a file with the same name (due to the + // way IOUtils.createUniqueFile works). That file's stream will not be closed + // and so we won't be able to delete it, resulting in an exception and + // therefore a test failure. + const { handle, stream } = await registerFileStream(contents, { + remove: false, + }); + + await IO.close({ handle }); + + ok(!(await IOUtils.exists(stream.path)), "File should no longer exist"); + + await Assert.rejects( + IO.read({ handle }), + err => err.message.includes(`Invalid stream handle`), + "Error contains expected message" + ); +}); + +add_task(async function unknownHandle({ client }) { + const { IO } = client; + const handle = "1000000"; + + await Assert.rejects( + IO.read({ handle }), + err => err.message.includes(`Invalid stream handle`), + "Error contains expected message" + ); +}); + +add_task(async function invalidHandleTypes({ client }) { + const { IO } = client; + for (const handle of [null, true, 1, [], {}]) { + await Assert.rejects( + IO.read({ handle }), + err => err.message.includes(`handle: string value expected`), + "Error contains expected message" + ); + } +}); + +add_task(async function invalidOffsetTypes({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const offset of [null, true, "1", [], {}]) { + await Assert.rejects( + IO.read({ handle, offset }), + err => err.message.includes(`offset: integer value expected`), + "Error contains expected message" + ); + } +}); + +add_task(async function invalidSizeTypes({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const size of [null, true, "1", [], {}]) { + await Assert.rejects( + IO.read({ handle, size }), + err => err.message.includes(`size: integer value expected`), + "Error contains expected message" + ); + } +}); diff --git a/remote/cdp/test/browser/io/head.js b/remote/cdp/test/browser/io/head.js new file mode 100644 index 0000000000..4c6a67178e --- /dev/null +++ b/remote/cdp/test/browser/io/head.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); + +const { streamRegistry } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/domains/parent/IO.sys.mjs" +); + +async function registerFileStream(contents, options) { + const stream = await createFileStream(contents, options); + const handle = streamRegistry.add(stream); + + return { handle, stream }; +} diff --git a/remote/cdp/test/browser/log/browser.toml b/remote/cdp/test/browser/log/browser.toml new file mode 100644 index 0000000000..728e461609 --- /dev/null +++ b/remote/cdp/test/browser/log/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_entryAdded.js"] diff --git a/remote/cdp/test/browser/log/browser_entryAdded.js b/remote/cdp/test/browser/log/browser_entryAdded.js new file mode 100644 index 0000000000..1c6ba2e0a8 --- /dev/null +++ b/remote/cdp/test/browser/log/browser_entryAdded.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventsWhenLogDomainDisabled({ client }) { + await runEntryAddedTest(client, 0, async () => { + await throwScriptError("foo"); + }); +}); + +add_task(async function noEventsAfterLogDomainDisabled({ client }) { + const { Log } = client; + + await Log.enable(); + await Log.disable(); + + await runEntryAddedTest(client, 0, async () => { + await throwScriptError("foo"); + }); +}); + +add_task(async function noEventsForConsoleMessageWithException({ client }) { + const { Log } = client; + + await Log.enable(); + + const context = await enableRuntime(client); + await runEntryAddedTest(client, 0, async () => { + evaluate(client, context.id, () => { + const foo = {}; + foo.bar(); + }); + }); +}); + +add_task(async function eventsForScriptErrorWithoutException({ client }) { + const { Log } = client; + + await Log.enable(); + + await enableRuntime(client); + const events = await runEntryAddedTest(client, 1, async () => { + throwScriptError({ + text: "foo", + sourceName: "https://foo.bar", + lineNumber: 7, + category: "javascript", + }); + }); + + is(events[0].source, "javascript", "Got expected source"); + is(events[0].level, "error", "Got expected level"); + is(events[0].text, "foo", "Got expected text"); + is(events[0].url, "https://foo.bar", "Got expected url"); + is(events[0].lineNumber, 7, "Got expected line number"); +}); + +add_task(async function eventsForScriptErrorLevels({ client }) { + const { Log } = client; + + await Log.enable(); + + const flags = { + info: Ci.nsIScriptError.infoFlag, + warning: Ci.nsIScriptError.warningFlag, + error: Ci.nsIScriptError.errorFlag, + }; + + await enableRuntime(client); + for (const [level, flag] of Object.entries(flags)) { + const events = await runEntryAddedTest(client, 1, async () => { + throwScriptError({ text: level, flag }); + }); + + is(events[0].text, level, "Got expected text"); + is(events[0].level, level, "Got expected level"); + } +}); + +add_task(async function eventsForScriptErrorContent({ client }) { + const { Log } = client; + + await Log.enable(); + + const context = await enableRuntime(client); + const events = await runEntryAddedTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.execCommand("copy"); + }); + }); + + is(events[0].source, "other", "Got expected source"); + is(events[0].level, "warning", "Got expected level"); + ok( + events[0].text.includes("document.execCommand(‘cut’/‘copy’) was denied"), + "Got expected text" + ); + is(events[0].url, undefined, "Got undefined url"); + is(events[0].lineNumber, 2, "Got expected line number"); +}); + +async function runEntryAddedTest(client, eventCount, callback, options = {}) { + const { Log } = client; + + const EVENT_ENTRY_ADDED = "Log.entryAdded"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Log.entryAdded, + eventName: EVENT_ENTRY_ADDED, + messageFn: payload => `Received "${EVENT_ENTRY_ADDED}"`, + }); + + const timeBefore = Date.now(); + await callback(); + + const entryAddedEvents = await history.record(); + is(entryAddedEvents.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for entryAdded events + entryAddedEvents.forEach(event => { + const timestamp = event.payload.entry.timestamp; + + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + "Got valid timestamp" + ); + }); + + return entryAddedEvents.map(event => event.payload.entry); +} diff --git a/remote/cdp/test/browser/log/head.js b/remote/cdp/test/browser/log/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/log/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/network/browser.toml b/remote/cdp/test/browser/network/browser.toml new file mode 100644 index 0000000000..8ec02a25d7 --- /dev/null +++ b/remote/cdp/test/browser/network/browser.toml @@ -0,0 +1,55 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", + "doc_empty.html", + "doc_frameset.html", + "doc_get_cookies_frame.html", + "doc_get_cookies_page.html", + "doc_networkEvents.html", + "file_networkEvents.js", + "file_framesetEvents.js", + "sjs-cookies.sjs", +] + +["browser_deleteCookies.js"] +https_first_disabled = true + +["browser_emulateNetworkConditions.js"] + +["browser_getAllCookies.js"] +https_first_disabled = true + +["browser_getCookies.js"] +https_first_disabled = true + +["browser_navigationEvents.js"] +https_first_disabled = true + +["browser_requestWillBeSent.js"] +https_first_disabled = true + +["browser_responseReceived.js"] +https_first_disabled = true + +["browser_setCacheDisabled.js"] + +["browser_setCookie.js"] + +["browser_setCookies.js"] + +["browser_setUserAgentOverride.js"] diff --git a/remote/cdp/test/browser/network/browser_deleteCookies.js b/remote/cdp/test/browser/network/browser_deleteCookies.js new file mode 100644 index 0000000000..1b5b0779d5 --- /dev/null +++ b/remote/cdp/test/browser/network/browser_deleteCookies.js @@ -0,0 +1,299 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/cdp/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "http://example.org"; +const DEFAULT_HOSTNAME = "example.org"; +const ALT_HOST = "http://example.net"; +const SECURE_HOST = "https://example.com"; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.deleteCookies(), + err => err.message.includes("name: string value expected"), + "Fails without any arguments" + ); +}); + +add_task(async function failureWithoutDomainOrURL({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.deleteCookies({ name: "foo" }), + err => + err.message.includes( + "At least one of the url and domain needs to be specified" + ), + "Fails without domain or URL" + ); +}); + +add_task(async function failureWithInvalidProtocol({ client }) { + const { Network } = client; + + const FTP_URL = `ftp://${DEFAULT_HOSTNAME}`; + + await Assert.rejects( + Network.deleteCookies({ name: "foo", url: FTP_URL }), + err => err.message.includes("An http or https url must be specified"), + "Fails for invalid protocol in URL" + ); +}); + +add_task(async function pristineContext({ client }) { + const { Network } = client; + + await loadURL(DEFAULT_URL); + + const { cookies } = await Network.getCookies(); + is(cookies.length, 0, "No cookies have been found"); + + await Network.deleteCookies({ name: "foo", url: DEFAULT_URL }); +}); + +add_task(async function fromHostWithPort({ client }) { + const { Network } = client; + + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}`; + await loadURL(PORT_URL + "?name=id&value=1"); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: PORT_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificDomain({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + domain: DEFAULT_HOSTNAME, + }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificURL({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: DEFAULT_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSecureURL({ client }) { + const { Network } = client; + + const SECURE_URL = `${SECURE_HOST}${SJS_PATH}`; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + await loadURL(`${SECURE_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.org", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: SECURE_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(DEFAULT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificDomainAndURL({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + // Domain has precedence before URL + await Network.deleteCookies({ + name: cookie.name, + domain: DEFAULT_HOSTNAME, + url: ALT_URL, + }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + + const PATH = "/browser/remote/cdp/test/browser/"; + const PARENT_PATH = "/browser/remote/cdp/test/"; + const SUB_PATH = "/browser/remote/cdp/test/browser/network/"; + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + let result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookie has been found"); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: SUB_PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: PARENT_PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookie has been found"); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: "/foo/bar", + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); diff --git a/remote/cdp/test/browser/network/browser_emulateNetworkConditions.js b/remote/cdp/test/browser/network/browser_emulateNetworkConditions.js new file mode 100644 index 0000000000..858e551aca --- /dev/null +++ b/remote/cdp/test/browser/network/browser_emulateNetworkConditions.js @@ -0,0 +1,208 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const pageEmptyURL = + "https://example.com/browser/remote/cdp/test/browser/page/doc_empty.html"; + +/* + * Set the optional preference to disallow access to localhost when offline. This is + * required because `example.com` resolves to `localhost` in the tests and therefore + * would still be accessible even though we are simulating being offline. + * By setting this preference, we make sure that these connections to `localhost` + * (and by extension, to `example.com`) will fail when we are offline. + */ +Services.prefs.setBoolPref("network.disable-localhost-when-offline", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.disable-localhost-when-offline"); +}); + +/** + * Acts just as `add_task`, but does cleanup afterwards + * + * @param {Function} taskFn + */ +function add_networking_task(taskFn) { + add_task(async client => { + try { + await taskFn(client); + } finally { + Services.io.offline = false; + } + }); +} + +add_networking_task(async function offlineWithoutArguments({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.emulateNetworkConditions(), + /offline: boolean value expected/, + "Fails without any arguments" + ); +}); + +add_networking_task(async function offlineWithEmptyArguments({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.emulateNetworkConditions({}), + /offline: boolean value expected/, + "Fails with only empty arguments" + ); +}); + +add_networking_task(async function offlineWithInvalidArguments({ client }) { + const { Network } = client; + const testTable = [null, undefined, 1, "foo", [], {}]; + + for (const testCase of testTable) { + const testType = typeof testCase; + await Assert.rejects( + Network.emulateNetworkConditions({ offline: testCase }), + /offline: boolean value expected/, + `Fails with ${testType}-type argument for offline` + ); + } +}); + +add_networking_task(async function offlineWithUnsupportedArguments({ client }) { + const { Network } = client; + + // Random valid values for the Network.emulateNetworkConditions command, even though we don't support them yet + const args = { + offline: true, + latency: 500, + downloadThroughput: 500, + uploadThroughput: 500, + connectionType: "cellular2g", + someFutureArg: false, + }; + + await Network.emulateNetworkConditions(args); + + ok(true, "No errors should be thrown due to non-implemented arguments"); +}); + +add_networking_task(async function emulateOfflineWhileOnline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert we really can't navigate after setting offline + await assertOfflineNavigationFails(); +}); + +add_networking_task(async function emulateOfflineWhileOffline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert for no-offline event, because we're offline - and changing to offline - so nothing changes + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert we still can't navigate after setting offline twice + await assertOfflineNavigationFails(); +}); + +add_networking_task(async function emulateOnlineWhileOnline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for no-offline event, because we're online - and changing to online - so nothing changes + await Network.emulateNetworkConditions({ offline: false }); + await assertOfflineStatus(false); +}); + +add_networking_task(async function emulateOnlineWhileOffline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline event, because we're online - and changing to offline + const offlineChanged = Promise.race([ + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "online", + true + ), + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "offline", + true + ), + ]); + + await Network.emulateNetworkConditions({ offline: true }); + + info("Waiting for offline event on window"); + is(await offlineChanged, "offline", "Only the offline-event should fire"); + await assertOfflineStatus(true); + + // Assert for online event, because we're offline - and changing to online + const offlineChangedBack = Promise.race([ + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "online", + true + ), + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "offline", + true + ), + ]); + await Network.emulateNetworkConditions({ offline: false }); + + info("Waiting for online event on window"); + is(await offlineChangedBack, "online", "Only the online-event should fire"); + await assertOfflineStatus(false); +}); + +/** + * Navigates to a page, and asserting any status code to appear + */ +async function assertOfflineNavigationFails() { + const browser = gBrowser.selectedTab.linkedBrowser; + let netErrorLoaded = BrowserTestUtils.waitForErrorPage(browser); + + BrowserTestUtils.startLoadingURIString(browser, pageEmptyURL); + await netErrorLoaded; +} + +/** + * Checks on the page what the value of window.navigator.onLine is on the currently navigated page + * + * @param {boolean} offline + * True if offline is expected + */ +function assertOfflineStatus(offline) { + is( + Services.io.offline, + offline, + "Services.io.offline should be " + (offline ? "true" : "false") + ); + + return SpecialPowers.spawn(gBrowser.selectedBrowser, [offline], offline => { + is( + content.navigator.onLine, + !offline, + "Page should be " + (offline ? "offline" : "online") + ); + }); +} diff --git a/remote/cdp/test/browser/network/browser_getAllCookies.js b/remote/cdp/test/browser/network/browser_getAllCookies.js new file mode 100644 index 0000000000..2e2d404410 --- /dev/null +++ b/remote/cdp/test/browser/network/browser_getAllCookies.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/cdp/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "http://example.org"; +const ALT_HOST = "http://example.net"; +const SECURE_HOST = "https://example.com"; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function noCookiesWhenNoneAreSet({ client }) { + const { Network } = client; + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 0, "No cookies have been found"); +}); + +add_task(async function noCookiesForPristineContext({ client }) { + const { Network } = client; + await loadURL(DEFAULT_URL); + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromHostWithPort({ client }) { + const { Network } = client; + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}?name=id&value=1`; + await loadURL(PORT_URL); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "All cookies have been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromMultipleOrigins({ client }) { + const { Network } = client; + await loadURL(`${ALT_HOST}${SJS_PATH}?name=users&value=password`); + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=secure&value=password`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie1 = { name: "foo", value: "bar", domain: "example.org" }; + const cookie2 = { name: "secure", value: "password", domain: "example.com" }; + const cookie3 = { name: "users", value: "password", domain: "example.net" }; + + try { + const { cookies } = await Network.getAllCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, 3, "All cookies have been found"); + assertCookie(cookies[0], cookie1); + assertCookie(cookies[1], cookie2); + assertCookie(cookies[2], cookie3); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function secure({ client }) { + const { Network } = client; + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=foo&value=bar&secure`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + // Cookie returned for secure protocols + let result = await Network.getAllCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + + // For unsecure protocols the secure cookies are also returned + await loadURL(DEFAULT_URL); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function expiry({ client }) { + const { Network } = client; + const date = new Date(); + date.setDate(date.getDate() + 3); + + const encodedDate = encodeURI(date.toUTCString()); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&expiry=${encodedDate}`); + + const cookie = { + name: "foo", + value: "bar", + expires: Math.floor(date.getTime() / 1000), + session: false, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function session({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + expiry: -1, + session: true, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + const PATH = "/browser/remote/cdp/test/browser/"; + const PARENT_PATH = "/browser/remote/cdp/test/"; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_HOST}${PATH}`); + let result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_HOST}${SJS_PATH}`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_HOST}${PARENT_PATH}`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_HOST}/foo/bar`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function httpOnly({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&httpOnly`); + + const cookie = { + name: "foo", + value: "bar", + httpOnly: true, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function sameSite({ client }) { + const { Network } = client; + for (const value of ["Lax", "Strict"]) { + console.log(`Test cookie with SameSite=${value}`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&sameSite=${value}`); + + const cookie = { + name: "foo", + value: "bar", + sameSite: value, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); diff --git a/remote/cdp/test/browser/network/browser_getCookies.js b/remote/cdp/test/browser/network/browser_getCookies.js new file mode 100644 index 0000000000..99fedb86be --- /dev/null +++ b/remote/cdp/test/browser/network/browser_getCookies.js @@ -0,0 +1,351 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const DEFAULT_DOMAIN = "example.org"; +const ALT_DOMAIN = "example.net"; +const SECURE_DOMAIN = "example.com"; + +const DEFAULT_HOST = "http://" + DEFAULT_DOMAIN; +const ALT_HOST = "http://" + ALT_DOMAIN; +const SECURE_HOST = "https://" + SECURE_DOMAIN; + +const BASE_PATH = "/browser/remote/cdp/test/browser/network"; +const SJS_PATH = `${BASE_PATH}/sjs-cookies.sjs`; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function noCookiesWhenNoneAreSet({ client }) { + const { Network } = client; + const { cookies } = await Network.getCookies({ urls: [DEFAULT_HOST] }); + is(cookies.length, 0, "No cookies have been found"); +}); + +add_task(async function noCookiesForPristineContext({ client }) { + const { Network } = client; + await loadURL(DEFAULT_URL); + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromHostWithPort({ client }) { + const { Network } = client; + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}?name=id&value=1`; + await loadURL(PORT_URL); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "All cookies have been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromCurrentURL({ client }) { + const { Network } = client; + await loadURL(`${ALT_HOST}${SJS_PATH}?name=user&value=password`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=user&value=password`); + + const cookie1 = { name: "foo", value: "bar", domain: "example.org" }; + const cookie2 = { name: "user", value: "password", domain: "example.org" }; + + try { + const { cookies } = await Network.getCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, 2, "All cookies have been found"); + assertCookie(cookies[0], cookie1); + assertCookie(cookies[1], cookie2); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesIncludingSubFrames({ client }) { + const GET_COOKIES_PAGE_URL = `${DEFAULT_HOST}${BASE_PATH}/doc_get_cookies_page.html`; + + const { Network } = client; + await loadURL(GET_COOKIES_PAGE_URL); + + const cookie_page = { name: "page", value: "mainpage", path: BASE_PATH }; + const cookie_frame = { name: "frame", value: "subframe", path: BASE_PATH }; + + try { + const { cookies } = await Network.getCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, 2, "All cookies have been found including subframe"); + assertCookie(cookies[0], cookie_frame); + assertCookie(cookies[1], cookie_page); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function secure({ client }) { + const { Network } = client; + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=foo&value=bar&secure`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + // Cookie returned for secure protocols + let result = await Network.getCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + + // For unsecure protocols no secure cookies are returned + await loadURL(DEFAULT_URL); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No secure cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function expiry({ client }) { + const { Network } = client; + const date = new Date(); + date.setDate(date.getDate() + 3); + + const encodedDate = encodeURI(date.toUTCString()); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&expiry=${encodedDate}`); + + const cookie = { + name: "foo", + value: "bar", + expires: Math.floor(date.getTime() / 1000), + session: false, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function session({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + expiry: -1, + session: true, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + const PATH = "/browser/remote/cdp/test/browser/"; + const PARENT_PATH = "/browser/remote/cdp/test/"; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_HOST}${PATH}`); + let result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_HOST}${SJS_PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_HOST}${PARENT_PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookies have been found"); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_HOST}/foo/bar`); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function httpOnly({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&httpOnly`); + + const cookie = { + name: "foo", + value: "bar", + httpOnly: true, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function sameSite({ client }) { + const { Network } = client; + for (const value of ["Lax", "Strict"]) { + console.log(`Test cookie with SameSite=${value}`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&sameSite=${value}`); + + const cookie = { + name: "foo", + value: "bar", + sameSite: value, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); + +add_task(async function testUrlsMissing({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_HOST}${BASE_PATH}/doc_get_cookies_page.html`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + await loadURL(`${ALT_HOST}${SJS_PATH}?name=alt&value=true`); + + const cookie = { + name: "alt", + value: "true", + domain: ALT_DOMAIN, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function testUrls({ client }) { + const { Network } = client; + await loadURL(`${SECURE_HOST}${BASE_PATH}/doc_get_cookies_page.html`); + await loadURL(`${DEFAULT_HOST}${BASE_PATH}/doc_get_cookies_page.html`); + await loadURL(`${ALT_HOST}${SJS_PATH}?name=alt&value=true`); + + const cookie1 = { + name: "page", + value: "mainpage", + path: BASE_PATH, + domain: DEFAULT_DOMAIN, + }; + const cookie2 = { + name: "frame", + value: "subframe", + path: BASE_PATH, + domain: DEFAULT_DOMAIN, + }; + const cookie3 = { + name: "page", + value: "mainpage", + path: BASE_PATH, + domain: SECURE_DOMAIN, + }; + const cookie4 = { + name: "frame", + value: "subframe", + path: BASE_PATH, + domain: SECURE_DOMAIN, + }; + + try { + const { cookies } = await Network.getCookies({ + urls: [`${DEFAULT_HOST}${BASE_PATH}`, `${SECURE_HOST}${BASE_PATH}`], + }); + is(cookies.length, 4, "4 cookies have been found"); + assertCookie(cookies[0], cookie1); + assertCookie(cookies[1], cookie2); + assertCookie(cookies[2], cookie3); + assertCookie(cookies[3], cookie4); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function testUrlsInvalidTypes({ client }) { + const { Network } = client; + + const testTable = [null, 1, "foo", true, {}]; + + for (const testCase of testTable) { + await Assert.rejects( + Network.getCookies({ urls: testCase }), + /urls: array expected/, + `Fails the argument type for urls` + ); + } +}); + +add_task(async function testUrlsEntriesInvalidTypes({ client }) { + const { Network } = client; + + const testTable = [[null], [1], [true]]; + + for (const testCase of testTable) { + await Assert.rejects( + Network.getCookies({ urls: testCase }), + /urls: string value expected at index 0/, + `Fails the argument type for urls` + ); + } +}); + +add_task(async function testUrlsEmpty({ client }) { + const { Network } = client; + + const { cookies } = await Network.getCookies({ urls: [] }); + is(cookies.length, 0, "No cookies returned"); +}); diff --git a/remote/cdp/test/browser/network/browser_navigationEvents.js b/remote/cdp/test/browser/network/browser_navigationEvents.js new file mode 100644 index 0000000000..57680c2a57 --- /dev/null +++ b/remote/cdp/test/browser/network/browser_navigationEvents.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test order and consistency of Network/Page events as a whole. +// Details of specific events are checked in event-specific test files. + +// Bug 1734694: network request header mismatch when using HTTPS +const BASE_PATH = "http://example.com/browser/remote/cdp/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function eventsForTopFrameNavigation({ client }) { + const { history, frameId: frameIdNav } = await prepareTest( + client, + FRAMESET_URL, + 10 + ); + + const documentEvents = filterEventsByType(history, "Document"); + const scriptEvents = filterEventsByType(history, "Script"); + const subdocumentEvents = filterEventsByType(history, "Subdocument"); + + is(documentEvents.length, 2, "Expected number of Document events"); + is(subdocumentEvents.length, 2, "Expected number of Subdocument events"); + is(scriptEvents.length, 4, "Expected number of Script events"); + + const navigatedEvents = history.findEvents("Page.navigate"); + is(navigatedEvents.length, 1, "Expected number of navigate done events"); + + const frameAttachedEvents = history.findEvents("Page.frameAttached"); + is(frameAttachedEvents.length, 1, "Expected number of frame attached events"); + + // network events for document and script + assertEventOrder(documentEvents[0], documentEvents[1]); + assertEventOrder(documentEvents[1], navigatedEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(navigatedEvents[0], scriptEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(scriptEvents[0], scriptEvents[1]); + + const docRequest = documentEvents[0].payload; + is(docRequest.documentURL, FRAMESET_URL, "documenURL matches target url"); + is(docRequest.frameId, frameIdNav, "Got the expected frame id"); + is(docRequest.request.url, FRAMESET_URL, "Got the Document request"); + + const docResponse = documentEvents[1].payload; + is(docResponse.frameId, frameIdNav, "Got the expected frame id"); + is(docResponse.response.url, FRAMESET_URL, "Got the Document response"); + ok(!!docResponse.response.headers.server, "Document response has headers"); + // TODO? response reports extra request header "upgrade-insecure-requests":"1" + // Assert.deepEqual( + // docResponse.response.requestHeaders, + // docRequest.request.headers, + // "Response event reports same request headers as request event" + // ); + + const scriptRequest = scriptEvents[0].payload; + is( + scriptRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document" + ); + is(scriptRequest.frameId, frameIdNav, "Got the expected frame id"); + is(scriptRequest.request.url, FRAMESET_JS_URL, "Got the Script request"); + + const scriptResponse = scriptEvents[1].payload; + is(scriptResponse.frameId, frameIdNav, "Got the expected frame id"); + todo( + scriptResponse.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent responses (Bug 1637838)" + ); + is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response"); + Assert.deepEqual( + scriptResponse.response.requestHeaders, + scriptRequest.request.headers, + "Response event reports same request headers as request event" + ); + + // frame is attached after all resources of the document have been loaded + // and before sub document starts loading + assertEventOrder(scriptEvents[1], frameAttachedEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(frameAttachedEvents[0], subdocumentEvents[0], { + ignoreTimestamps: true, + }); + + const { frameId: frameIdSubFrame, parentFrameId } = + frameAttachedEvents[0].payload; + is(parentFrameId, frameIdNav, "Got expected parent frame id"); + + // network events for subdocument and script + assertEventOrder(subdocumentEvents[0], subdocumentEvents[1]); + assertEventOrder(subdocumentEvents[1], scriptEvents[2]); + assertEventOrder(scriptEvents[2], scriptEvents[3]); + + const subdocRequest = subdocumentEvents[0].payload; + is( + subdocRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document" + ); + is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request"); + + const subdocResponse = subdocumentEvents[1].payload; + is(subdocResponse.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subdocResponse.response.url, PAGE_URL, "Got the Subdocument response"); + + const subscriptRequest = scriptEvents[2].payload; + is(subscriptRequest.documentURL, PAGE_URL, "documentURL is trigger document"); + is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subscriptRequest.request.url, PAGE_JS_URL, "Got the Script request"); + + const subscriptResponse = scriptEvents[3].payload; + is(subscriptResponse.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subscriptResponse.response.url, PAGE_JS_URL, "Got the Script response"); + todo( + subscriptResponse.loaderId === subdocRequest.loaderId, + "The same loaderId is used for dependent responses (Bug 1637838)" + ); + Assert.deepEqual( + subscriptResponse.response.requestHeaders, + subscriptRequest.request.headers, + "Response event reports same request headers as request event" + ); + + const lifeCycleEvents = history + .findEvents("Page.lifecycleEvent") + .map(event => event.payload); + for (const { name, loaderId } of lifeCycleEvents) { + is( + loaderId, + docRequest.loaderId, + `${name} lifecycle event has same loaderId as Document request` + ); + } +}); + +async function prepareTest(client, url, totalCount) { + const REQUEST = "Network.requestWillBeSent"; + const RESPONSE = "Network.responseReceived"; + const FRAMEATTACHED = "Page.frameAttached"; + const LIFECYCLE = "Page.livecycleEvent"; + + const { Network, Page } = client; + const history = new RecordEvents(totalCount); + + history.addRecorder({ + event: Network.requestWillBeSent, + eventName: REQUEST, + messageFn: payload => { + return `Received ${REQUEST} for ${payload.request?.url}`; + }, + }); + + history.addRecorder({ + event: Network.responseReceived, + eventName: RESPONSE, + messageFn: payload => { + return `Received ${RESPONSE} for ${payload.response?.url}`; + }, + }); + + history.addRecorder({ + event: Page.frameAttached, + eventName: FRAMEATTACHED, + messageFn: ({ frameId, parentFrameId: parentId }) => { + return `Received ${FRAMEATTACHED} frame=${frameId} parent=${parentId}`; + }, + }); + + history.addRecorder({ + event: Page.lifecycleEvent, + eventName: LIFECYCLE, + messageFn: payload => { + return `Received ${LIFECYCLE} ${payload.name}`; + }, + }); + + await Network.enable(); + await Page.enable(); + + const navigateDone = history.addPromise("Page.navigate"); + const { frameId } = await Page.navigate({ url }).then(navigateDone); + ok(frameId, "Page.navigate returned a frameId"); + + info("Wait for events"); + const events = await history.record(); + + info(`Received events: ${events.map(getDescriptionForEvent)}`); + is(events.length, totalCount, "Received expected number of events"); + + return { history, frameId }; +} diff --git a/remote/cdp/test/browser/network/browser_requestWillBeSent.js b/remote/cdp/test/browser/network/browser_requestWillBeSent.js new file mode 100644 index 0000000000..9a4745952c --- /dev/null +++ b/remote/cdp/test/browser/network/browser_requestWillBeSent.js @@ -0,0 +1,224 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_PATH = "https://example.com/browser/remote/cdp/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_URL_JS = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_EMPTY_URL = `${BASE_PATH}/doc_empty.html`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_EMPTY_HASH = `#`; +const PAGE_HASH = `#foo`; +const PAGE_URL_WITH_HASH = `${PAGE_URL}${PAGE_HASH}`; +const PAGE_URL_WITH_EMPTY_HASH = `${PAGE_URL}${PAGE_EMPTY_HASH}`; +const PAGE_URL_JS = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function noEventsWhenNetworkDomainDisabled({ client }) { + const history = configureHistory(client, 0); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function noEventsAfterNetworkDomainDisabled({ client }) { + const { Network } = client; + + const history = configureHistory(client, 0); + await Network.enable(); + await Network.disable(); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function documentNavigationWithResource({ client }) { + const { Page, Network } = client; + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameAttached = Page.frameAttached(); + const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL }); + const { frameId: frameIdSubFrame } = await frameAttached; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 4, "Expected number of Network.requestWillBeSent events"); + + // Check top-level document request + const docRequest = events[0].payload; + is(docRequest.type, "Document", "Document request has the expected type"); + is(docRequest.documentURL, FRAMESET_URL, "documentURL matches requested url"); + is(docRequest.frameId, frameIdNav, "Got the expected frame id"); + is(docRequest.request.url, FRAMESET_URL, "Got the Document request"); + is(docRequest.request.urlFragment, undefined, "Has no URL fragment set"); + is(docRequest.request.method, "GET", "Has the expected request method"); + is( + docRequest.requestId, + docRequest.loaderId, + "The request id is equal to the loader id" + ); + is( + docRequest.request.headers.host, + "example.com", + "Document request has headers" + ); + + // Check top-level script request + const scriptRequest = events[1].payload; + is(scriptRequest.type, "Script", "Script request has the expected type"); + is( + scriptRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document for the script request" + ); + is(scriptRequest.frameId, frameIdNav, "Got the expected frame id"); + is(scriptRequest.request.url, FRAMESET_URL_JS, "Got the Script request"); + is(scriptRequest.request.method, "GET", "Has the expected request method"); + is( + scriptRequest.request.headers.host, + "example.com", + "Script request has headers" + ); + todo( + scriptRequest.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent requests (Bug 1637838)" + ); + assertEventOrder(events[0], events[1]); + + // Check subdocument request + const subdocRequest = events[2].payload; + is( + subdocRequest.type, + "Subdocument", + "Subdocument request has the expected type" + ); + is(subdocRequest.documentURL, FRAMESET_URL, "documenURL matches request url"); + is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is( + subdocRequest.requestId, + subdocRequest.loaderId, + "The request id is equal to the loader id" + ); + is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request"); + is(subdocRequest.request.method, "GET", "Has the expected request method"); + is( + subdocRequest.request.headers.host, + "example.com", + "Subdocument request has headers" + ); + assertEventOrder(events[1], events[2]); + + // Check script request (frame) + const subscriptRequest = events[3].payload; + is(subscriptRequest.type, "Script", "Script request has the expected type"); + is( + subscriptRequest.documentURL, + PAGE_URL, + "documentURL is trigger document for the script request" + ); + is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + todo( + subscriptRequest.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent requests (Bug 1637838)" + ); + is(subscriptRequest.request.url, PAGE_URL_JS, "Got the Script request"); + is( + subscriptRequest.request.method, + "GET", + "Script request has the expected method" + ); + is( + subscriptRequest.request.headers.host, + "example.com", + "Script request has headers" + ); + assertEventOrder(events[2], events[3]); +}); + +add_task(async function documentNavigationToURLWithHash({ client }) { + const { Page, Network } = client; + + await loadURL(PAGE_EMPTY_URL); + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameNavigated = Page.frameNavigated(); + const { frameId: frameIdNav } = await Page.navigate({ + url: PAGE_URL_WITH_HASH, + }); + await frameNavigated; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 2, "Expected number of Network.requestWillBeSent events"); + + // Check top-level document request only for fragment usage + const docRequest = events[0].payload; + is(docRequest.documentURL, PAGE_URL, "documentURL matches requested URL"); + is(docRequest.request.url, PAGE_URL, "Request url matches requested URL"); + is( + docRequest.request.urlFragment, + PAGE_HASH, + "Request URL fragment is present" + ); +}); + +add_task(async function documentNavigationToURLWithEmptyHash({ client }) { + const { Page, Network } = client; + + await loadURL(PAGE_EMPTY_URL); + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameNavigated = Page.frameNavigated(); + const { frameId: frameIdNav } = await Page.navigate({ + url: PAGE_URL_WITH_EMPTY_HASH, + }); + await frameNavigated; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 2, "Expected number of Network.requestWillBeSent events"); + + // Check top-level document request only for fragment usage + const docRequest = events[0].payload; + is(docRequest.documentURL, PAGE_URL, "documentURL matches requested URL"); + is(docRequest.request.url, PAGE_URL, "Request url matches requested URL"); + is( + docRequest.request.urlFragment, + PAGE_EMPTY_HASH, + "Request URL fragment is present" + ); +}); + +function configureHistory(client, total) { + const REQUEST = "Network.requestWillBeSent"; + + const { Network } = client; + const history = new RecordEvents(total); + + history.addRecorder({ + event: Network.requestWillBeSent, + eventName: REQUEST, + messageFn: payload => { + return `Received ${REQUEST} for ${payload.request.url}`; + }, + }); + + return history; +} diff --git a/remote/cdp/test/browser/network/browser_responseReceived.js b/remote/cdp/test/browser/network/browser_responseReceived.js new file mode 100644 index 0000000000..41e854de8a --- /dev/null +++ b/remote/cdp/test/browser/network/browser_responseReceived.js @@ -0,0 +1,236 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_PATH = "https://example.com/browser/remote/cdp/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function noEventsWhenNetworkDomainDisabled({ client }) { + const history = configureHistory(client, 0); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function noEventsAfterNetworkDomainDisabled({ client }) { + const { Network } = client; + + const history = configureHistory(client, 0); + await Network.enable(); + await Network.disable(); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function documentNavigationWithResource({ client }) { + const { Page, Network } = client; + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameAttached = Page.frameAttached(); + const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL }); + const { frameId: frameIdSubframe } = await frameAttached; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 4, "Expected number of Network.responseReceived events"); + + // Check top-level document response + const docResponse = events[0].payload; + is(docResponse.type, "Document", "Document response has expected type"); + is(docResponse.frameId, frameIdNav, "Got the expected frame id"); + is( + docResponse.requestId, + docResponse.loaderId, + "The response id is equal to the loader id" + ); + is(docResponse.response.url, FRAMESET_URL, "Got the Document response"); + is( + docResponse.response.mimeType, + "text/html", + "Document response has expected mimeType" + ); + ok(!!docResponse.response.headers.server, "Document response has headers"); + is(docResponse.response.status, 200, "Document response has expected status"); + is( + docResponse.response.statusText, + "OK", + "Document response has expected status text" + ); + if (docResponse.response.fromDiskCache === false) { + is( + docResponse.response.remoteIPAddress, + "127.0.0.1", + "Document response has the expected IP address" + ); + ok( + typeof docResponse.response.remotePort == "number", + "Document response has a remotePort" + ); + } + is( + docResponse.response.protocol, + "http/1.1", + "Document response has expected protocol" + ); + + // Check top-level script response + const scriptResponse = events[1].payload; + is(scriptResponse.type, "Script", "Script response has expected type"); + is(scriptResponse.frameId, frameIdNav, "Got the expected frame id"); + is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response"); + is( + scriptResponse.response.mimeType, + "application/x-javascript", + "Script response has expected mimeType" + ); + ok(!!scriptResponse.response.headers.server, "Script response has headers"); + is( + scriptResponse.response.status, + 200, + "Script response has the expected status" + ); + is( + scriptResponse.response.statusText, + "OK", + "Script response has the expected status text" + ); + if (scriptResponse.response.fromDiskCache === false) { + is( + scriptResponse.response.remoteIPAddress, + docResponse.response.remoteIPAddress, + "Script response has same IP address as document response" + ); + ok( + typeof scriptResponse.response.remotePort == "number", + "Script response has a remotePort" + ); + } + is( + scriptResponse.response.protocol, + "http/1.1", + "Script response has the expected protocol" + ); + + // Check subdocument response + const frameDocResponse = events[2].payload; + is( + frameDocResponse.type, + "Subdocument", + "Subdocument response has expected type" + ); + is(frameDocResponse.frameId, frameIdSubframe, "Got the expected frame id"); + is( + frameDocResponse.requestId, + frameDocResponse.loaderId, + "The response id is equal to the loader id" + ); + is( + frameDocResponse.response.url, + PAGE_URL, + "Got the expected Document response" + ); + is( + frameDocResponse.response.mimeType, + "text/html", + "Document response has expected mimeType" + ); + ok( + !!frameDocResponse.response.headers.server, + "Subdocument response has headers" + ); + is( + frameDocResponse.response.status, + 200, + "Subdocument response has expected status" + ); + is( + frameDocResponse.response.statusText, + "OK", + "Subdocument response has expected status text" + ); + if (frameDocResponse.response.fromDiskCache === false) { + is( + frameDocResponse.response.remoteIPAddress, + "127.0.0.1", + "Subdocument response has the expected IP address" + ); + ok( + typeof frameDocResponse.response.remotePort == "number", + "Subdocument response has a remotePort" + ); + } + is( + frameDocResponse.response.protocol, + "http/1.1", + "Subdocument response has expected protocol" + ); + + // Check frame script response + const frameScriptResponse = events[3].payload; + is(frameScriptResponse.type, "Script", "Script response has expected type"); + is(frameScriptResponse.frameId, frameIdSubframe, "Got the expected frame id"); + is(frameScriptResponse.response.url, PAGE_JS_URL, "Got the Script response"); + is( + frameScriptResponse.response.mimeType, + "application/x-javascript", + "Script response has expected mimeType" + ); + ok( + !!frameScriptResponse.response.headers.server, + "Script response has headers" + ); + is( + frameScriptResponse.response.status, + 200, + "Script response has the expected status" + ); + is( + frameScriptResponse.response.statusText, + "OK", + "Script response has the expected status text" + ); + if (frameScriptResponse.response.fromDiskCache === false) { + is( + frameScriptResponse.response.remoteIPAddress, + docResponse.response.remoteIPAddress, + "Script response has same IP address as document response" + ); + ok( + typeof frameScriptResponse.response.remotePort == "number", + "Script response has a remotePort" + ); + } + is( + frameScriptResponse.response.protocol, + "http/1.1", + "Script response has the expected protocol" + ); +}); + +function configureHistory(client, total) { + const RESPONSE = "Network.responseReceived"; + + const { Network } = client; + const history = new RecordEvents(total); + + history.addRecorder({ + event: Network.responseReceived, + eventName: RESPONSE, + messageFn: payload => { + return `Received ${RESPONSE} for ${payload.response.url}`; + }, + }); + return history; +} diff --git a/remote/cdp/test/browser/network/browser_setCacheDisabled.js b/remote/cdp/test/browser/network/browser_setCacheDisabled.js new file mode 100644 index 0000000000..4af9f0fc07 --- /dev/null +++ b/remote/cdp/test/browser/network/browser_setCacheDisabled.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { INHIBIT_CACHING, LOAD_BYPASS_CACHE, LOAD_NORMAL } = Ci.nsIRequest; + +const TEST_PAGE = + "https://example.com/browser/remote/cdp/test/browser/network/doc_empty.html"; + +add_task(async function cacheEnabledAfterDisabled({ client }) { + const { Network } = client; + await Network.setCacheDisabled({ cacheDisabled: true }); + await Network.setCacheDisabled({ cacheDisabled: false }); + + await watchLoadFlags(LOAD_NORMAL, TEST_PAGE); + await loadURL(TEST_PAGE); + await waitForLoadFlags(); +}); + +add_task(async function cacheEnabledByDefault({ Network }) { + await watchLoadFlags(LOAD_NORMAL, TEST_PAGE); + await loadURL(TEST_PAGE); + await waitForLoadFlags(); +}); + +add_task(async function cacheDisabled({ client }) { + const { Network } = client; + await Network.setCacheDisabled({ cacheDisabled: true }); + + await watchLoadFlags(LOAD_BYPASS_CACHE | INHIBIT_CACHING, TEST_PAGE); + await loadURL(TEST_PAGE); + await waitForLoadFlags(); +}); + +// This helper will resolve when the content-process progressListener is started +// and ready to monitor requests. +// The promise itself will resolve when the progressListener will detect the +// expected flags for the provided url. +function watchLoadFlags(flags, url) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ flags, url }], + async (options = {}) => { + const { flags, url } = options; + + // an nsIWebProgressListener that checks all requests made by the docShell + // have the flags we expect. + var RequestWatcher = { + init(docShell, expectedLoadFlags, url, callback) { + this.callback = callback; + this.docShell = docShell; + this.expectedLoadFlags = expectedLoadFlags; + this.url = url; + + this.requestCount = 0; + + const { NOTIFY_STATE_DOCUMENT, NOTIFY_STATE_REQUEST } = + Ci.nsIWebProgress; + + this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + .addProgressListener( + this, + NOTIFY_STATE_DOCUMENT | NOTIFY_STATE_REQUEST + ); + }, + + onStateChange(webProgress, request, flags, status) { + // We are checking requests - if there isn't one, ignore it. + if (!request) { + return; + } + + // We will usually see requests for 'about:document-onload-blocker' not + // have the flag, so we just ignore them. + // We also see, eg, resource://gre-resources/loading-image.png, so + // skip resource:// URLs too. + // We may also see, eg, chrome://global/skin/icons/chevron.svg, so + // skip chrome:// URLs too. + if ( + request.name.startsWith("about:") || + request.name.startsWith("resource:") || + request.name.startsWith("chrome:") + ) { + return; + } + is( + request.loadFlags & this.expectedLoadFlags, + this.expectedLoadFlags, + "request " + request.name + " has the expected flags" + ); + this.requestCount += 1; + + var stopFlags = + Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + + if (request.name == this.url && (flags & stopFlags) == stopFlags) { + this.docShell.removeProgressListener(this); + ok( + this.requestCount > 1, + this.url + " saw " + this.requestCount + " requests" + ); + this.callback(); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + + // Store the promise that should be awaited for on the current window. + content.resolveCheckLoadFlags = new Promise(resolve => { + RequestWatcher.init(docShell, flags, url, resolve); + }); + } + ); +} + +// Wait for the latest promise created in-content by watchLoadFlags to resolve. +function waitForLoadFlags() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + await content.resolveCheckLoadFlags; + delete content.resolveCheckLoadFlags; + }); +} diff --git a/remote/cdp/test/browser/network/browser_setCookie.js b/remote/cdp/test/browser/network/browser_setCookie.js new file mode 100644 index 0000000000..42a6c6ace4 --- /dev/null +++ b/remote/cdp/test/browser/network/browser_setCookie.js @@ -0,0 +1,291 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/cdp/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "example.org"; +const ALT_HOST = "foo.example.org"; +const SECURE_HOST = "example.com"; + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.setCookie(), + err => err.message.includes("name: string value expected"), + "Fails without any arguments" + ); +}); + +add_task(async function failureWithMissingNameAndValue({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.setCookie({ + value: "bar", + domain: "example.org", + }), + err => err.message.includes("name: string value expected"), + "Fails without name specified" + ); + + await Assert.rejects( + Network.setCookie({ + name: "foo", + domain: "example.org", + }), + err => err.message.includes("value: string value expected"), + "Fails without value specified" + ); +}); + +add_task(async function failureWithMissingDomainAndURL({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.setCookie({ name: "foo", value: "bar" }), + err => + err.message.includes( + "At least one of the url and domain needs to be specified" + ), + "Fails without domain and URL specified" + ); +}); + +add_task(async function setCookieWithDomain({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithEmptyDomain({ client }) { + const { Network } = client; + + try { + const { success } = await Network.setCookie({ + name: "foo", + value: "bar", + url: "", + }); + ok(!success, "Cookie has not been set"); + + const cookies = getCookies(); + is(cookies.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithURL({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + url: `http://${ALT_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithEmptyURL({ client }) { + const { Network } = client; + + try { + const { success } = await Network.setCookie({ + name: "foo", + value: "bar", + url: "", + }); + ok(!success, "No cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithDomainAndURL({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + url: `http://${DEFAULT_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithHttpOnly({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + httpOnly: true, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithExpiry({ client }) { + const { Network } = client; + + const tomorrow = Math.floor(Date.now() / 1000) + 60 * 60 * 24; + + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + expires: tomorrow, + session: false, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithPath({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + path: SJS_PATH, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function testAddSameSiteCookie({ client }) { + const { Network } = client; + + for (const sameSite of ["None", "Lax", "Strict"]) { + console.log(`Check same site value: ${sameSite}`); + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + }; + if (sameSite != "None") { + cookie.sameSite = sameSite; + } + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + sameSite, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); + +add_task(async function testAddSecureCookie({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + url: `https://${SECURE_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + ok(cookies[0].secure, `Cookie for HTTPS is secure`); + } finally { + Services.cookies.removeAll(); + } +}); diff --git a/remote/cdp/test/browser/network/browser_setCookies.js b/remote/cdp/test/browser/network/browser_setCookies.js new file mode 100644 index 0000000000..53c6021e1c --- /dev/null +++ b/remote/cdp/test/browser/network/browser_setCookies.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALT_HOST = "foo.example.org"; +const DEFAULT_HOST = "example.org"; + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + await Assert.rejects( + Network.setCookies(), + err => err.message.includes("Invalid parameters (cookies: array expected)"), + "Fails without any arguments" + ); +}); + +add_task(async function setCookies({ client }) { + const { Network } = client; + + const expected_cookies = [ + { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + }, + { + name: "user", + value: "password", + domain: ALT_HOST, + }, + ]; + + try { + await Network.setCookies({ cookies: expected_cookies }); + + const cookies = getCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, expected_cookies.length, "Correct number of cookies"); + assertCookie(cookies[0], expected_cookies[0]); + assertCookie(cookies[1], expected_cookies[1]); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookiesWithInvalidField({ client }) { + const { Network } = client; + + const cookies = [ + { + name: "foo", + value: "bar", + domain: "", + }, + ]; + + await Assert.rejects( + Network.setCookies({ cookies }), + err => err.message.includes("Invalid cookie fields"), + "Fails with an invalid field" + ); +}); diff --git a/remote/cdp/test/browser/network/browser_setUserAgentOverride.js b/remote/cdp/test/browser/network/browser_setUserAgentOverride.js new file mode 100644 index 0000000000..911a0a296c --- /dev/null +++ b/remote/cdp/test/browser/network/browser_setUserAgentOverride.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL(`<script>document.write(navigator.userAgent);</script>`); + +// Network.setUserAgentOverride is a redirect to Emulation.setUserAgentOverride. +// Run at least one test for setting and resetting the user agent to make sure +// that the redirect works. + +add_task(async function forwardToEmulation({ client }) { + const { Network } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + const originalPlatform = await getNavigatorProperty("platform"); + + isnot(originalUserAgent, userAgent, "Custom user agent hasn't been set"); + isnot(originalPlatform, platform, "Custom platform hasn't been set"); + + await Network.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + await Network.setUserAgentOverride({ userAgent: "", platform: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); + is( + await getNavigatorProperty("platform"), + originalPlatform, + "Custom platform has been reset" + ); + + await Network.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); +}); + +async function getNavigatorProperty(prop) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [prop], _prop => { + return content.navigator[_prop]; + }); +} diff --git a/remote/cdp/test/browser/network/doc_empty.html b/remote/cdp/test/browser/network/doc_empty.html new file mode 100644 index 0000000000..779a0ab052 --- /dev/null +++ b/remote/cdp/test/browser/network/doc_empty.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test page</title> +</head> +<body> + <div id="content">Example page</div> +</body> +</html> diff --git a/remote/cdp/test/browser/network/doc_frameset.html b/remote/cdp/test/browser/network/doc_frameset.html new file mode 100644 index 0000000000..2710312381 --- /dev/null +++ b/remote/cdp/test/browser/network/doc_frameset.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frameset for Network events</title> + <script type="text/javascript" src="file_framesetEvents.js"></script> +</head> +<body> + <iframe src="doc_networkEvents.html"></iframe> +</body> +</html> diff --git a/remote/cdp/test/browser/network/doc_get_cookies_frame.html b/remote/cdp/test/browser/network/doc_get_cookies_frame.html new file mode 100644 index 0000000000..2996b91589 --- /dev/null +++ b/remote/cdp/test/browser/network/doc_get_cookies_frame.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Sub Frame to get cookies</title> +</head> + +<body> + <div id="content">Example Sub Frame</div> + <script type="text/javascript"> + document.cookie = "frame=subframe"; + </script> +</body> + +</html> diff --git a/remote/cdp/test/browser/network/doc_get_cookies_page.html b/remote/cdp/test/browser/network/doc_get_cookies_page.html new file mode 100644 index 0000000000..c62c65cf66 --- /dev/null +++ b/remote/cdp/test/browser/network/doc_get_cookies_page.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <title>Page to get cookies</title> + +</head> + +<body> + <iframe src="doc_get_cookies_frame.html"></iframe> + <script type="text/javascript"> + document.cookie = "page=mainpage"; + </script> +</body> + +</html> diff --git a/remote/cdp/test/browser/network/doc_networkEvents.html b/remote/cdp/test/browser/network/doc_networkEvents.html new file mode 100644 index 0000000000..0c03609337 --- /dev/null +++ b/remote/cdp/test/browser/network/doc_networkEvents.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test page for Network events</title> + <script type="text/javascript" src="file_networkEvents.js"></script> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/network/file_framesetEvents.js b/remote/cdp/test/browser/network/file_framesetEvents.js new file mode 100644 index 0000000000..4771d96f79 --- /dev/null +++ b/remote/cdp/test/browser/network/file_framesetEvents.js @@ -0,0 +1,3 @@ +/* eslint-disable no-unused-vars */ +// Test file to emit Network events. +var foo = true; diff --git a/remote/cdp/test/browser/network/file_networkEvents.js b/remote/cdp/test/browser/network/file_networkEvents.js new file mode 100644 index 0000000000..4771d96f79 --- /dev/null +++ b/remote/cdp/test/browser/network/file_networkEvents.js @@ -0,0 +1,3 @@ +/* eslint-disable no-unused-vars */ +// Test file to emit Network events. +var foo = true; diff --git a/remote/cdp/test/browser/network/head.js b/remote/cdp/test/browser/network/head.js new file mode 100644 index 0000000000..3347e79e0f --- /dev/null +++ b/remote/cdp/test/browser/network/head.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); + +function assertCookie(cookie, expected = {}) { + const { + name = "", + value = "", + domain = "example.org", + path = "/", + expires = -1, + size = name.length + value.length, + httpOnly = false, + secure = false, + session = true, + sameSite, + } = expected; + + const expectedCookie = { + name, + value, + domain, + path, + expires, + size, + httpOnly, + secure, + session, + }; + + if (sameSite) { + expectedCookie.sameSite = sameSite; + } + + Assert.deepEqual(cookie, expectedCookie); +} + +function assertEventOrder(first, second, options = {}) { + const { ignoreTimestamps = false } = options; + + const firstDescription = getDescriptionForEvent(first); + const secondDescription = getDescriptionForEvent(second); + + ok( + first.index < second.index, + `${firstDescription} received before ${secondDescription})` + ); + + if (!ignoreTimestamps) { + ok( + first.payload.timestamp <= second.payload.timestamp, + `Timestamp of ${firstDescription}) is earlier than ${secondDescription})` + ); + } +} + +function filterEventsByType(history, type) { + return history.filter(event => event.payload.type == type); +} + +function getCookies() { + return Services.cookies.cookies.map(cookie => { + const data = { + name: cookie.name, + value: cookie.value, + domain: cookie.host, + path: cookie.path, + expires: cookie.isSession ? -1 : cookie.expiry, + // The size is the combined length of both the cookie name and value + size: cookie.name.length + cookie.value.length, + httpOnly: cookie.isHttpOnly, + secure: cookie.isSecure, + session: cookie.isSession, + }; + + if (cookie.sameSite) { + const sameSiteMap = new Map([ + [Ci.nsICookie.SAMESITE_LAX, "Lax"], + [Ci.nsICookie.SAMESITE_STRICT, "Strict"], + ]); + + data.sameSite = sameSiteMap.get(cookie.sameSite); + } + + return data; + }); +} + +function getDescriptionForEvent(event) { + const { eventName, payload } = event; + + return `${eventName}(${payload.type || payload.name || payload.frameId})`; +} diff --git a/remote/cdp/test/browser/network/sjs-cookies.sjs b/remote/cdp/test/browser/network/sjs-cookies.sjs new file mode 100644 index 0000000000..8cc0ec67ed --- /dev/null +++ b/remote/cdp/test/browser/network/sjs-cookies.sjs @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + const queryString = new URLSearchParams(request.queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + if (queryString.has("name") && queryString.has("value")) { + const name = queryString.get("name"); + const value = queryString.get("value"); + const path = queryString.get("path") || "/"; + + const expiry = queryString.get("expiry"); + const httpOnly = queryString.has("httpOnly"); + const secure = queryString.has("secure"); + const sameSite = queryString.get("sameSite"); + + let cookie = `${name}=${value}; Path=${path}`; + + if (expiry) { + cookie += `; Expires=${expiry}`; + } + + if (httpOnly) { + cookie += "; HttpOnly"; + } + + if (sameSite != undefined) { + cookie += `; sameSite=${sameSite}`; + } + + if (secure) { + cookie += "; Secure"; + } + + response.setHeader("Set-Cookie", cookie, true); + response.write(`Set cookie: ${cookie}`); + } +} diff --git a/remote/cdp/test/browser/page/browser.toml b/remote/cdp/test/browser/page/browser.toml new file mode 100644 index 0000000000..23d1b0fde5 --- /dev/null +++ b/remote/cdp/test/browser/page/browser.toml @@ -0,0 +1,81 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", + "doc_empty.html", + "doc_frame.html", + "doc_frameset_multi.html", + "doc_frameset_nested.html", + "doc_frameset_single.html", + "sjs_redirect.sjs", +] + +["browser_bringToFront.js"] + +["browser_captureScreenshot.js"] + +["browser_createIsolatedWorld.js"] + +["browser_domContentEventFired.js"] + +["browser_frameAttached.js"] + +["browser_frameDetached.js"] + +["browser_frameNavigated.js"] + +["browser_frameStartedLoading.js"] + +["browser_frameStoppedLoading.js"] + +["browser_getFrameTree.js"] + +["browser_getLayoutMetrics.js"] + +["browser_getNavigationHistory.js"] + +["browser_javascriptDialog_alert.js"] + +["browser_javascriptDialog_beforeunload.js"] + +["browser_javascriptDialog_confirm.js"] + +["browser_javascriptDialog_otherTarget.js"] + +["browser_javascriptDialog_prompt.js"] + +["browser_lifecycleEvent.js"] +https_first_disabled = true + +["browser_loadEventFired.js"] + +["browser_navigate.js"] +https_first_disabled = true + +["browser_navigateToHistoryEntry.js"] + +["browser_navigatedWithinDocument.js"] + +["browser_navigationEvents.js"] + +["browser_printToPDF.js"] + +["browser_reload.js"] + +["browser_runtimeEvents.js"] + +["browser_scriptToEvaluateOnNewDocument.js"] diff --git a/remote/cdp/test/browser/page/browser_bringToFront.js b/remote/cdp/test/browser/page/browser_bringToFront.js new file mode 100644 index 0000000000..ed00071922 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_bringToFront.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FIRST_DOC = toDataURL("first"); +const SECOND_DOC = toDataURL("second"); + +add_task(async function testBringToFrontUpdatesSelectedTab({ client }) { + const tab = gBrowser.selectedTab; + + await loadURL(FIRST_DOC); + + info("Open another tab that should become the front tab"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + SECOND_DOC + ); + + try { + is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab"); + + const { Page } = client; + info( + "Call Page.bringToFront() and check that the test tab becomes the selected tab" + ); + await Page.bringToFront(); + is(gBrowser.selectedTab, tab, "Selected tab is the target tab again"); + is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused"); + } finally { + BrowserTestUtils.removeTab(otherTab); + } +}); + +add_task(async function testBringToFrontUpdatesFocusedWindow({ client }) { + const tab = gBrowser.selectedTab; + + await loadURL(FIRST_DOC); + + is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused"); + + const otherWindow = await BrowserTestUtils.openNewBrowserWindow(); + + try { + is(otherWindow, getFocusedNavigator(), "The new window is focused"); + + const { Page } = client; + info( + "Call Page.bringToFront() and check that the tab window is focused again" + ); + await Page.bringToFront(); + is( + tab.ownerGlobal, + getFocusedNavigator(), + "The initial window is focused again" + ); + } finally { + await BrowserTestUtils.closeWindow(otherWindow); + } +}); + +function getFocusedNavigator() { + return Services.wm.getMostRecentWindow("navigator:browser"); +} diff --git a/remote/cdp/test/browser/page/browser_captureScreenshot.js b/remote/cdp/test/browser/page/browser_captureScreenshot.js new file mode 100644 index 0000000000..ecd688fd14 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_captureScreenshot.js @@ -0,0 +1,553 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function documentSmallerThanViewport({ client }) { + const { Page } = client; + + await loadURLWithElement(); + + info("Check that captureScreenshot() captures the viewport by default"); + const { data } = await Page.captureScreenshot(); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + const { mimeType, width, height } = await getImageDetails(data); + + is(mimeType, "image/png", "Screenshot has correct MIME type"); + is(width, (viewport.width - viewport.x) * scale, "Image has expected width"); + is( + height, + (viewport.height - viewport.y) * scale, + "Image has expected height" + ); +}); + +add_task(async function documentLargerThanViewport({ client }) { + const { Page } = client; + + await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world")); + + info("Check that captureScreenshot() captures the viewport by default"); + const { data } = await Page.captureScreenshot(); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const scrollbarSize = await getScrollbarSize(); + const viewport = await getViewportSize(); + const { mimeType, width, height } = await getImageDetails(data); + + is(mimeType, "image/png", "Screenshot has correct MIME type"); + is( + width, + (viewport.width - viewport.x - scrollbarSize.width) * scale, + "Image has expected width" + ); + is( + height, + (viewport.height - viewport.y - scrollbarSize.height) * scale, + "Image has expected height" + ); +}); + +add_task(async function invalidFormat({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + await Assert.rejects( + Page.captureScreenshot({ format: "foo" }), + err => err.message.includes(`Unsupported MIME type: image`), + "captureScreenshot raised error for invalid image format" + ); +}); + +add_task(async function asJPEGFormat({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + info("Check that captureScreenshot() captures as JPEG format"); + const { data } = await Page.captureScreenshot({ format: "jpeg" }); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + const { mimeType, height, width } = await getImageDetails(data); + + is(mimeType, "image/jpeg", "Screenshot has correct MIME type"); + is(width, (viewport.width - viewport.x) * scale); + is(height, (viewport.height - viewport.y) * scale); +}); + +add_task(async function asJPEGFormatAndQuality({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + info("Check that captureScreenshot() captures as JPEG format"); + const imageDefault = await Page.captureScreenshot({ format: "jpeg" }); + ok(!!imageDefault, "Screenshot data with default quality is not empty"); + + const image100 = await Page.captureScreenshot({ + format: "jpeg", + quality: 100, + }); + ok(!!image100, "Screenshot data with quality 100 is not empty"); + + const image10 = await Page.captureScreenshot({ + format: "jpeg", + quality: 10, + }); + ok(!!image10, "Screenshot data with quality 10 is not empty"); + + const infoDefault = await getImageDetails(imageDefault.data); + const info100 = await getImageDetails(image100.data); + const info10 = await getImageDetails(image10.data); + + // All screenshots are of mimeType JPEG + is( + infoDefault.mimeType, + "image/jpeg", + "Screenshot with default quality has correct MIME type" + ); + is( + info100.mimeType, + "image/jpeg", + "Screenshot with quality 100 has correct MIME type" + ); + is( + info10.mimeType, + "image/jpeg", + "Screenshot with quality 10 has correct MIME type" + ); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + + // Images are all of the same dimension + is(infoDefault.width, (viewport.width - viewport.x) * scale); + is(infoDefault.height, (viewport.height - viewport.y) * scale); + + is(info100.width, (viewport.width - viewport.x) * scale); + is(info100.height, (viewport.height - viewport.y) * scale); + + is(info10.width, (viewport.width - viewport.x) * scale); + is(info10.height, (viewport.height - viewport.y) * scale); + + // Images of different quality result in different content sizes + ok( + info100.length > infoDefault.length, + "Size of quality 100 is larger than default" + ); + ok( + info10.length < infoDefault.length, + "Size of quality 10 is smaller than default" + ); +}); + +add_task(async function clipMissingProperties({ client }) { + const { Page } = client; + const contentSize = await getContentSize(); + + for (const prop of ["x", "y", "width", "height", "scale"]) { + console.info(`Check for missing ${prop}`); + + const clip = { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + }; + clip[prop] = undefined; + + await Assert.rejects( + Page.captureScreenshot({ clip }), + err => err.message.includes(`clip.${prop}: double value expected`), + `raised error for missing clip.${prop} property` + ); + } +}); + +add_task(async function clipOutOfBoundsXAndY({ client }) { + const { Page } = client; + + const ratio = await getDevicePixelRatio(); + const size = 50; + + await loadURLWithElement(); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: size, + height: size, + scale: 1, + }, + }); + + for (const x of [-1, contentSize.width]) { + console.info(`Check out-of-bounds x for ${x}`); + const { data } = await Page.captureScreenshot({ + clip: { + x, + y: 0, + width: size, + height: size, + scale: 1, + }, + }); + const { width, height } = await getImageDetails(data); + + is(width, size * ratio, "Image has expected width"); + is(height, size * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } + + for (const y of [-1, contentSize.height]) { + console.info(`Check out-of-bounds y for ${y}`); + const { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y, + width: size, + height: size, + scale: 1, + }, + }); + const { width, height } = await getImageDetails(data); + + is(width, size * ratio, "Image has expected width"); + is(height, size * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipOutOfBoundsWidthAndHeight({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world")); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + scale: 1, + }, + }); + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds width for ${value}`); + const clip = { + x: 0, + y: 0, + width: value, + height: contentSize.height, + scale: 1, + }; + + const { data } = await Page.captureScreenshot({ clip }); + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds height for ${value}`); + const clip = { + x: 0, + y: 0, + width: contentSize.width, + height: value, + scale: 1, + }; + + const { data } = await Page.captureScreenshot({ clip }); + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipOutOfBoundsScale({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURLWithElement(); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + scale: 1, + }, + }); + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds scale for ${value}`); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 50, + height: 50, + scale: value, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipScale({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + for (const scale of [1.5, 2]) { + console.info(`Check scale for ${scale}`); + await loadURLWithElement({ width: 100 * scale, height: 100 * scale }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100 * scale, + height: 100 * scale, + scale: 1, + }, + }); + + await loadURLWithElement({ width: 100, height: 100 }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100, + height: 100, + scale, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, 100 * ratio * scale, "Image has expected width"); + is(height, 100 * ratio * scale, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipScaleAndDevicePixelRatio({ client }) { + const { Page } = client; + + const originalRatio = await getDevicePixelRatio(); + + const ratio = 2; + const scale = 1.5; + const size = 100; + + const expectedSize = size * ratio * scale; + + console.info(`Create reference screenshot: ${expectedSize}x${expectedSize}`); + await loadURLWithElement({ + width: expectedSize, + height: expectedSize, + }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: expectedSize, + height: expectedSize, + scale: 1, + }, + }); + + await setDevicePixelRatio(originalRatio * ratio); + + await loadURLWithElement({ width: size, height: size }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: size, + height: size, + scale, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, expectedSize * originalRatio, "Image has expected width"); + is(height, expectedSize * originalRatio, "Image has expected height"); + is(data, refData, "Image is equal"); +}); + +add_task(async function clipPosition({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURLWithElement(); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100, + height: 100, + scale: 1, + }, + }); + + for (const [x, y] of [ + [10, 20], + [20, 10], + [20, 20], + ]) { + console.info(`Check postion for ${x} and ${y}`); + await loadURLWithElement({ x, y }); + var { data } = await Page.captureScreenshot({ + clip: { + x, + y, + width: 100, + height: 100, + scale: 1, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, 100 * ratio, "Image has expected width"); + is(height, 100 * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipDimension({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + for (const [width, height] of [ + [10, 20], + [20, 10], + [20, 20], + ]) { + console.info(`Check width and height for ${width} and ${height}`); + + // Get reference image as section from a larger image + await loadURLWithElement({ width: 50, height: 50 }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width, + height, + scale: 1, + }, + }); + + await loadURLWithElement({ width, height }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width, + height, + scale: 1, + }, + }); + + const dimension = await getImageDetails(data); + is(dimension.width, width * ratio, "Image has expected width"); + is(dimension.height, height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +async function loadURLWithElement(options = {}) { + const { x = 0, y = 0, width = 100, height = 100 } = options; + + const doc = ` + <style> + body { + margin: 0; + } + div { + margin-left: ${x}px; + margin-top: ${y}px; + width: ${width}px; + height: ${height}px; + background: green; + } + </style> + <body> + <div></div> + `; + + await loadURL(toDataURL(doc)); +} + +async function getDevicePixelRatio() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.browsingContext.overrideDPPX || content.devicePixelRatio; + }); +} + +async function setDevicePixelRatio(dppx) { + gBrowser.selectedBrowser.browsingContext.overrideDPPX = dppx; +} + +async function getImageDetails(image) { + const mimeType = getMimeType(image); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ mimeType, image }], + async function ({ mimeType, image }) { + return new Promise(resolve => { + const img = new content.Image(); + img.addEventListener( + "load", + () => { + resolve({ + mimeType, + width: img.width, + height: img.height, + length: image.length, + }); + }, + { once: true } + ); + + img.src = `data:${mimeType};base64,${image}`; + }); + } + ); +} + +function getMimeType(image) { + // Decode from base64 and convert the first 4 bytes to hex + const raw = atob(image).slice(0, 4); + let magicBytes = ""; + for (let i = 0; i < raw.length; i++) { + magicBytes += raw.charCodeAt(i).toString(16).toUpperCase(); + } + + switch (magicBytes) { + case "89504E47": + return "image/png"; + case "FFD8FFDB": + case "FFD8FFE0": + return "image/jpeg"; + default: + throw new Error("Unknown MIME type"); + } +} diff --git a/remote/cdp/test/browser/page/browser_createIsolatedWorld.js b/remote/cdp/test/browser/page/browser_createIsolatedWorld.js new file mode 100644 index 0000000000..41ed83d264 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_createIsolatedWorld.js @@ -0,0 +1,471 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test Page.createIsolatedWorld + +const WORLD_NAME_1 = "testWorld1"; +const WORLD_NAME_2 = "testWorld2"; + +const DESTROYED = "Runtime.executionContextDestroyed"; +const CREATED = "Runtime.executionContextCreated"; +const CLEARED = "Runtime.executionContextsCleared"; + +add_task(async function frameIdMissing({ client }) { + const { Page } = client; + + await Assert.rejects( + Page.createIsolatedWorld({ + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }), + /frameId: string value expected/, + `Fails with missing frameId` + ); +}); + +add_task(async function frameIdInvalidTypes({ client }) { + const { Page } = client; + + for (const frameId of [null, true, 1, [], {}]) { + await Assert.rejects( + Page.createIsolatedWorld({ + frameId, + }), + /frameId: string value expected/, + `Fails with invalid type: ${frameId}` + ); + } +}); + +add_task(async function worldNameInvalidTypes({ client }) { + const { Page } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: PAGE_URL }); + await loadEvent; + + for (const worldName of [null, true, 1, [], {}]) { + await Assert.rejects( + Page.createIsolatedWorld({ + frameId, + worldName, + }), + /worldName: string value expected/, + `Fails with invalid type: ${worldName}` + ); + } +}); + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + const history = recordEvents(Runtime, 0); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: PAGE_URL }); + await loadEvent; + + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEvents({ history, expectedEvents: [] }); + } catch (e) { + errorThrown = e.message; + } + todo( + errorThrown === "", + "No contexts tracked internally without Runtime enabled (Bug 1623482)" + ); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + await Runtime.disable(); + info("Runtime notifications are disabled"); + + const history = recordEvents(Runtime, 0); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: PAGE_URL }); + await loadEvent; + + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_2, + grantUniversalAccess: true, + }); + await assertEvents({ history, expectedEvents: [] }); +}); + +add_task(async function contextCreatedAfterNavigation({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + + const history = recordEvents(Runtime, 3); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: PAGE_URL }); + await loadEvent; + + const { executionContextId: isolatedId } = await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEvents({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, PAGE_URL + CREATED, // isolated, PAGE_URL + ], + }); + + const contexts = history + .findEvents(CREATED) + .map(event => event.payload.context); + const defaultContext = contexts[0]; + const isolatedContext = contexts[1]; + is(defaultContext.auxData.isDefault, true, "Default context is default"); + is( + defaultContext.auxData.type, + "default", + "Default context has type 'default'" + ); + is(defaultContext.origin, BASE_ORIGIN, "Default context has expected origin"); + checkIsolated(isolatedContext, isolatedId, WORLD_NAME_1, frameId); + compareContexts(isolatedContext, defaultContext); +}); + +add_task(async function contextDestroyedForNavigation({ client }) { + const { Page, Runtime } = client; + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + await Page.enable(); + + const history = recordEvents(Runtime, 4, true); + const frameNavigated = Page.frameNavigated(); + await Page.navigate({ url: PAGE_URL }); + await frameNavigated; + + await assertEvents({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + DESTROYED, // isolated, about:blank + CLEARED, + CREATED, // default, PAGE_URL + ], + }); + + const destroyed = history + .findEvents(DESTROYED) + .map(event => event.payload.executionContextId); + ok(destroyed.includes(isolatedContext.id), "Isolated context destroyed"); + ok(destroyed.includes(defaultContext.id), "Default context destroyed"); + + const { context: newContext } = history.findEvent(CREATED).payload; + is(newContext.auxData.isDefault, true, "The new context is a default one"); + ok(!!newContext.id, "The new context has an id"); + ok( + ![defaultContext.id, isolatedContext.id].includes(newContext.id), + "The new context has a new id" + ); +}); + +add_task(async function contextsForFramesetNavigation({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + + // check creation when navigating to a frameset + const historyTo = recordEvents(Runtime, 5); + const loadEventTo = Page.loadEventFired(); + const { frameId: frameIdTo } = await Page.navigate({ + url: FRAMESET_SINGLE_URL, + }); + await loadEventTo; + + const { frameTree } = await Page.getFrameTree(); + const subFrame = frameTree.childFrames[0].frame; + + const { executionContextId: contextIdParent } = + await Page.createIsolatedWorld({ + frameId: frameIdTo, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + const { executionContextId: contextIdSubFrame } = + await Page.createIsolatedWorld({ + frameId: subFrame.id, + worldName: WORLD_NAME_2, + grantUniversalAccess: true, + }); + + await assertEvents({ + history: historyTo, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, FRAMESET_SINGLE_URL + CREATED, // default, PAGE_URL + CREATED, // isolated, FRAMESET_SINGLE_URL + CREATED, // isolated, PAGE_URL + ], + }); + + const contextsCreated = historyTo + .findEvents(CREATED) + .map(event => event.payload.context); + const parentDefaultContextCreated = contextsCreated[0]; + const frameDefaultContextCreated = contextsCreated[1]; + const parentIsolatedContextCreated = contextsCreated[2]; + const frameIsolatedContextCreated = contextsCreated[3]; + + checkIsolated( + parentIsolatedContextCreated, + contextIdParent, + WORLD_NAME_1, + frameIdTo + ); + compareContexts(parentIsolatedContextCreated, parentDefaultContextCreated); + + checkIsolated( + frameIsolatedContextCreated, + contextIdSubFrame, + WORLD_NAME_2, + subFrame.id + ); + compareContexts(frameIsolatedContextCreated, frameDefaultContextCreated); + + // check destroying when navigating away from a frameset + const historyFrom = recordEvents(Runtime, 6); + const loadEventFrom = Page.loadEventFired(); + await Page.navigate({ url: PAGE_URL }); + await loadEventFrom; + + await assertEvents({ + history: historyFrom, + expectedEvents: [ + DESTROYED, // default, PAGE_URL + DESTROYED, // isolated, PAGE_URL + DESTROYED, // default, FRAMESET_SINGLE_URL + DESTROYED, // isolated, FRAMESET_SINGLE_URL + CREATED, // default, PAGE_URL + ], + }); + + const contextsDestroyed = historyFrom + .findEvents(DESTROYED) + .map(event => event.payload.executionContextId); + contextsCreated.forEach(context => { + ok( + contextsDestroyed.includes(context.id), + `Context with id ${context.id} destroyed` + ); + }); + + const { context: newContext } = historyFrom.findEvent(CREATED).payload; + is(newContext.auxData.isDefault, true, "The new context is a default one"); + ok(!!newContext.id, "The new context has an id"); + ok( + ![parentDefaultContextCreated.id, frameDefaultContextCreated.id].includes( + newContext.id + ), + "The new context has a new id" + ); +}); + +add_task(async function evaluateInIsolatedAndDefault({ client }) { + const { Runtime } = client; + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + const { result: objDefault } = await Runtime.evaluate({ + contextId: defaultContext.id, + expression: "({ foo: 1 })", + }); + const { result: objIsolated } = await Runtime.evaluate({ + contextId: isolatedContext.id, + expression: "({ foo: 10 })", + }); + const { result: result1 } = await Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: objIsolated.objectId }], + }); + is(result1.value, 11, "Isolated context incremented the expected value"); + + await Assert.rejects( + Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: objDefault.objectId }], + }), + /Could not find object with given id/, + "Contexts do not share objects" + ); +}); + +add_task(async function contextEvaluationIsIsolated({ client }) { + const { Runtime } = client; + + // If a document makes changes to standard global object, an isolated + // world should not be affected + await loadURL(toDataURL("<script>window.Node = null</script>")); + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + const { result: result1 } = await Runtime.callFunctionOn({ + executionContextId: defaultContext.id, + functionDeclaration: "arg => window.Node", + }); + const { result: result2 } = await Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => window.Node", + }); + is(result1.value, null, "Default context sees content changes to global"); + todo_isnot( + result2.value, + null, + "Isolated context is not affected by changes to global, Bug 1601421" + ); +}); + +function checkIsolated(context, expectedId, expectedName, expectedFrameId) { + is( + expectedId, + context.id, + "createIsolatedWorld returns id of isolated context" + ); + is( + context.auxData.frameId, + expectedFrameId, + "Isolated context has expected frameId" + ); + is(context.auxData.isDefault, false, "Isolated context is not default"); + is(context.auxData.type, "isolated", "Isolated context has type 'isolated'"); + is(context.name, expectedName, "Isolated context is named as requested"); + ok(!!context.origin, "Isolated context has an origin"); +} + +function compareContexts(isolatedContext, defaultContext) { + isnot( + defaultContext.name, + isolatedContext.name, + "The contexts have different names" + ); + isnot( + defaultContext.id, + isolatedContext.id, + "The contexts have different ids" + ); + is( + defaultContext.origin, + isolatedContext.origin, + "The contexts have same origin" + ); + is( + defaultContext.auxData.frameId, + isolatedContext.auxData.frameId, + "The contexts have same frameId" + ); +} + +async function createIsolatedContext( + client, + defaultContext, + worldName = WORLD_NAME_1 +) { + const { Page, Runtime } = client; + + const frameId = defaultContext.auxData.frameId; + + const isolatedContextCreated = Runtime.executionContextCreated(); + const { executionContextId: isolatedId } = await Page.createIsolatedWorld({ + frameId, + worldName, + grantUniversalAccess: true, + }); + const { context: isolatedContext } = await isolatedContextCreated; + info("Isolated world created"); + + checkIsolated(isolatedContext, isolatedId, worldName, frameId); + compareContexts(isolatedContext, defaultContext); + + return isolatedContext; +} + +function recordEvents(Runtime, total, cleared = false) { + const history = new RecordEvents(total); + + history.addRecorder({ + event: Runtime.executionContextDestroyed, + eventName: DESTROYED, + messageFn: payload => { + return `Received ${DESTROYED} for id ${payload.executionContextId}`; + }, + }); + history.addRecorder({ + event: Runtime.executionContextCreated, + eventName: CREATED, + messageFn: payload => { + return ( + `Received ${CREATED} for id ${payload.context.id}` + + ` type: ${payload.context.auxData.type}` + + ` name: ${payload.context.name}` + + ` origin: ${payload.context.origin}` + ); + }, + }); + if (cleared) { + history.addRecorder({ + event: Runtime.executionContextsCleared, + eventName: CLEARED, + }); + } + + return history; +} + +async function assertEvents(options = {}) { + const { history, expectedEvents, timeout = 1000 } = options; + const events = await history.record(timeout); + const eventNames = events.map(item => item.eventName); + info(`Expected events: ${expectedEvents}`); + info(`Received events: ${eventNames}`); + is( + events.length, + expectedEvents.length, + "Received expected number of Runtime context events" + ); + Assert.deepEqual( + eventNames.sort(), + expectedEvents.sort(), + "Received expected Runtime context events" + ); +} diff --git a/remote/cdp/test/browser/page/browser_domContentEventFired.js b/remote/cdp/test/browser/page/browser_domContentEventFired.js new file mode 100644 index 0000000000..7bdf8f127d --- /dev/null +++ b/remote/cdp/test/browser/page/browser_domContentEventFired.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runContentEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runContentEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runContentEventFiredTest(client, expectedEventCount, callback) { + const { Page } = client; + + if (![0, 1].includes(expectedEventCount)) { + throw new Error(`Invalid value for expectedEventCount`); + } + + const DOM_CONTENT_EVENT_FIRED = "Page.domContentEventFired"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.domContentEventFired, + eventName: DOM_CONTENT_EVENT_FIRED, + messageFn: payload => { + return `Received ${DOM_CONTENT_EVENT_FIRED} at time ${payload.timestamp}`; + }, + }); + + const timeBefore = Date.now() / 1000; + await callback(); + const domContentEventFiredEvents = await history.record(); + const timeAfter = Date.now() / 1000; + + is( + domContentEventFiredEvents.length, + expectedEventCount, + "Got expected amount of domContentEventFired events" + ); + if (expectedEventCount == 0) { + return; + } + + const timestamp = domContentEventFiredEvents[0].payload.timestamp; + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]` + ); +} diff --git a/remote/cdp/test/browser/page/browser_frameAttached.js b/remote/cdp/test/browser/page/browser_frameAttached.js new file mode 100644 index 0000000000..bda66b2814 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_frameAttached.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_NESTED_URL); + + await Page.enable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 2, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 3, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenAttachingFrame({ client }) { + const { Page } = client; + + await loadURL(FRAMESET_NESTED_URL); + + await Page.enable(); + + await runFrameAttachedTest(client, 1, async () => { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [PAGE_FRAME_URL], + async frameURL => { + const frame = content.document.createElement("iframe"); + frame.src = frameURL; + const loaded = new Promise(resolve => (frame.onload = resolve)); + content.document.body.appendChild(frame); + await loaded; + } + ); + }); +}); + +async function runFrameAttachedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const ATTACHED = "Page.frameAttached"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameAttached, + eventName: ATTACHED, + messageFn: payload => { + return `Received ${ATTACHED} for frame id ${payload.frameId}`; + }, + }); + + const framesBefore = await getFlattenedFrameTree(client); + await callback(); + const framesAfter = await getFlattenedFrameTree(client); + + const frameAttachedEvents = await history.record(); + + if (expectedEventCount == 0) { + is(frameAttachedEvents.length, 0, "Got no frame attached event"); + return; + } + + // check how many frames were attached or detached + const count = Math.abs(framesBefore.size - framesAfter.size); + + is(count, expectedEventCount, "Expected amount of frames attached"); + is( + frameAttachedEvents.length, + count, + "Received the expected amount of frameAttached events" + ); + + // extract the new or removed frames + const framesAll = new Map([...framesBefore, ...framesAfter]); + const expectedFrames = new Map( + [...framesAll].filter(([key, _value]) => { + return !framesBefore.has(key) && framesAfter.has(key); + }) + ); + + frameAttachedEvents.forEach(({ payload }) => { + const { frameId, parentFrameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = expectedFrames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameAttached event" + ); + is( + parentFrameId, + expectedFrame.parentId, + "Got expected parent frame id for frameAttached event" + ); + }); +} diff --git a/remote/cdp/test/browser/page/browser_frameDetached.js b/remote/cdp/test/browser/page/browser_frameDetached.js new file mode 100644 index 0000000000..90db5087b1 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_frameDetached.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Disable bfcache to force documents to be destroyed on navigation +Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.sessionhistory.max_total_viewers"); +}); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away from a page with an iframe"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + + await Page.enable(); + await Page.disable(); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + + await Page.enable(); + + await runFrameDetachedTest(client, 2, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + + await Page.enable(); + + await runFrameDetachedTest(client, 3, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenDetachingFrame({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + + await Page.enable(); + + await runFrameDetachedTest(client, 1, async () => { + // Remove the single frame from the page + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const frame = content.document.getElementsByTagName("iframe")[0]; + frame.remove(); + }); + }); +}); + +add_task(async function eventWhenDetachingNestedFrames({ client }) { + const { Page, Runtime } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + + await Page.enable(); + await Runtime.enable(); + + const { context } = await Runtime.executionContextCreated(); + + await runFrameDetachedTest(client, 3, async () => { + // Remove top-frame, which also removes any nested frames + await evaluate(client, context.id, async () => { + const frame = document.getElementsByTagName("iframe")[0]; + frame.remove(); + }); + }); +}); + +async function runFrameDetachedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const DETACHED = "Page.frameDetached"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameDetached, + eventName: DETACHED, + messageFn: payload => { + return `Received ${DETACHED} for frame id ${payload.frameId}`; + }, + }); + + const framesBefore = await getFlattenedFrameTree(client); + await callback(); + const framesAfter = await getFlattenedFrameTree(client); + + const frameDetachedEvents = await history.record(); + + if (expectedEventCount == 0) { + is(frameDetachedEvents.length, 0, "Got no frame detached event"); + return; + } + + // check how many frames were attached or detached + const count = Math.abs(framesBefore.size - framesAfter.size); + + is(count, expectedEventCount, "Expected amount of frames detached"); + is( + frameDetachedEvents.length, + count, + "Received the expected amount of frameDetached events" + ); + + // extract the new or removed frames + const framesAll = new Map([...framesBefore, ...framesAfter]); + const expectedFrames = new Map( + [...framesAll].filter(([key, _value]) => { + return framesBefore.has(key) && !framesAfter.has(key); + }) + ); + + frameDetachedEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = expectedFrames.get(frameId); + + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameDetached event" + ); + }); +} diff --git a/remote/cdp/test/browser/page/browser_frameNavigated.js b/remote/cdp/test/browser/page/browser_frameNavigated.js new file mode 100644 index 0000000000..4453c63749 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_frameNavigated.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameNavigatedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameNavigatedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runFrameNavigatedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const NAVIGATED = "Page.frameNavigated"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameNavigated, + eventName: NAVIGATED, + messageFn: payload => { + return `Received ${NAVIGATED} for frame id ${payload.frame.id}`; + }, + }); + + await callback(); + + const frameNavigatedEvents = await history.record(); + + is( + frameNavigatedEvents.length, + expectedEventCount, + "Got expected amount of frameNavigated events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameNavigatedEvents.forEach(({ payload }) => { + const { frame } = payload; + + const expectedFrame = frames.get(frame.id); + Assert.deepEqual(frame, expectedFrame, "Got expected frame details"); + }); +} diff --git a/remote/cdp/test/browser/page/browser_frameStartedLoading.js b/remote/cdp/test/browser/page/browser_frameStartedLoading.js new file mode 100644 index 0000000000..d5bb91a952 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_frameStartedLoading.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStartedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameStartedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runFrameStartedLoadingTest( + client, + expectedEventCount, + callback +) { + const { Page } = client; + + const STARTED_LOADING = "Page.frameStartedLoading"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameStartedLoading, + eventName: STARTED_LOADING, + messageFn: payload => { + return `Received ${STARTED_LOADING} for frame id ${payload.frameId}`; + }, + }); + + await callback(); + + const frameStartedLoadingEvents = await history.record(); + + is( + frameStartedLoadingEvents.length, + expectedEventCount, + "Got expected amount of frameStartedLoading events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameStartedLoadingEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = frames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameStartedLoading event" + ); + }); +} diff --git a/remote/cdp/test/browser/page/browser_frameStoppedLoading.js b/remote/cdp/test/browser/page/browser_frameStoppedLoading.js new file mode 100644 index 0000000000..9d7c37ddc4 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_frameStoppedLoading.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStoppedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameStoppedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runFrameStoppedLoadingTest( + client, + expectedEventCount, + callback +) { + const { Page } = client; + + const STOPPED_LOADING = "Page.frameStoppedLoading"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameStoppedLoading, + eventName: STOPPED_LOADING, + messageFn: payload => { + return `Received ${STOPPED_LOADING} for frame id ${payload.frameId}`; + }, + }); + + await callback(); + + const frameStoppedLoadingEvents = await history.record(); + + is( + frameStoppedLoadingEvents.length, + expectedEventCount, + "Got expected amount of frameStoppedLoading events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameStoppedLoadingEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = frames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameStartedLoading event" + ); + }); +} diff --git a/remote/cdp/test/browser/page/browser_getFrameTree.js b/remote/cdp/test/browser/page/browser_getFrameTree.js new file mode 100644 index 0000000000..e96dc26d45 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_getFrameTree.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function pageWithoutFrames({ client }) { + const { Page } = client; + + info("Navigate to a page without a frame"); + await loadURL(PAGE_URL); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + // Check top-level frame + const expectedFrame = expectedFrames.get(frameTree.frame.id); + is(frameTree.frame.id, expectedFrame.id, "Expected frame id found"); + is(frameTree.frame.parentId, undefined, "Parent frame doesn't exist"); + is(frameTree.name, undefined, "Top frame doens't contain name property"); + is(frameTree.frame.url, expectedFrame.url, "Expected url found"); + is(frameTree.childFrames, undefined, "No sub frames found"); +}); + +add_task(async function PageWithFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with frames"); + await loadURL(FRAMESET_MULTI_URL); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + let frame = frameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check top frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, undefined, "Parent frame doesn't exist"); + is(frame.name, undefined, "Top frame doesn't contain name property"); + is(frame.url, expectedFrame.url, "Expected URL found"); + + is(frameTree.childFrames.length, 2, "Expected two sub frames"); + for (const childFrameTree of frameTree.childFrames) { + let frame = childFrameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check sub frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(childFrameTree.childFrames, undefined, "No sub frames found"); + } +}); + +add_task(async function pageWithNestedFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with nested frames"); + await loadURL(FRAMESET_NESTED_URL); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + let frame = frameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check top frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, undefined, "Parent frame doesn't exist"); + is(frame.name, undefined, "Top frame doesn't contain name property"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(frameTree.childFrames.length, 1, "Expected a single sub frame"); + + const childFrameTree = frameTree.childFrames[0]; + frame = childFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check sub frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(childFrameTree.childFrames.length, 2, "Expected two sub frames"); + + let nestedChildFrameTree = childFrameTree.childFrames[0]; + frame = nestedChildFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check first nested frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(nestedChildFrameTree.childFrames, undefined, "No sub frames found"); + + nestedChildFrameTree = childFrameTree.childFrames[1]; + frame = nestedChildFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check second nested frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(nestedChildFrameTree.childFrames, undefined, "No sub frames found"); +}); + +/** + * Retrieve all frames for the current tab as flattened list. + */ +function getFlattenedFrameList() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const frames = new Map(); + + function getFrameDetails(context) { + const frameElement = context.embedderElement; + + const frame = { + id: context.id.toString(), + parentId: context.parent ? context.parent.id.toString() : null, + loaderId: null, + name: frameElement?.id || frameElement?.name, + url: context.docShell.domWindow.location.href, + securityOrigin: null, + mimeType: null, + }; + + if (context.parent) { + frame.parentId = context.parent.id.toString(); + } + + frames.set(context.id.toString(), frame); + + for (const childContext of context.children) { + getFrameDetails(childContext); + } + } + + getFrameDetails(content.docShell.browsingContext); + return frames; + }); +} diff --git a/remote/cdp/test/browser/page/browser_getLayoutMetrics.js b/remote/cdp/test/browser/page/browser_getLayoutMetrics.js new file mode 100644 index 0000000000..db8b3e8f3c --- /dev/null +++ b/remote/cdp/test/browser/page/browser_getLayoutMetrics.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function documentSmallerThanViewport({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport); + + is( + contentSize.x, + layoutViewport.pageX, + "X position of content is equal to layout viewport" + ); + is( + contentSize.y, + layoutViewport.pageY, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width <= layoutViewport.clientWidth, + "Width of content is smaller than the layout viewport" + ); + ok( + contentSize.height <= layoutViewport.clientHeight, + "Height of content is smaller than the layout viewport" + ); +}); + +add_task(async function documentLargerThanViewport({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world")); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport, { scrollbars: true }); + + is( + contentSize.x, + layoutViewport.pageX, + "X position of content is equal to layout viewport" + ); + is( + contentSize.y, + layoutViewport.pageY, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width > layoutViewport.clientWidth, + "Width of content is larger than the layout viewport" + ); + ok( + contentSize.height > layoutViewport.clientHeight, + "Height of content is larger than the layout viewport" + ); +}); + +add_task(async function documentLargerThanViewportScrolledXY({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world")); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.scrollTo(50, 100); + }); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport, { scrollbars: true }); + + is( + layoutViewport.pageX, + contentSize.x + 50, + "X position of content is equal to layout viewport" + ); + is( + layoutViewport.pageY, + contentSize.y + 100, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width > layoutViewport.clientWidth, + "Width of content is larger than the layout viewport" + ); + ok( + contentSize.height > layoutViewport.clientHeight, + "Height of content is larger than the layout viewport" + ); +}); + +async function checkContentSize(rect) { + const expected = await getContentSize(); + + is(rect.x, expected.x, "Expected x position returned"); + is(rect.y, expected.y, "Expected y position returned"); + is(rect.width, expected.width, "Expected width returned"); + is(rect.height, expected.height, "Expected height returned"); +} + +async function checkLayoutViewport(viewport, options = {}) { + const { scrollbars = false } = options; + + const expected = await getViewportSize(); + + if (scrollbars) { + const { width, height } = await getScrollbarSize(); + expected.width -= width; + expected.height -= height; + } + + is(viewport.pageX, expected.x, "Expected x position returned"); + is(viewport.pageY, expected.y, "Expected y position returned"); + is(viewport.clientWidth, expected.width, "Expected width returned"); + is(viewport.clientHeight, expected.height, "Expected height returned"); +} diff --git a/remote/cdp/test/browser/page/browser_getNavigationHistory.js b/remote/cdp/test/browser/page/browser_getNavigationHistory.js new file mode 100644 index 0000000000..1067629828 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_getNavigationHistory.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function singleEntry({ client }) { + const { Page } = client; + + const data = generateHistoryData(1); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); + +add_task(async function multipleEntriesWithLastIndex({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, data.length - 1); +}); + +add_task(async function multipleEntriesWithFirstIndex({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); + +add_task(async function locationRedirect({ client }) { + const { Page } = client; + + const pageEmptyURL = + "https://example.com/browser/remote/cdp/test/browser/page/doc_empty.html"; + const sjsURL = + "https://example.com/browser/remote/cdp/test/browser/page/sjs_redirect.sjs"; + const redirectURL = `${sjsURL}?${pageEmptyURL}`; + + const data = [ + { + url: pageEmptyURL, + userTypedURL: redirectURL, + title: "Empty page", + }, + ]; + + await loadURL(redirectURL, pageEmptyURL); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js b/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js new file mode 100644 index 0000000000..40c5e4898a --- /dev/null +++ b/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test a browser alert is detected via Page.javascriptDialogOpening and can be +// closed with Page.handleJavaScriptDialog +add_task(async function ({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Set window.alertIsClosed to false in the content page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + // This boolean will be flipped after closing the dialog + content.alertIsClosed = false; + }); + + info("Create an alert dialog again"); + const { message, type } = await createAlertDialog(Page); + is(type, "alert", "dialog event contains the correct type"); + is(message, "test-1234", "dialog event contains the correct text"); + + info("Close the dialog with accept:false"); + await Page.handleJavaScriptDialog({ accept: false }); + + info("Retrieve the alertIsClosed boolean on the content window"); + let alertIsClosed = await getContentProperty("alertIsClosed"); + ok(alertIsClosed, "The content process is no longer blocked on the alert"); + + info("Reset window.alertIsClosed to false in the content page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alertIsClosed = false; + }); + + info("Create an alert dialog again"); + await createAlertDialog(Page); + + info("Close the dialog with accept:true"); + await Page.handleJavaScriptDialog({ accept: true }); + + alertIsClosed = await getContentProperty("alertIsClosed"); + ok(alertIsClosed, "The content process is no longer blocked on the alert"); +}); + +function createAlertDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger an alert in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alert("test-1234"); + // Flip a boolean in the content page to check if the content process resumed + // after the alert was opened. + content.alertIsClosed = true; + }); + + return onDialogOpen; +} diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js b/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js new file mode 100644 index 0000000000..7cab579e2c --- /dev/null +++ b/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test beforeunload dialog events. +add_task(async function ({ client, tab }) { + info("Allow to trigger onbeforeunload without user interaction"); + await new Promise(resolve => { + const options = { + set: [["dom.require_user_interaction_for_beforeunload", false]], + }; + SpecialPowers.pushPrefEnv(options, resolve); + }); + + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Attach a valid onbeforeunload handler"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.onbeforeunload = () => true; + }); + + info("Trigger the beforeunload again but reject the prompt"); + const { type } = await triggerBeforeUnload(Page, tab, false); + is(type, "beforeunload", "dialog event contains the correct type"); + + info("Trigger the beforeunload again and accept the prompt"); + const onTabClose = BrowserTestUtils.waitForEvent(tab, "TabClose"); + await triggerBeforeUnload(Page, tab, true); + + info("Wait for the TabClose event"); + await onTabClose; +}); + +function triggerBeforeUnload(Page, tab, accept) { + // We use then here because after clicking on the close button, nothing + // in the main block of the function will be executed until the prompt + // is accepted or rejected. Attaching a then to this promise still works. + + const onDialogOpen = Page.javascriptDialogOpening().then( + async dialogEvent => { + await Page.handleJavaScriptDialog({ accept }); + return dialogEvent; + } + ); + + info("Click on the tab close icon"); + tab.closeButton.click(); + + return onDialogOpen; +} diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js b/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js new file mode 100644 index 0000000000..eec7e9828d --- /dev/null +++ b/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for window.confirm(). Check that the dialog is correctly detected and that it can +// be rejected or accepted. +add_task(async function ({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Create a confirm dialog to open"); + const { message, type } = await createConfirmDialog(Page); + + is(type, "confirm", "dialog event contains the correct type"); + is(message, "confirm-1234?", "dialog event contains the correct text"); + + info("Accept the dialog"); + await Page.handleJavaScriptDialog({ accept: true }); + let isConfirmed = await getContentProperty("isConfirmed"); + ok(isConfirmed, "The confirm dialog was accepted"); + + await createConfirmDialog(Page); + info("Trigger another confirm in the test page"); + + info("Reject the dialog"); + await Page.handleJavaScriptDialog({ accept: false }); + isConfirmed = await getContentProperty("isConfirmed"); + ok(!isConfirmed, "The confirm dialog was rejected"); +}); + +function createConfirmDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger a confirm in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.isConfirmed = content.confirm("confirm-1234?"); + }); + + return onDialogOpen; +} diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js b/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js new file mode 100644 index 0000000000..c57f5d5151 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +// Test that javascript dialog events are emitted by the page domain only if +// the dialog is created for the window of the target. +add_task(async function ({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + // Add a listener for dialogs on the test page. + Page.javascriptDialogOpening(() => { + ok(false, "Should never receive this event"); + }); + + info("Open another tab"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + toDataURL("test-page") + ); + is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab"); + + // Create a promise that resolve when dialog prompt is created. + // It will also take care of closing the dialog. + let onOtherPageDialog = PromptTestUtils.handleNextPrompt( + gBrowser.selectedBrowser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "alert" }, + { buttonNumClick: 0 } + ); + + info("Trigger an alert in the second page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alert("test"); + }); + + info("Wait for the alert to be detected and closed"); + await onOtherPageDialog; + + info("Call bringToFront on the test page to make sure we received"); + await Page.bringToFront(); + + BrowserTestUtils.removeTab(otherTab); +}); diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js b/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js new file mode 100644 index 0000000000..c3678e9295 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for window.prompt(). Check that the dialog is correctly detected and that it can +// be rejected or accepted, with a custom prompt text. +add_task(async function ({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Create a prompt dialog to open"); + const { message, type } = await createPromptDialog(Page); + + is(type, "prompt", "dialog event contains the correct type"); + is(message, "prompt-1234", "dialog event contains the correct text"); + + info("Accept the prompt"); + await Page.handleJavaScriptDialog({ accept: true, promptText: "some-text" }); + + let promptResult = await getContentProperty("promptResult"); + is(promptResult, "some-text", "The prompt text was correctly applied"); + + await createPromptDialog(Page); + info("Trigger another prompt in the test page"); + + info("Reject the prompt"); + await Page.handleJavaScriptDialog({ accept: false, promptText: "new-text" }); + + promptResult = await getContentProperty("promptResult"); + ok(!promptResult, "The prompt dialog was rejected"); +}); + +function createPromptDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger a prompt in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.promptResult = content.prompt("prompt-1234"); + }); + + return onDialogOpen; +} diff --git a/remote/cdp/test/browser/page/browser_lifecycleEvent.js b/remote/cdp/test/browser/page/browser_lifecycleEvent.js new file mode 100644 index 0000000000..da5f5da9e8 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_lifecycleEvent.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +add_task(async function noEventsWhenPageDomainDisabled({ client }) { + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventWhenLifeCycleDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterLifeCycleDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + await Page.setLifecycleEventsEnabled({ enabled: false }); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingToURLWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to a URL with no frames"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingToSameURLWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to the same page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenReloadingSameURLWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Reload page with no iframes"); + const pageLoaded = Page.loadEventFired(); + await Page.reload(); + await pageLoaded; + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runPageLifecycleTest(client, expectedEventSets, callback) { + const { Page } = client; + + const LIFECYCLE = "Page.lifecycleEvent"; + const LIFECYCLE_EVENTS = ["init", "DOMContentLoaded", "load"]; + + const expectedEventCount = expectedEventSets * LIFECYCLE_EVENTS.length; + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.lifecycleEvent, + eventName: LIFECYCLE, + messageFn: payload => { + return ( + `Received "${payload.name}" ${LIFECYCLE} ` + + `for frame id ${payload.frameId}` + ); + }, + }); + + await callback(); + + const flattenedFrameTree = await getFlattenedFrameTree(client); + + const lifecycleEvents = await history.record(); + is( + lifecycleEvents.length, + expectedEventCount, + "Got expected amount of lifecycle events" + ); + + if (expectedEventCount == 0) { + return; + } + + // Check lifecycle events for each frame + for (const frame of flattenedFrameTree.values()) { + info(`Check frame id ${frame.id}`); + + const frameEvents = lifecycleEvents.filter(({ payload }) => { + return payload.frameId == frame.id; + }); + + Assert.deepEqual( + frameEvents.map(event => event.payload.name), + LIFECYCLE_EVENTS, + "Received various lifecycle events in the expected order" + ); + + // Check data as exposed by each of these events + let lastTimestamp = frameEvents[0].payload.timestamp; + frameEvents.forEach(({ payload }, index) => { + ok( + payload.timestamp >= lastTimestamp, + "timestamp succeeds the one from the former event" + ); + lastTimestamp = payload.timestamp; + + is(payload.loaderId, frame.loaderId, `event has expected loaderId`); + }); + } +} diff --git a/remote/cdp/test/browser/page/browser_loadEventFired.js b/remote/cdp/test/browser/page/browser_loadEventFired.js new file mode 100644 index 0000000000..e85b298feb --- /dev/null +++ b/remote/cdp/test/browser/page/browser_loadEventFired.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runLoadEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runLoadEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with iframes"); + await loadURL(FRAMESET_MULTI_URL); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(FRAMESET_NESTED_URL); + }); +}); + +async function runLoadEventFiredTest(client, expectedEventCount, callback) { + const { Page } = client; + + if (![0, 1].includes(expectedEventCount)) { + throw new Error(`Invalid value for expectedEventCount`); + } + + const LOAD_EVENT_FIRED = "Page.loadEventFired"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.loadEventFired, + eventName: LOAD_EVENT_FIRED, + messageFn: payload => { + return `Received ${LOAD_EVENT_FIRED} at time ${payload.timestamp}`; + }, + }); + + const timeBefore = Date.now() / 1000; + await callback(); + const loadEventFiredEvents = await history.record(); + const timeAfter = Date.now() / 1000; + + is( + loadEventFiredEvents.length, + expectedEventCount, + "Got expected amount of loadEventFired events" + ); + if (expectedEventCount == 0) { + return; + } + + const timestamp = loadEventFiredEvents[0].payload.timestamp; + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]` + ); +} diff --git a/remote/cdp/test/browser/page/browser_navigate.js b/remote/cdp/test/browser/page/browser_navigate.js new file mode 100644 index 0000000000..8dff31e911 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_navigate.js @@ -0,0 +1,302 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testBasicNavigation({ client }) { + const { Page, Network } = client; + await Page.enable(); + await Network.enable(); + const loadEventFired = Page.loadEventFired(); + const requestEvent = Network.requestWillBeSent(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: PAGE_URL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as corresponding request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + + await loadEventFired; + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + + is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded"); +}); + +add_task(async function testTwoNavigations({ client }) { + const { Page, Network } = client; + await Page.enable(); + await Network.enable(); + let requestEvent = Network.requestWillBeSent(); + let loadEventFired = Page.loadEventFired(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: PAGE_URL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + await loadEventFired; + is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded"); + + loadEventFired = Page.loadEventFired(); + requestEvent = Network.requestWillBeSent(); + const { + frameId: frameId2, + loaderId: loaderId2, + errorText: errorText2, + } = await Page.navigate({ + url: PAGE_URL, + }); + const { loaderId: requestLoaderId2 } = await requestEvent; + ok(!!loaderId, "Page.navigate returns loaderId"); + ok(!!loaderId2, "Page.navigate returns loaderId"); + isnot(loaderId, loaderId2, "Page.navigate returns different loaderIds"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as corresponding request" + ); + is( + loaderId2, + requestLoaderId2, + "Page.navigate returns same loaderId as corresponding request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + is(errorText2, undefined, "No errorText on a successful navigation"); + is(frameId, frameId2, "Page.navigate return same frameId"); + + await loadEventFired; + is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded"); +}); + +add_task(async function testRedirect({ client }) { + const { Page, Network } = client; + const sjsURL = + "https://example.com/browser/remote/cdp/test/browser/page/sjs_redirect.sjs"; + const redirectURL = `${sjsURL}?${PAGE_URL}`; + await Page.enable(); + await Network.enable(); + const requestEvent = Network.requestWillBeSent(); + const loadEventFired = Page.loadEventFired(); + + const { frameId, loaderId, errorText } = await Page.navigate({ + url: redirectURL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as original request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + ok(!!frameId, "Page.navigate returns frameId"); + + await loadEventFired; + is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded"); +}); + +add_task(async function testUnknownHost({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://example-does-not-exist.com", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, "NS_ERROR_UNKNOWN_HOST", "Failed navigation returns errorText"); +}); + +add_task(async function testExpiredCertificate({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://expired.example.com", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + errorText, + "SEC_ERROR_EXPIRED_CERTIFICATE", + "Failed navigation returns errorText" + ); +}); + +add_task(async function testUnknownCertificate({ client }) { + const { Page, Network } = client; + await Network.enable(); + const requestEvent = Network.requestWillBeSent(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://self-signed.example.com", + }); + const { loaderId: requestLoaderId } = await requestEvent; + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as original request" + ); + is(errorText, "SSL_ERROR_UNKNOWN", "Failed navigation returns errorText"); +}); + +add_task(async function testNotFound({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://example.com/browser/remote/doesnotexist.html", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, undefined, "No errorText on a 404"); +}); + +add_task(async function testInvalidURL({ client }) { + const { Page } = client; + + for (let url of ["blah.com", "foo", "https\n//", "http", ""]) { + await Assert.rejects( + Page.navigate({ url }), + err => err.message.includes("invalid URL"), + `Invalid url ${url} causes error` + ); + } + + for (let url of [2, {}, true]) { + await Assert.rejects( + Page.navigate({ url }), + err => err.message.includes("string value expected"), + `Invalid url ${url} causes error` + ); + } +}); + +add_task(async function testDataURL({ client }) { + const { Page } = client; + const url = toDataURL("first"); + await Page.enable(); + const loadEventFired = Page.loadEventFired(); + const frameNavigatedFired = Page.frameNavigated(); + const { frameId, loaderId, errorText } = await Page.navigate({ url }); + is(errorText, undefined, "No errorText on a successful navigation"); + ok(!!loaderId, "Page.navigate returns loaderId"); + + await loadEventFired; + const { frame } = await frameNavigatedFired; + is(frame.loaderId, loaderId, "Page.navigate returns expected loaderId"); + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + is(gBrowser.selectedBrowser.currentURI.spec, url, "Expected URL loaded"); +}); + +add_task(async function testFileURL({ client }) { + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + if (AppConstants.DEBUG) { + // Bug 1634695 Navigating to a file URL forces the TabSession to destroy + // abruptly and content domains are not properly destroyed, which creates + // window leaks and fails the test in DEBUG mode. + return; + } + + const { Page } = client; + const dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("doc_empty.html"); + + // The file can be a symbolic link on local build. Normalize it to make sure + // the path matches to the actual URI opened in the new tab. + dir.normalize(); + const url = Services.io.newFileURI(dir).spec; + const browser = gBrowser.selectedTab.linkedBrowser; + const loaded = BrowserTestUtils.browserLoaded(browser, false, url); + + const { /* frameId, */ loaderId, errorText } = await Page.navigate({ url }); + is(errorText, undefined, "No errorText on a successful navigation"); + ok(!!loaderId, "Page.navigate returns loaderId"); + + // Bug 1634693 Page.loadEventFired isn't emitted after file: navigation + await loaded; + is(browser.currentURI.spec, url, "Expected URL loaded"); + // Bug 1634695 Navigating to file: returns wrong frame id and hangs + // content page domain methods + // const currentFrame = await getTopFrame(client); + // ok(frameId === currentFrame.id, "Page.navigate returns expected frameId"); +}); + +add_task(async function testAbout({ client }) { + const { Page } = client; + await Page.enable(); + let loadEventFired = Page.loadEventFired(); + let frameNavigatedFired = Page.frameNavigated(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "about:blank", + }); + ok(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, undefined, "No errorText on a successful navigation"); + + await loadEventFired; + const { frame } = await frameNavigatedFired; + is(frame.loaderId, loaderId, "Page.navigate returns expected loaderId"); + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + is( + gBrowser.selectedBrowser.currentURI.spec, + "about:blank", + "Expected URL loaded" + ); +}); + +add_task(async function testSameDocumentNavigation({ client }) { + const { Page } = client; + const { frameId, loaderId } = await Page.navigate({ + url: PAGE_URL, + }); + ok(!!loaderId, "Page.navigate returns loaderId"); + + await Page.enable(); + const navigatedWithinDocument = Page.navigatedWithinDocument(); + + info("Check that Page.navigate can navigate to an anchor"); + const sameDocumentURL = `${PAGE_URL}#hash`; + const { frameId: sameDocumentFrameId, loaderId: sameDocumentLoaderId } = + await Page.navigate({ url: sameDocumentURL }); + ok( + !sameDocumentLoaderId, + "Page.navigate does not return a loaderId for same document navigation" + ); + is( + sameDocumentFrameId, + frameId, + "Page.navigate returned the expected frame id" + ); + + const { frameId: navigatedFrameId, url } = await navigatedWithinDocument; + is( + frameId, + navigatedFrameId, + "navigatedWithinDocument returns the expected frameId" + ); + is(url, sameDocumentURL, "navigatedWithinDocument returns the expected url"); + is( + gBrowser.selectedBrowser.currentURI.spec, + sameDocumentURL, + "Expected URL loaded" + ); + + info("Check that navigating to the same hash URL does not timeout"); + const { frameId: sameHashFrameId, loaderId: sameHashLoaderId } = + await Page.navigate({ url: sameDocumentURL }); + ok( + !sameHashLoaderId, + "Page.navigate does not return a loaderId for same document navigation" + ); + is(sameHashFrameId, frameId, "Page.navigate returned the expected frame id"); +}); + +async function getTopFrame(client) { + const frames = await getFlattenedFrameTree(client); + return Array.from(frames.values())[0]; +} diff --git a/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js b/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js new file mode 100644 index 0000000000..8a7f3cb21a --- /dev/null +++ b/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function toUnknownEntryId({ client }) { + const { Page } = client; + + const { entries } = await Page.getNavigationHistory(); + const ids = entries.map(entry => entry.id); + + await Assert.rejects( + Page.navigateToHistoryEntry({ entryId: Math.max(...ids) + 1 }), + /No entry with passed id/, + "Unknown entry id raised error" + ); +}); + +add_task(async function toSameEntry({ client }) { + const { Page } = client; + + const data = generateHistoryData(1); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[0].url, + "Expected URL loaded" + ); +}); + +add_task(async function oneEntryBackInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex - 1].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, currentIndex - 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[currentIndex - 1].url, + "Expected URL loaded" + ); +}); + +add_task(async function oneEntryForwardInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex + 1].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, currentIndex + 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[currentIndex + 1].url, + "Expected URL loaded" + ); +}); + +add_task(async function toFirstEntryInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[0].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[0].url, + "Expected URL loaded" + ); +}); + +add_task(async function toLastEntryInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const { entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ + entryId: entries[entries.length - 1].id, + }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, data.length - 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[data.length - 1].url, + "Expected URL loaded" + ); +}); diff --git a/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js b/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js new file mode 100644 index 0000000000..13bab69387 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await loadURL(PAGE_URL); + await runNavigatedWithinDocumentTest(client, 0, async () => { + info("Navigate to the '#hash' anchor in the page"); + await navigateToAnchor(PAGE_URL, "hash"); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await loadURL(PAGE_URL); + + await runNavigatedWithinDocumentTest(client, 0, async () => { + info("Navigate to the '#hash' anchor in the page"); + await navigateToAnchor(PAGE_URL, "hash"); + }); +}); + +add_task(async function eventWhenNavigatingToHash({ client }) { + const { Page } = client; + + await Page.enable(); + + await loadURL(PAGE_URL); + + await runNavigatedWithinDocumentTest(client, 1, async () => { + info("Navigate to the '#hash' anchor in the page"); + await navigateToAnchor(PAGE_URL, "hash"); + }); +}); + +add_task(async function eventWhenNavigatingToDifferentHash({ client }) { + const { Page } = client; + + await Page.enable(); + + await navigateToAnchor(PAGE_URL, "hash"); + + await runNavigatedWithinDocumentTest(client, 1, async () => { + info("Navigate to the '#hash' anchor in the page"); + await navigateToAnchor(PAGE_URL, "other-hash"); + }); +}); + +add_task(async function eventWhenNavigatingToHashInFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await loadURL(FRAMESET_NESTED_URL); + + await runNavigatedWithinDocumentTest(client, 1, async () => { + info("Navigate to the '#hash' anchor in the first iframe"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const iframe = content.frames[0]; + const baseUrl = iframe.location.href; + iframe.location.href = baseUrl + "#hash-first-frame"; + }); + }); + + await runNavigatedWithinDocumentTest(client, 2, async () => { + info("Navigate to the '#hash' anchor in the nested iframes"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const nestedFrame1 = content.frames[0].frames[0]; + const baseUrl1 = nestedFrame1.location.href; + nestedFrame1.location.href = baseUrl1 + "#hash-nested-frame-1"; + const nestedFrame2 = content.frames[0].frames[0]; + const baseUrl2 = nestedFrame2.location.href; + nestedFrame2.location.href = baseUrl2 + "#hash-nested-frame-2"; + }); + }); +}); + +async function runNavigatedWithinDocumentTest( + client, + expectedEventCount, + callback +) { + const { Page } = client; + + const NAVIGATED = "Page.navigatedWithinDocument"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.navigatedWithinDocument, + eventName: NAVIGATED, + messageFn: payload => { + return `Received ${NAVIGATED} for frame id ${payload.frameId}`; + }, + }); + + await callback(); + + const navigatedWithinDocumentEvents = await history.record(); + + is( + navigatedWithinDocumentEvents.length, + expectedEventCount, + "Got expected amount of navigatedWithinDocument events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + navigatedWithinDocumentEvents.forEach(({ payload }) => { + const { frameId, url } = payload; + + const frame = frames.get(frameId); + ok(frame, "Returned a valid frame id"); + is(url, frame.url, "Returned the expectedUrl"); + }); +} + +function navigateToAnchor(baseUrl, hash) { + const url = `${baseUrl}#${hash}`; + const onLocationChange = BrowserTestUtils.waitForLocationChange( + gBrowser, + url + ); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); + return onLocationChange; +} diff --git a/remote/cdp/test/browser/page/browser_navigationEvents.js b/remote/cdp/test/browser/page/browser_navigationEvents.js new file mode 100644 index 0000000000..1e589a3970 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_navigationEvents.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Page navigation events + +const RANDOM_ID_PAGE_URL = toDataURL( + `<script>window.randomId = Math.random() + "-" + Date.now();</script>` +); + +const promises = new Set(); +const resolutions = new Map(); + +add_task(async function pageWithoutFrame({ client }) { + await loadURL(PAGE_URL); + + const { Page } = client; + + // turn on navigation related events, such as DOMContentLoaded et al. + await Page.enable(); + info("Page domain has been enabled"); + + const { frameTree } = await Page.getFrameTree(); + + // Save the given `promise` resolution into the `promises` global Set + function recordPromise(name, promise) { + promise.then(event => { + info(`Received Page.${name}`); + resolutions.set(name, event); + }); + promises.add(promise); + } + // Record all Page events that we assert in this test + function recordPromises() { + recordPromise("frameStartedLoading", Page.frameStartedLoading()); + recordPromise("frameNavigated", Page.frameNavigated()); + recordPromise("domContentEventFired", Page.domContentEventFired()); + recordPromise("loadEventFired", Page.loadEventFired()); + recordPromise("frameStoppedLoading", Page.frameStoppedLoading()); + } + + info("Test Page.navigate"); + recordPromises(); + + let navigatedWithinDocumentResolved = false; + Page.navigatedWithinDocument().finally( + () => (navigatedWithinDocumentResolved = true) + ); + + const url = RANDOM_ID_PAGE_URL; + const { frameId } = await Page.navigate({ url }); + info("A new page has been requested"); + + ok(frameId, "Page.navigate returned a frameId"); + is( + frameId, + frameTree.frame.id, + "The Page.navigate's frameId is the same than getFrameTree's one" + ); + + await assertNavigationEvents({ url, frameId }); + + const randomId1 = await getTestTabRandomId(); + ok(!!randomId1, "Test tab has a valid randomId"); + + info("Test Page.reload"); + recordPromises(); + + await Page.reload(); + info("The page has been reloaded"); + + await assertNavigationEvents({ url, frameId }); + + const randomId2 = await getTestTabRandomId(); + ok(!!randomId2, "Test tab has a valid randomId"); + isnot( + randomId2, + randomId1, + "Test tab randomId has been updated after reload" + ); + + info("Test Page.navigate with the same URL still reloads the current page"); + recordPromises(); + + await Page.navigate({ url }); + info("The page has been reloaded"); + + await assertNavigationEvents({ url, frameId }); + + const randomId3 = await getTestTabRandomId(); + ok(!!randomId3, "Test tab has a valid randomId"); + isnot( + randomId3, + randomId2, + "Test tab randomId has been updated after reload" + ); + + ok( + !navigatedWithinDocumentResolved, + "navigatedWithinDocument never resolved during the test" + ); +}); + +add_task(async function pageWithSingleFrame({ client }) { + const { Page } = client; + + await Page.enable(); + + // Store all frameNavigated events in an array + const frameNavigatedEvents = []; + Page.frameNavigated(e => frameNavigatedEvents.push(e)); + + info("Navigate to a page containing an iframe"); + const onStoppedLoading = Page.frameStoppedLoading(); + const { frameId } = await Page.navigate({ url: FRAMESET_SINGLE_URL }); + await onStoppedLoading; + + is(frameNavigatedEvents.length, 2, "Received 2 frameNavigated events"); + is( + frameNavigatedEvents[0].frame.id, + frameId, + "Received the correct frameId for the frameNavigated event" + ); +}); + +add_task(async function sameDocumentNavigation({ client }) { + await loadURL(PAGE_URL); + + const { Page } = client; + + // turn on navigation related events, such as DOMContentLoaded et al. + await Page.enable(); + info("Page domain has been enabled"); + + const { frameTree } = await Page.getFrameTree(); + + info("Test Page.navigate for a same document navigation"); + const onNavigatedWithinDocument = Page.navigatedWithinDocument(); + + let unexpectedEventResolved = false; + Promise.race([ + Page.frameStartedLoading(), + Page.frameNavigated(), + Page.domContentEventFired(), + Page.loadEventFired(), + Page.frameStoppedLoading(), + ]).then(() => (unexpectedEventResolved = true)); + + const url = `${PAGE_URL}#some-hash`; + const { frameId } = await Page.navigate({ url }); + ok(frameId, "Page.navigate returned a frameId"); + is( + frameId, + frameTree.frame.id, + "The Page.navigate's frameId is the same than getFrameTree's one" + ); + + const event = await onNavigatedWithinDocument; + is( + event.frameId, + frameId, + "The navigatedWithinDocument frameId is the same as in Page.navigate" + ); + is(event.url, url, "The navigatedWithinDocument url is the expected url"); + ok(!unexpectedEventResolved, "No unexpected navigation event resolved."); +}); + +async function assertNavigationEvents({ url, frameId }) { + // Wait for all the promises to resolve + await Promise.all(promises); + + // Assert the order in which they resolved + const expectedResolutions = [ + "frameStartedLoading", + "frameNavigated", + "domContentEventFired", + "loadEventFired", + "frameStoppedLoading", + ]; + Assert.deepEqual( + [...resolutions.keys()], + expectedResolutions, + "Received various Page navigation events in the expected order" + ); + + // Now assert the data exposed by each of these events + const frameStartedLoading = resolutions.get("frameStartedLoading"); + is( + frameStartedLoading.frameId, + frameId, + "frameStartedLoading frameId is the same one" + ); + + const frameNavigated = resolutions.get("frameNavigated"); + ok( + !frameNavigated.frame.parentId, + "frameNavigated is for the top level document and has a null parentId" + ); + is(frameNavigated.frame.id, frameId, "frameNavigated id is the right one"); + is( + frameNavigated.frame.name, + undefined, + "frameNavigated name isn't implemented yet" + ); + is(frameNavigated.frame.url, url, "frameNavigated url is the right one"); + + const frameStoppedLoading = resolutions.get("frameStoppedLoading"); + is( + frameStoppedLoading.frameId, + frameId, + "frameStoppedLoading frameId is the same one" + ); + + promises.clear(); + resolutions.clear(); +} + +async function getTestTabRandomId() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.randomId; + }); +} diff --git a/remote/cdp/test/browser/page/browser_printToPDF.js b/remote/cdp/test/browser/page/browser_printToPDF.js new file mode 100644 index 0000000000..fed1a6162e --- /dev/null +++ b/remote/cdp/test/browser/page/browser_printToPDF.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div style='background-color: green'>Hello world</div>"); + +add_task(async function transferModes({ client }) { + const { IO, Page } = client; + await loadURL(DOC); + + // as base64 encoded data + const base64 = await Page.printToPDF({ transferMode: "ReturnAsBase64" }); + is(base64.stream, null, "No stream handle is returned"); + ok(!!base64.data, "Base64 encoded data is returned"); + verifyPDF(atob(base64.data).trimEnd()); + + // defaults to base64 encoded data + const defaults = await Page.printToPDF(); + is(defaults.stream, null, "By default no stream handle is returned"); + ok(!!defaults.data, "By default base64 encoded data is returned"); + verifyPDF(atob(defaults.data).trimEnd()); + + // unknown transfer modes default to base64 + const fallback = await Page.printToPDF({ transferMode: "ReturnAsFoo" }); + is(fallback.stream, null, "Unknown mode doesn't return a stream"); + ok(!!fallback.data, "Unknown mode defaults to base64 encoded data"); + verifyPDF(atob(fallback.data).trimEnd()); + + // as stream handle + const stream = await Page.printToPDF({ transferMode: "ReturnAsStream" }); + ok(!!stream.stream, "Stream handle is returned"); + is(stream.data, null, "No base64 encoded data is returned"); + let streamData = ""; + + while (true) { + const { data, base64Encoded, eof } = await IO.read({ + handle: stream.stream, + }); + streamData += base64Encoded ? atob(data) : data; + if (eof) { + await IO.close({ handle: stream.stream }); + break; + } + } + + verifyPDF(streamData.trimEnd()); +}); + +function verifyPDF(data) { + is(data.slice(0, 5), "%PDF-", "Decoded data starts with the PDF signature"); + is(data.slice(-5), "%%EOF", "Decoded data ends with the EOF flag"); +} diff --git a/remote/cdp/test/browser/page/browser_reload.js b/remote/cdp/test/browser/page/browser_reload.js new file mode 100644 index 0000000000..0872337551 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_reload.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testReload({ client }) { + const { Page } = client; + await loadURL(toDataURL("halløj")); + + info("Reloading document"); + await Page.enable(); + const loaded = Page.loadEventFired(); + await Page.reload(); + await loaded; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + ok(!content.docShell.isForceReloading, "Document is not force-reloaded"); + }); +}); + +add_task(async function testReloadIgnoreCache({ client }) { + const { Page } = client; + await loadURL(toDataURL("halløj")); + + info("Force-reloading document"); + await Page.enable(); + const loaded = Page.loadEventFired(); + await Page.reload({ ignoreCache: true }); + await loaded; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + ok(content.docShell.isForceReloading, "Document is force-reloaded"); + }); +}); diff --git a/remote/cdp/test/browser/page/browser_runtimeEvents.js b/remote/cdp/test/browser/page/browser_runtimeEvents.js new file mode 100644 index 0000000000..7f6d7ec926 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_runtimeEvents.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Assert the order of Runtime.executionContextDestroyed, +// Page.frameNavigated, and Runtime.executionContextCreated + +add_task(async function testCDP({ client }) { + await loadURL(PAGE_URL); + + const { Page, Runtime } = client; + + const events = []; + function assertReceivedEvents(expected, message) { + Assert.deepEqual(events, expected, message); + // Empty the list of received events + events.splice(0); + } + Page.frameNavigated(() => { + events.push("frameNavigated"); + }); + Runtime.executionContextCreated(() => { + events.push("executionContextCreated"); + }); + Runtime.executionContextDestroyed(() => { + events.push("executionContextDestroyed"); + }); + + // turn on navigation related events, such as DOMContentLoaded et al. + await Page.enable(); + info("Page domain has been enabled"); + + const onExecutionContextCreated = Runtime.executionContextCreated(); + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Runtime.enable will dispatch `executionContextCreated` for the existing document + let { context } = await onExecutionContextCreated; + ok(!!context.id, `The execution context has an id ${context.id}`); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + assertReceivedEvents( + ["executionContextCreated"], + "Received only executionContextCreated event after Runtime.enable call" + ); + + const { frameTree } = await Page.getFrameTree(); + is( + frameTree.frame.id, + context.auxData.frameId, + "getFrameTree and executionContextCreated refers about the same frame Id" + ); + + const onFrameNavigated = Page.frameNavigated(); + const onExecutionContextDestroyed = Runtime.executionContextDestroyed(); + const onExecutionContextCreated2 = Runtime.executionContextCreated(); + const url = toDataURL("test-page"); + const { frameId } = await Page.navigate({ url }); + info("A new page has been requested"); + ok(frameId, "Page.navigate returned a frameId"); + is( + frameId, + frameTree.frame.id, + "The Page.navigate's frameId is the same than getFrameTree's one" + ); + + const frameNavigated = await onFrameNavigated; + ok( + !frameNavigated.frame.parentId, + "frameNavigated is for the top level document and has a null parentId" + ); + is( + frameNavigated.frame.id, + frameId, + "frameNavigated id is the same than the one returned by Page.navigate" + ); + is( + frameNavigated.frame.name, + undefined, + "frameNavigated name isn't implemented yet" + ); + is( + frameNavigated.frame.url, + url, + "frameNavigated url is the same being given to Page.navigate" + ); + + const { executionContextId } = await onExecutionContextDestroyed; + ok(executionContextId, "The destroyed event reports an id"); + is( + executionContextId, + context.id, + "The destroyed event is for the first reported execution context" + ); + + ({ context } = await onExecutionContextCreated2); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + is( + context.auxData.frameId, + frameId, + "The execution context frame id is the same " + + "the one returned by Page.navigate" + ); + + isnot( + executionContextId, + context.id, + "The destroyed id is different from the created one" + ); + + assertReceivedEvents( + ["executionContextDestroyed", "frameNavigated", "executionContextCreated"], + "Received frameNavigated between the two execution context events during navigation to another URL" + ); +}); diff --git a/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js b/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js new file mode 100644 index 0000000000..274119ffd4 --- /dev/null +++ b/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test Page.addScriptToEvaluateOnNewDocument and Page.removeScriptToEvaluateOnNewDocument +// +// TODO Bug 1601695 - Schedule script evaluation and check for correct frame id + +const WORLD = "testWorld"; + +add_task(async function uniqueIdForAddedScripts({ client }) { + const { Page, Runtime } = client; + + await loadURL(PAGE_URL); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length, "Script id is non-empty"); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + ok(id2.length, "Script id is non-empty"); + isnot(id1, id2, "Two scripts should have different ids"); + + await Runtime.enable(); + + // flush event for PAGE_URL default context + await Runtime.executionContextCreated(); + await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL, []); +}); + +add_task(async function addScriptAfterNavigation({ client }) { + const { Page } = client; + + await loadURL(PAGE_URL); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length, "Script id is non-empty"); + + await loadURL(PAGE_FRAME_URL); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 2;", + }); + ok(id2.length, "Script id is non-empty"); + isnot(id1, id2, "Two scripts should have different ids"); +}); + +add_task(async function addWithIsolatedWorldAndNavigate({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + await Runtime.enable(); + + const contextsCreated = recordContextCreated(Runtime, 3); + + const loadEventFired = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: PAGE_URL }); + await loadEventFired; + + // flush context-created events for the steps above + await contextsCreated; + + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: WORLD, + }); + + const isolatedId = await Page.createIsolatedWorld({ + frameId, + worldName: WORLD, + grantUniversalAccess: true, + }); + + const contexts = await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL); + isnot(contexts[1].id, isolatedId, "The context has a new id"); +}); + +add_task(async function addWithIsolatedWorldNavigateTwice({ client }) { + const { Page, Runtime } = client; + + await Runtime.enable(); + + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: WORLD, + }); + + await checkIsolatedContextAfterLoad(client, PAGE_URL); + await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL); +}); + +add_task(async function addTwoScriptsWithIsolatedWorld({ client }) { + const { Page, Runtime } = client; + + await Runtime.enable(); + + const names = [WORLD, "A_whole_new_world"]; + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: names[0], + }); + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 8;", + worldName: names[1], + }); + + await checkIsolatedContextAfterLoad(client, PAGE_URL, names); +}); + +function recordContextCreated(Runtime, expectedCount) { + return new Promise(resolve => { + const ctx = []; + const unsubscribe = Runtime.executionContextCreated(payload => { + ctx.push(payload.context); + info( + `Runtime.executionContextCreated: ${payload.context.auxData.type} ` + + `(${payload.context.origin})` + ); + if (ctx.length > expectedCount) { + unsubscribe(); + resolve(ctx); + } + }); + timeoutPromise(1000).then(() => { + unsubscribe(); + resolve(ctx); + }); + }); +} + +async function checkIsolatedContextAfterLoad(client, url, names = [WORLD]) { + const { Page, Runtime } = client; + + await Page.enable(); + + // At least the default context will get created + const expected = names.length + 1; + + const contextsCreated = recordContextCreated(Runtime, expected); + const frameNavigated = Page.frameNavigated(); + const { frameId } = await Page.navigate({ url }); + await frameNavigated; + const contexts = await contextsCreated; + + is(contexts.length, expected, "Expected number of contexts got created"); + is(contexts[0].auxData.frameId, frameId, "Expected frame id found"); + is(contexts[0].auxData.isDefault, true, "Got default context"); + is(contexts[0].auxData.type, "default", "Got default context"); + is(contexts[0].name, "", "Get context with empty name"); + + names.forEach((name, index) => { + is(contexts[index + 1].name, name, "Get context with expected name"); + is(contexts[index + 1].auxData.frameId, frameId, "Expected frame id found"); + is(contexts[index + 1].auxData.isDefault, false, "Got isolated context"); + is(contexts[index + 1].auxData.type, "isolated", "Got isolated context"); + }); + + return contexts; +} diff --git a/remote/cdp/test/browser/page/doc_empty.html b/remote/cdp/test/browser/page/doc_empty.html new file mode 100644 index 0000000000..e59d2d8901 --- /dev/null +++ b/remote/cdp/test/browser/page/doc_empty.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Empty page</title> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/page/doc_frame.html b/remote/cdp/test/browser/page/doc_frame.html new file mode 100644 index 0000000000..e2efd61554 --- /dev/null +++ b/remote/cdp/test/browser/page/doc_frame.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frame page</title> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/page/doc_frameset_multi.html b/remote/cdp/test/browser/page/doc_frameset_multi.html new file mode 100644 index 0000000000..dd59a60431 --- /dev/null +++ b/remote/cdp/test/browser/page/doc_frameset_multi.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frameset with multiple frames</title> +</head> +<body> + <iframe src="doc_empty.html"></iframe> + <iframe src="doc_frame.html"></iframe> +</body> +</html> diff --git a/remote/cdp/test/browser/page/doc_frameset_nested.html b/remote/cdp/test/browser/page/doc_frameset_nested.html new file mode 100644 index 0000000000..bd0b4b48c9 --- /dev/null +++ b/remote/cdp/test/browser/page/doc_frameset_nested.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frameset with nested frames</title> +</head> +<body> + <iframe src="doc_frameset_multi.html"></iframe> +</body> +</html> diff --git a/remote/cdp/test/browser/page/doc_frameset_single.html b/remote/cdp/test/browser/page/doc_frameset_single.html new file mode 100644 index 0000000000..2ad56a140e --- /dev/null +++ b/remote/cdp/test/browser/page/doc_frameset_single.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frameset with a single frame</title> +</head> +<body> + <iframe src="doc_frame.html"></iframe> +</body> +</html> diff --git a/remote/cdp/test/browser/page/head.js b/remote/cdp/test/browser/page/head.js new file mode 100644 index 0000000000..46a4bdc21b --- /dev/null +++ b/remote/cdp/test/browser/page/head.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); + +const { PollPromise } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Sync.sys.mjs" +); + +const BASE_ORIGIN = "https://example.com"; +const BASE_PATH = `${BASE_ORIGIN}/browser/remote/cdp/test/browser/page`; +const FRAMESET_MULTI_URL = `${BASE_PATH}/doc_frameset_multi.html`; +const FRAMESET_NESTED_URL = `${BASE_PATH}/doc_frameset_nested.html`; +const FRAMESET_SINGLE_URL = `${BASE_PATH}/doc_frameset_single.html`; +const PAGE_FRAME_URL = `${BASE_PATH}/doc_frame.html`; +const PAGE_URL = `${BASE_PATH}/doc_empty.html`; + +const TIMEOUT_SET_HISTORY_INDEX = 1000; + +function assertHistoryEntries(history, expectedData, expectedIndex) { + const { currentIndex, entries } = history; + + is(currentIndex, expectedIndex, "Got expected current index"); + is( + entries.length, + expectedData.length, + "Found expected count of history entries" + ); + + entries.forEach((entry, index) => { + ok(!!entry.id, "History entry has an id set"); + is( + entry.url, + expectedData[index].url, + "History entry has the correct URL set" + ); + is( + entry.userTypedURL, + expectedData[index].userTypedURL, + "History entry has the correct user typed URL set" + ); + is( + entry.title, + expectedData[index].title, + "History entry has the correct title set" + ); + }); +} + +function generateHistoryData(count) { + const data = []; + + for (let index = 0; index < count; index++) { + const url = toDataURL(`<head><title>Test ${index + 1}</title></head>`); + data.push({ + url, + userTypedURL: url, + title: `Test ${index + 1}`, + }); + } + + return data; +} + +async function getContentSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const docEl = content.document.documentElement; + + return { + x: 0, + y: 0, + width: docEl.scrollWidth, + height: docEl.scrollHeight, + }; + }); +} + +async function getViewportSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return { + x: content.pageXOffset, + y: content.pageYOffset, + width: content.innerWidth, + height: content.innerHeight, + }; + }); +} + +function getCurrentHistoryIndex() { + return new Promise(resolve => { + SessionStore.getSessionHistory(window.gBrowser.selectedTab, history => { + resolve(history.index); + }); + }); +} + +async function gotoHistoryIndex(index) { + gBrowser.gotoIndex(index); + + // On some platforms the requested index isn't set immediately. + await PollPromise( + async (resolve, reject) => { + const currentIndex = await getCurrentHistoryIndex(); + if (currentIndex == index) { + resolve(); + } else { + reject(); + } + }, + { timeout: TIMEOUT_SET_HISTORY_INDEX } + ); +} diff --git a/remote/cdp/test/browser/page/sjs_redirect.sjs b/remote/cdp/test/browser/page/sjs_redirect.sjs new file mode 100644 index 0000000000..b3dbf44f53 --- /dev/null +++ b/remote/cdp/test/browser/page/sjs_redirect.sjs @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/remote/cdp/test/browser/runtime/browser.toml b/remote/cdp/test/browser/runtime/browser.toml new file mode 100644 index 0000000000..8632b7fad2 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser.toml @@ -0,0 +1,48 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "doc_console_events.html", + "doc_console_events_onload.html", + "doc_empty.html", + "doc_frame.html", + "doc_frameset_single.html", + "head.js", +] + +["browser_callFunctionOn.js"] + +["browser_callFunctionOn_awaitPromise.js"] + +["browser_callFunctionOn_returnByValue.js"] + +["browser_consoleAPICalled.js"] +https_first_disabled = true + +["browser_evaluate.js"] + +["browser_evaluate_awaitPromise.js"] + +["browser_evaluate_returnByValue.js"] + +["browser_exceptionThrown.js"] +https_first_disabled = true + +["browser_executionContextEvents.js"] + +["browser_getProperties.js"] + +["browser_remoteObjects.js"] diff --git a/remote/cdp/test/browser/runtime/browser_callFunctionOn.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn.js new file mode 100644 index 0000000000..965f9f9267 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn.js @@ -0,0 +1,285 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function FunctionDeclarationMissing({ client }) { + const { Runtime } = client; + await Assert.rejects( + Runtime.callFunctionOn(), + err => err.message.includes("functionDeclaration: string value expected"), + "functionDeclaration: string value expected" + ); +}); + +add_task(async function functionDeclarationInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const functionDeclaration of [null, true, 1, [], {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ functionDeclaration, executionContextId }), + err => err.message.includes("functionDeclaration: string value expected"), + "functionDeclaration: string value expected" + ); + } +}); + +add_task(async function functionDeclarationGetCurrentLocation({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => location.href", + executionContextId, + }); + is(result.value, TEST_DOC, "Works against the test page"); +}); + +add_task(async function argumentsInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const args of [null, true, 1, "foo", {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + arguments: args, + executionContextId, + }), + err => err.message.includes("arguments: array value expected"), + "arguments: array value expected" + ); + } +}); + +add_task(async function argumentsPrimitiveTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const args of [null, true, 1, "foo", {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + arguments: args, + executionContextId, + }), + err => err.message.includes("arguments: array value expected"), + "arguments: array value expected" + ); + } +}); + +add_task(async function executionContextIdNorObjectIdSpecified({ client }) { + const { Runtime } = client; + + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + }), + err => + err.message.includes( + "Either objectId or executionContextId must be specified" + ), + "Either objectId or executionContextId must be specified" + ); +}); + +add_task(async function executionContextIdInvalidTypes({ client }) { + const { Runtime } = client; + + for (const executionContextId of [null, true, "foo", [], {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId, + }), + err => err.message.includes("executionContextId: number value expected"), + "executionContextId: number value expected" + ); + } +}); + +add_task(async function executionContextIdInvalidValue({ client }) { + const { Runtime } = client; + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId: -1, + }), + err => err.message.includes("Cannot find context with specified id"), + "Cannot find context with specified id" + ); +}); + +add_task(async function objectIdInvalidTypes({ client }) { + const { Runtime } = client; + + for (const objectId of [null, true, 1, [], {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ functionDeclaration: "", objectId }), + err => err.message.includes("objectId: string value expected"), + "objectId: string value expected" + ); + } +}); + +add_task(async function objectId({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + // First create an object + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => ({ foo: 42 })", + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for objects"); + ok(!!result.objectId, "Got an object id"); + + // Then apply a method on this object + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: "function () { return this.foo; }", + executionContextId, + objectId: result.objectId, + }); + + is(result2.type, "number", "The type is correct"); + is(result2.subtype, undefined, "The subtype is undefined for numbers"); + is(result2.value, 42, "Expected value returned"); +}); + +add_task(async function objectIdArgumentReference({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + // First create a remote JS object + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => ({ foo: 1 })", + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for objects"); + ok(!!result.objectId, "Got an object id"); + + // Then increment the `foo` attribute of this JS object, + // while returning this attribute value + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: result.objectId }], + executionContextId, + }); + + Assert.deepEqual( + result2, + { + type: "number", + value: 2, + }, + "The result has the expected type and value" + ); + + // Finally, try to pass this JS object and get it back. Ensure that it + // returns the same object id. Also increment the attribute again. + const { result: result3 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => { arg.foo++; return arg; }", + arguments: [{ objectId: result.objectId }], + executionContextId, + }); + + is(result3.type, "object", "The type is correct"); + is(result3.subtype, undefined, "The subtype is undefined for objects"); + // Remote objects don't have unique ids. So you may have multiple object ids + // that reference the same remote object + ok(!!result3.objectId, "Got an object id"); + isnot(result3.objectId, result.objectId, "The object id is different"); + + // Assert that we can still access this object and that its foo attribute + // has been incremented. Use the second object id we got from previous call + // to callFunctionOn. + const { result: result4 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => arg.foo", + arguments: [{ objectId: result3.objectId }], + executionContextId, + }); + + Assert.deepEqual( + result4, + { + type: "number", + value: 3, + }, + "The result has the expected type and value" + ); +}); + +add_task(async function exceptionDetailsJavascriptError({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "doesNotExists()", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "doesNotExists is not defined", + }, + "Javascript error is passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowError({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => { throw new Error('foo') }", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowValue({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => { throw 'foo' }", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + exception: { + type: "string", + value: "foo", + }, + }, + "Exception details are passed as a RemoteObject" + ); +}); diff --git a/remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js new file mode 100644 index 0000000000..a0f96d9b33 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function awaitPromiseInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const awaitPromise of [null, 1, "foo", [], {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + awaitPromise, + executionContextId, + }), + err => err.message.includes("awaitPromise: boolean value expected"), + "awaitPromise: boolean value expected" + ); + } +}); + +add_task(async function awaitPromiseResolve({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.resolve(42)", + awaitPromise: true, + executionContextId, + }); + + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseDelayedResolve({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: true, + executionContextId, + }); + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseReject({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.reject(42)", + awaitPromise: true, + executionContextId, + }); + // TODO: Implement all values for exceptionDetails (bug 1548480) + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedReject({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: + "() => new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: true, + executionContextId, + }); + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedRejectError({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: + "() => new Promise((_,r) => setTimeout(() => r(new Error('foo')), 0))", + awaitPromise: true, + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function awaitPromiseResolveWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.resolve(42)", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseRejectWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.reject(42)", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: + "() => new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); diff --git a/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js new file mode 100644 index 0000000000..8ad34e9a56 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js @@ -0,0 +1,395 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function returnAsObjectTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [ + { expression: "({foo:true})", type: "object", subtype: undefined }, + { expression: "Symbol('foo')", type: "symbol", subtype: undefined }, + { expression: "new Promise(()=>{})", type: "object", subtype: "promise" }, + { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" }, + { expression: "new WeakMap()", type: "object", subtype: "weakmap" }, + { expression: "new WeakSet()", type: "object", subtype: "weakset" }, + { expression: "new Map()", type: "object", subtype: "map" }, + { expression: "new Set()", type: "object", subtype: "set" }, + { expression: "/foo/", type: "object", subtype: "regexp" }, + { expression: "[1, 2]", type: "object", subtype: "array" }, + { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" }, + { expression: "new Date()", type: "object", subtype: "date" }, + { + expression: "document", + type: "object", + subtype: "node", + className: "HTMLDocument", + description: "#document", + }, + { + expression: `{{ + const div = document.createElement('div'); + div.id = "foo"; + return div; + }}`, + type: "object", + subtype: "node", + className: "HTMLDivElement", + description: "div#foo", + }, + ]; + + for (const entry of expressions) { + const { expression, type, subtype, className, description } = entry; + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${expression}`, + executionContextId, + }); + + is(result.type, type, "The type is correct"); + is(result.subtype, subtype, "The subtype is correct"); + ok(!!result.objectId, "Got an object id"); + if (className) { + is(result.className, className, "The className is correct"); + } + if (description) { + is(result.description, description, "The description is correct"); + } + } +}); + +add_task(async function returnAsObjectDifferentObjectIds({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [{}, "document"]; + for (const expression of expressions) { + const { result: result1 } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + is( + result1.objectId, + result2.objectId, + `Different object ids returned for ${expression}` + ); + } +}); + +add_task(async function returnAsObjectPrimitiveTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [42, "42", true, 4.2]; + for (const expression of expressions) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + is(result.value, expression, `Evaluating primitive '${expression}' works`); + is(result.type, typeof expression, `${expression} type is correct`); + } +}); + +add_task(async function returnAsObjectNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const expression of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${expression}`, + executionContextId, + }); + Assert.deepEqual( + result, + { + type, + unserializableValue: expression, + description: expression, + }, + `Evaluating unserializable '${expression}' works` + ); + } + } +}); + +// `null` is special as it has its own subtype, is of type 'object' +// but is returned as a value, without an `objectId` attribute +add_task(async function returnAsObjectNull({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => null", + executionContextId, + }); + Assert.deepEqual( + result, + { + type: "object", + subtype: "null", + value: null, + }, + "Null type is correct" + ); +}); + +// undefined doesn't work with JSON.stringify, so test it independently +add_task(async function returnAsObjectUndefined({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => undefined", + executionContextId, + }); + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function returnByValueInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const returnByValue of [null, 1, "foo", [], {}]) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId, + returnByValue, + }), + err => err.message.includes("returnByValue: boolean value expected"), + "returnByValue: boolean value expected" + ); + } +}); + +add_task(async function returnByValueCyclicValue({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const functionDeclarations = [ + "() => { const b = { a: 1}; b.b = b; return b; }", + "() => window", + ]; + + for (const functionDeclaration of functionDeclarations) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration, + executionContextId, + returnByValue: true, + }), + err => err.message.includes("Object reference chain is too long"), + "Object reference chain is too long" + ); + } +}); + +add_task(async function returnByValueNotPossible({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const functionDeclarations = [ + "() => Symbol('foo')", + "() => [Symbol('foo')]", + "() => { return {a: Symbol('foo')}; }", + ]; + + for (const functionDeclaration of functionDeclarations) { + await Assert.rejects( + Runtime.callFunctionOn({ + functionDeclaration, + executionContextId, + returnByValue: true, + }), + err => err.message.includes("Object couldn't be returned by value"), + "Object couldn't be returned by value" + ); + } +}); + +add_task(async function returnByValue({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const values = [ + null, + 42, + 42.0, + "42", + true, + false, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => (${JSON.stringify(value)})`, + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + "The returned value is the same than the input value" + ); + } +}); + +add_task(async function returnByValueNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => (${unserializableValue})`, + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + "The returned value is the same than the input value" + ); + } + } +}); + +// Test undefined individually as JSON.stringify doesn't return a string +add_task(async function returnByValueUndefined({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => {}", + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function returnByValueArguments({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const values = [ + 42, + 42.0, + "42", + true, + false, + null, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "a => a", + arguments: [{ value }], + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + "The returned value is the same than the input value" + ); + } +}); + +add_task(async function returnByValueArgumentsNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "a => a", + arguments: [{ unserializableValue }], + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + "The returned value is the same than the input value" + ); + } + } +}); diff --git a/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js b/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js new file mode 100644 index 0000000000..eac774bd28 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js @@ -0,0 +1,380 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Request a longer timeout as we have many tests which are longer +requestLongerTimeout(2); + +const PAGE_CONSOLE_EVENTS = + "https://example.com/browser/remote/cdp/test/browser/runtime/doc_console_events.html"; +const PAGE_CONSOLE_EVENTS_ONLOAD = + "https://example.com/browser/remote/cdp/test/browser/runtime/doc_console_events_onload.html"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + await runConsoleTest(client, 0, async () => { + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.console.log("foo") + ); + }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + await runConsoleTest(client, 0, async () => { + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.console.log("foo") + ); + }); +}); + +add_task(async function noEventsForJavascriptErrors({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + const context = await enableRuntime(client); + + await runConsoleTest(client, 0, async () => { + evaluate(client, context.id, () => { + document.getElementById("js-error").click(); + }); + }); +}); + +add_task(async function consoleAPI({ client }) { + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, () => { + console.log("foo"); + }); + }); + + is(events[0].type, "log", "Got expected type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); +}); + +add_task(async function consoleAPIBeforeEnable({ client }) { + const { Runtime } = client; + const timeBefore = Date.now(); + + const check = async () => { + const events = await runConsoleTest( + client, + 1, + async () => { + await Runtime.enable(); + }, + // Set custom before timestamp as the event is before our callback + { timeBefore } + ); + + is(events[0].type, "log", "Got expected type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + }; + + // Load the page which runs a log on load + await loadURL(PAGE_CONSOLE_EVENTS_ONLOAD); + await check(); + + // Disable and re-enable Runtime domain, should send event again + await Runtime.disable(); + await check(); +}); + +add_task(async function consoleAPITypes({ client }) { + const expectedEvents = ["dir", "error", "log", "timeEnd", "trace", "warning"]; + const levels = ["dir", "error", "log", "time", "timeEnd", "trace", "warn"]; + + const context = await enableRuntime(client); + + const events = await runConsoleTest( + client, + expectedEvents.length, + async () => { + for (const level of levels) { + await evaluate( + client, + context.id, + level => { + console[level]("foo"); + }, + level + ); + } + } + ); + + events.forEach((event, index) => { + console.log(`Check for event type "${expectedEvents[index]}"`); + is(event.type, expectedEvents[index], "Got expected type"); + }); +}); + +add_task(async function consoleAPIArgumentsCount({ client }) { + const argumentList = [[], ["foo"], ["foo", "bar", "cheese"]]; + + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, argumentList.length, async () => { + for (const args of argumentList) { + await evaluate( + client, + context.id, + args => { + console.log(...args); + }, + args + ); + } + }); + + events.forEach((event, index) => { + console.log(`Check for event args "${argumentList[index]}"`); + + const argValues = event.args.map(arg => arg.value); + Assert.deepEqual(argValues, argumentList[index], "Got expected args"); + }); +}); + +add_task(async function consoleAPIArgumentsAsRemoteObject({ client }) { + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, () => { + console.log("foo", Symbol("foo")); + }); + }); + + Assert.equal(events[0].args.length, 2, "Got expected amount of arguments"); + + is(events[0].args[0].type, "string", "Got expected argument type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + + is(events[0].args[1].type, "symbol", "Got expected argument type"); + is( + events[0].args[1].description, + "Symbol(foo)", + "Got expected argument description" + ); + ok(!!events[0].args[1].objectId, "Got objectId for argument"); +}); + +add_task(async function consoleAPIByContentInteraction({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.getElementById("console-error").click(); + }); + }); + + is(events[0].type, "error", "Got expected type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); + + const { callFrames } = events[0].stackTrace; + is(callFrames.length, 1, "Got expected amount of call frames"); + + is(callFrames[0].functionName, "", "Got expected call frame function name"); + is(callFrames[0].lineNumber, 0, "Got expected call frame line number"); + is(callFrames[0].columnNumber, 8, "Got expected call frame column number"); + is( + callFrames[0].url, + "javascript:console.error('foo')", + "Got expected call frame URL" + ); +}); + +add_task(async function consoleAPIByScript({ client }) { + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, function runLog() { + console.trace("foo"); + }); + }); + + const { callFrames } = events[0].stackTrace; + is(callFrames.length, 1, "Got expected amount of call frames"); + + is( + callFrames[0].functionName, + "runLog", + "Got expected call frame function name" + ); + is(callFrames[0].lineNumber, 1, "Got expected call frame line number"); + is(callFrames[0].columnNumber, 14, "Got expected call frame column number"); + is(callFrames[0].url, "debugger eval code", "Got expected call frame URL"); +}); + +add_task(async function consoleAPIByScriptSubstack({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, () => { + document.getElementById("log-wrapper").click(); + }); + }); + + const { callFrames } = events[0].stackTrace; + is(callFrames.length, 5, "Got expected amount of call frames"); + + is( + callFrames[0].functionName, + "runLogCaller", + "Got expected call frame function name (frame 1)" + ); + is( + callFrames[0].lineNumber, + 13, + "Got expected call frame line number (frame 1)" + ); + is( + callFrames[0].columnNumber, + 16, + "Got expected call frame column number (frame 1)" + ); + is( + callFrames[0].url, + PAGE_CONSOLE_EVENTS, + "Got expected call frame UR (frame 1)" + ); + + is( + callFrames[1].functionName, + "runLogChild", + "Got expected call frame function name (frame 2)" + ); + is( + callFrames[1].lineNumber, + 17, + "Got expected call frame line number (frame 2)" + ); + is( + callFrames[1].columnNumber, + 8, + "Got expected call frame column number (frame 2)" + ); + is( + callFrames[1].url, + PAGE_CONSOLE_EVENTS, + "Got expected call frame URL (frame 2)" + ); + + is( + callFrames[2].functionName, + "runLogParent", + "Got expected call frame function name (frame 3)" + ); + is( + callFrames[2].lineNumber, + 20, + "Got expected call frame line number (frame 3)" + ); + is( + callFrames[2].columnNumber, + 6, + "Got expected call frame column number (frame 3)" + ); + is( + callFrames[2].url, + PAGE_CONSOLE_EVENTS, + "Got expected call frame URL (frame 3)" + ); + + is( + callFrames[3].functionName, + "onclick", + "Got expected call frame function name (frame 4)" + ); + is( + callFrames[3].lineNumber, + 0, + "Got expected call frame line number (frame 4)" + ); + is( + callFrames[3].columnNumber, + 0, + "Got expected call frame column number (frame 4)" + ); + is( + callFrames[3].url, + PAGE_CONSOLE_EVENTS, + "Got expected call frame URL (frame 4)" + ); + + is( + callFrames[4].functionName, + "", + "Got expected call frame function name (frame 5)" + ); + is( + callFrames[4].lineNumber, + 1, + "Got expected call frame line number (frame 5)" + ); + is( + callFrames[4].columnNumber, + 45, + "Got expected call frame column number (frame 5)" + ); + is( + callFrames[4].url, + "debugger eval code", + "Got expected call frame URL (frame 5)" + ); +}); + +async function runConsoleTest(client, eventCount, callback, options = {}) { + let { timeBefore } = options; + + const { Runtime } = client; + + const EVENT_CONSOLE_API_CALLED = "Runtime.consoleAPICalled"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Runtime.consoleAPICalled, + eventName: EVENT_CONSOLE_API_CALLED, + messageFn: payload => + `Received ${EVENT_CONSOLE_API_CALLED} for ${payload.type}`, + }); + + timeBefore ??= Date.now(); + await callback(); + + const consoleAPIentries = await history.record(); + is(consoleAPIentries.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for consoleAPICalled events + consoleAPIentries.forEach(({ payload }) => { + const timestamp = payload.timestamp; + + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]` + ); + }); + + return consoleAPIentries.map(event => event.payload); +} diff --git a/remote/cdp/test/browser/runtime/browser_evaluate.js b/remote/cdp/test/browser/runtime/browser_evaluate.js new file mode 100644 index 0000000000..871462439e --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_evaluate.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function contextIdInvalidValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + await Assert.rejects( + Runtime.evaluate({ expression: "", contextId: -1 }), + err => err.message.includes("Cannot find context with specified id"), + "Cannot find context with specified id" + ); +}); + +add_task(async function contextIdNotSpecified({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ expression: "location.href" }); + is(result.value, TEST_DOC, "Works against the current document"); +}); + +add_task(async function contextIdSpecified({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + const { id: contextId } = await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "location.href", + contextId, + }); + is(result.value, TEST_DOC, "Works against the targetted document"); +}); + +add_task(async function returnAsObjectTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [ + { expression: "({foo:true})", type: "object", subtype: undefined }, + { expression: "Symbol('foo')", type: "symbol", subtype: undefined }, + { expression: "new Promise(()=>{})", type: "object", subtype: "promise" }, + { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" }, + { expression: "new WeakMap()", type: "object", subtype: "weakmap" }, + { expression: "new WeakSet()", type: "object", subtype: "weakset" }, + { expression: "new Map()", type: "object", subtype: "map" }, + { expression: "new Set()", type: "object", subtype: "set" }, + { expression: "/foo/", type: "object", subtype: "regexp" }, + { expression: "[1, 2]", type: "object", subtype: "array" }, + { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" }, + { expression: "new Date()", type: "object", subtype: "date" }, + { + expression: "document", + type: "object", + subtype: "node", + className: "HTMLDocument", + description: "#document", + }, + { + expression: `(() => {{ + const div = document.createElement('div'); + div.id = "foo"; + return div; + }})()`, + type: "object", + subtype: "node", + className: "HTMLDivElement", + description: "div#foo", + }, + ]; + + for (const entry of expressions) { + const { expression, type, subtype, className, description } = entry; + + const { result } = await Runtime.evaluate({ expression }); + + is(result.type, type, "The type is correct"); + is(result.subtype, subtype, "The subtype is correct"); + ok(!!result.objectId, "Got an object id"); + if (className) { + is(result.className, className, "The className is correct"); + } + if (description) { + is(result.description, description, "The description is correct"); + } + } +}); + +add_task(async function returnAsObjectDifferentObjectIds({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [{}, "document"]; + for (const expression of expressions) { + const { result: result1 } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + const { result: result2 } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + is( + result1.objectId, + result2.objectId, + `Different object ids returned for ${expression}` + ); + } +}); + +add_task(async function returnAsObjectPrimitiveTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [42, "42", true, 4.2]; + for (const expression of expressions) { + const { result } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + is(result.value, expression, `Evaluating primitive '${expression}' works`); + is(result.type, typeof expression, `${expression} type is correct`); + } +}); + +add_task(async function returnAsObjectNotSerializable({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const expression of notSerializableNumbers[type]) { + const { result } = await Runtime.evaluate({ expression }); + Assert.deepEqual( + result, + { + type, + unserializableValue: expression, + description: expression, + }, + `Evaluating unserializable '${expression}' works` + ); + } + } +}); + +// `null` is special as it has its own subtype, is of type 'object' +// but is returned as a value, without an `objectId` attribute +add_task(async function returnAsObjectNull({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "null", + }); + Assert.deepEqual( + result, + { + type: "object", + subtype: "null", + value: null, + }, + "Null type is correct" + ); +}); + +// undefined doesn't work with JSON.stringify, so test it independently +add_task(async function returnAsObjectUndefined({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "undefined", + }); + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function exceptionDetailsJavascriptError({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "doesNotExists()", + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "doesNotExists is not defined", + }, + "Javascript error is passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowError({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "throw new Error('foo')", + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "throw 'foo'", + }); + + Assert.deepEqual( + exceptionDetails, + { + exception: { + type: "string", + value: "foo", + }, + }, + "Exception details are passed as a RemoteObject" + ); +}); diff --git a/remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js b/remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js new file mode 100644 index 0000000000..2b1c87c2d3 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function awaitPromiseInvalidTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + for (const awaitPromise of [null, 1, "foo", [], {}]) { + await Assert.rejects( + Runtime.evaluate({ + expression: "", + awaitPromise, + }), + err => err.message.includes("awaitPromise: boolean value expected"), + "awaitPromise: boolean value expected" + ); + } +}); + +add_task(async function awaitPromiseResolve({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.resolve(42)", + awaitPromise: true, + }); + + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseReject({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "Promise.reject(42)", + awaitPromise: true, + }); + // TODO: Implement all values for exceptionDetails (bug 1548480) + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedResolve({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: true, + }); + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseDelayedReject({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: true, + }); + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedRejectError({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: + "new Promise((_,r) => setTimeout(() => r(new Error('foo')), 0))", + awaitPromise: true, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function awaitPromiseResolveWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.resolve(42)", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseRejectWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.reject(42)", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); diff --git a/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js b/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js new file mode 100644 index 0000000000..762d280b31 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function returnByValueInvalidTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + for (const returnByValue of [null, 1, "foo", [], {}]) { + await Assert.rejects( + Runtime.evaluate({ + expression: "", + returnByValue, + }), + err => err.message.includes("returnByValue: boolean value expected"), + "returnByValue: boolean value expected" + ); + } +}); + +add_task(async function returnByValueCyclicValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = ["const b = { a: 1}; b.b = b; b", "window"]; + + for (const expression of expressions) { + await Assert.rejects( + Runtime.evaluate({ + expression, + returnByValue: true, + }), + err => err.message.includes("Object reference chain is too long"), + "Object reference chain is too long" + ); + } +}); + +add_task(async function returnByValueNotPossible({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = ["Symbol(42)", "[Symbol(42)]", "{a: Symbol(42)}"]; + + for (const expression of expressions) { + await Assert.rejects( + Runtime.evaluate({ + expression, + returnByValue: true, + }), + err => err.message.includes("Object couldn't be returned by value"), + "Object couldn't be returned by value" + ); + } +}); + +add_task(async function returnByValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const values = [ + null, + 42, + 42.0, + "42", + true, + false, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.evaluate({ + expression: `(${JSON.stringify(value)})`, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + `Returned expected value for ${JSON.stringify(value)}` + ); + } +}); + +add_task(async function returnByValueNotSerializable({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.evaluate({ + expression: `(${unserializableValue})`, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + `Returned expected value for ${JSON.stringify(unserializableValue)}` + ); + } + } +}); + +// Test undefined individually as JSON.stringify doesn't return a string +add_task(async function returnByValueUndefined({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "undefined", + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); diff --git a/remote/cdp/test/browser/runtime/browser_exceptionThrown.js b/remote/cdp/test/browser/runtime/browser_exceptionThrown.js new file mode 100644 index 0000000000..23e6dc2de9 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_exceptionThrown.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_CONSOLE_EVENTS = + "https://example.com/browser/remote/cdp/test/browser/runtime/doc_console_events.html"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function noEventsForScriptErrorWithoutException({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function eventsForScriptErrorWithException({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + + const context = await enableRuntime(client); + + const events = await runExceptionThrownTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.getElementById("js-error").click(); + }); + }); + + is( + typeof events[0].exceptionId, + "number", + "Got expected type for exception id" + ); + is( + events[0].text, + "TypeError: foo.click is not a function", + "Got expected text" + ); + is(events[0].lineNumber, 8, "Got expected line number"); + is(events[0].columnNumber, 10, "Got expected column number"); + is(events[0].url, PAGE_CONSOLE_EVENTS, "Got expected url"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); + + const callFrames = events[0].stackTrace.callFrames; + is(callFrames.length, 2, "Got expected amount of call frames"); + + is(callFrames[0].functionName, "throwError", "Got expected function name"); + is(typeof callFrames[0].scriptId, "string", "Got scriptId as string"); + is(callFrames[0].url, PAGE_CONSOLE_EVENTS, "Got expected url"); + is(callFrames[0].lineNumber, 8, "Got expected line number"); + is(callFrames[0].columnNumber, 10, "Got expected column number"); + + is(callFrames[1].functionName, "onclick", "Got expected function name"); + is(callFrames[1].url, PAGE_CONSOLE_EVENTS, "Got expected url"); +}); + +async function runExceptionThrownTest( + client, + eventCount, + callback, + options = {} +) { + const { Runtime } = client; + + const EVENT_EXCEPTION_THROWN = "Runtime.exceptionThrown"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Runtime.exceptionThrown, + eventName: EVENT_EXCEPTION_THROWN, + messageFn: payload => `Received "${payload.name}"`, + }); + + const timeBefore = Date.now(); + await callback(); + + const exceptionThrownEvents = await history.record(); + is(exceptionThrownEvents.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for entryAdded events + exceptionThrownEvents.forEach(event => { + const details = event.payload.exceptionDetails; + const timestamp = event.payload.timestamp; + + is(typeof details, "object", "Got expected 'exceptionDetails' property"); + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]` + ); + }); + + return exceptionThrownEvents.map(event => event.payload.exceptionDetails); +} diff --git a/remote/cdp/test/browser/runtime/browser_executionContextEvents.js b/remote/cdp/test/browser/runtime/browser_executionContextEvents.js new file mode 100644 index 0000000000..9e6230901a --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_executionContextEvents.js @@ -0,0 +1,332 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime execution context events + +const DESTROYED = "Runtime.executionContextDestroyed"; +const CREATED = "Runtime.executionContextCreated"; +const CLEARED = "Runtime.executionContextsCleared"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + const history = recordContextEvents(Runtime, 0); + await loadURL(PAGE_FRAME_URL); + await assertEventOrder({ history, expectedEvents: [] }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + const history = recordContextEvents(Runtime, 0); + await loadURL(PAGE_FRAME_URL); + await assertEventOrder({ history, expectedEvents: [] }); +}); + +add_task(async function eventsWhenNavigatingWithNoFrames({ client }) { + const { Page, Runtime } = client; + + const previousContext = await enableRuntime(client); + const history = recordContextEvents(Runtime, 3); + + const { frameId } = await Page.navigate({ url: PAGE_FRAME_URL }); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = + history.findEvent(DESTROYED).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context: contextCreated } = history.findEvent(CREATED).payload; + checkDefaultContext(contextCreated); + isnot( + contextCreated.id, + previousContext.id, + "The new execution context has a different id" + ); + is( + contextCreated.auxData.frameId, + frameId, + "The execution context frame id is the same " + + "than the one returned by Page.navigate" + ); +}); + +add_task(async function eventsWhenNavigatingFrameSet({ client }) { + const { Runtime } = client; + + const previousContext = await enableRuntime(client); + + // Check navigation to a frameset + const historyTo = recordContextEvents(Runtime, 4); + await loadURL(FRAMESET_SINGLE_URL); + await assertEventOrder({ + history: historyTo, + expectedEvents: [DESTROYED, CLEARED, CREATED, CREATED], + }); + + const { executionContextId: destroyedId } = + historyTo.findEvent(DESTROYED).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const contexts = historyTo.findEvents(CREATED); + const createdTopContext = contexts[0].payload.context; + const createdFrameContext = contexts[1].payload.context; + + checkDefaultContext(createdTopContext); + isnot( + createdTopContext.id, + previousContext.id, + "The new execution context has a different id" + ); + is( + createdTopContext.origin, + BASE_ORIGIN, + "The execution context origin is the frameset" + ); + + checkDefaultContext(createdFrameContext); + isnot( + createdFrameContext.id, + createdTopContext.id, + "The new frame's execution context has a different id" + ); + is( + createdFrameContext.origin, + BASE_ORIGIN, + "The frame's execution context origin is the frame" + ); + + // Check navigation from a frameset + const historyFrom = recordContextEvents(Runtime, 4); + await loadURL(PAGE_FRAME_URL); + await assertEventOrder({ + history: historyFrom, + // Bug 1644657: The cleared event should come last but we emit destroy events + // for the top-level context and for frames afterward. Chrome only sends out + // the cleared event on navigation. + expectedEvents: [DESTROYED, CLEARED, CREATED, DESTROYED], + }); + + const destroyedContextIds = historyFrom.findEvents(DESTROYED); + is( + destroyedContextIds[0].payload.executionContextId, + createdTopContext.id, + "The destroyed event reports the previous context id" + ); + is( + destroyedContextIds[1].payload.executionContextId, + createdFrameContext.id, + "The destroyed event reports the previous frame's context id" + ); + + const { context: contextCreated } = historyFrom.findEvent(CREATED).payload; + checkDefaultContext(contextCreated); + isnot( + contextCreated.id, + createdTopContext.id, + "The new execution context has a different id" + ); + is( + contextCreated.origin, + BASE_ORIGIN, + "The execution context origin is not the frameset" + ); +}); + +add_task(async function eventsWhenNavigatingBackWithNoFrames({ client }) { + const { Runtime } = client; + + // Load an initial URL so that navigating back will work + await loadURL(PAGE_FRAME_URL); + const previousContext = await enableRuntime(client); + + const executionContextCreated = Runtime.executionContextCreated(); + await loadURL(PAGE_URL); + const { context: createdContext } = await executionContextCreated; + + const history = recordContextEvents(Runtime, 3); + gBrowser.selectedBrowser.goBack(); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = + history.findEvent(DESTROYED).payload; + is( + destroyedId, + createdContext.id, + "The destroyed event reports the current context id" + ); + + const { context } = history.findEvent(CREATED).payload; + checkDefaultContext(context); + is( + context.origin, + previousContext.origin, + "The new execution context has the same origin as the previous one." + ); + isnot( + context.id, + previousContext.id, + "The new execution context has a different id" + ); + ok(context.auxData.isDefault, "The execution context is the default one"); + is( + context.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is always the same" + ); + is(context.auxData.type, "default", "Execution context has 'default' type"); + is(context.name, "", "The default execution context is named ''"); + + const { result } = await Runtime.evaluate({ + contextId: context.id, + expression: "location.href", + }); + is( + result.value, + PAGE_FRAME_URL, + "Runtime.evaluate works and is against the page we just navigated to" + ); +}); + +add_task(async function eventsWhenReloadingPageWithNoFrames({ client }) { + const { Page, Runtime } = client; + + // Load an initial URL so that reload will work + await loadURL(PAGE_FRAME_URL); + const previousContext = await enableRuntime(client); + + await Page.enable(); + + const history = recordContextEvents(Runtime, 3); + const frameNavigated = Page.frameNavigated(); + gBrowser.selectedBrowser.reload(); + await frameNavigated; + + await assertEventOrder({ history }); + + const { executionContextId } = history.findEvent(DESTROYED).payload; + is( + executionContextId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context } = history.findEvent(CREATED).payload; + checkDefaultContext(context); + is( + context.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is the same as before reloading" + ); + + isnot( + executionContextId, + context.id, + "The destroyed id is different from the created one" + ); +}); + +add_task(async function eventsWhenNavigatingByLocationWithNoFrames({ client }) { + const { Runtime } = client; + + const previousContext = await enableRuntime(client); + const history = recordContextEvents(Runtime, 3); + + await Runtime.evaluate({ + contextId: previousContext.id, + expression: `window.location = '${PAGE_FRAME_URL}';`, + }); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = + history.findEvent(DESTROYED).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context: createdContext } = history.findEvent(CREATED).payload; + checkDefaultContext(createdContext); + is( + createdContext.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is identical " + + "to the one from before before setting the window's location" + ); + isnot( + destroyedId, + createdContext.id, + "The destroyed id is different from the created one" + ); +}); + +function recordContextEvents(Runtime, total) { + const history = new RecordEvents(total); + + history.addRecorder({ + event: Runtime.executionContextDestroyed, + eventName: DESTROYED, + messageFn: payload => { + return `Received ${DESTROYED} for id ${payload.executionContextId}`; + }, + }); + history.addRecorder({ + event: Runtime.executionContextCreated, + eventName: CREATED, + messageFn: ({ context }) => { + return ( + `Received ${CREATED} for id ${context.id}` + + ` type: ${context.auxData.type}` + + ` name: ${context.name}` + + ` origin: ${context.origin}` + ); + }, + }); + history.addRecorder({ + event: Runtime.executionContextsCleared, + eventName: CLEARED, + }); + + return history; +} + +async function assertEventOrder(options = {}) { + const { history, expectedEvents = [DESTROYED, CLEARED, CREATED] } = options; + const events = await history.record(); + const eventNames = events.map(item => item.eventName); + info(`Expected events: ${expectedEvents}`); + info(`Received events: ${eventNames}`); + + is( + events.length, + expectedEvents.length, + "Received expected number of Runtime context events" + ); + Assert.deepEqual( + events.map(item => item.eventName), + expectedEvents, + "Received Runtime context events in expected order" + ); +} + +function checkDefaultContext(context) { + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + is(context.auxData.type, "default", "Execution context has 'default' type"); + ok(!!context.origin, "The execution context has an origin"); + is(context.name, "", "The default execution context is named ''"); +} diff --git a/remote/cdp/test/browser/runtime/browser_getProperties.js b/remote/cdp/test/browser/runtime/browser_getProperties.js new file mode 100644 index 0000000000..f897c3dfcd --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_getProperties.js @@ -0,0 +1,184 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime remote object + +add_task(async function ({ client }) { + const firstContext = await testRuntimeEnable(client); + const contextId = firstContext.id; + + await testGetOwnSimpleProperties(client, contextId); + await testGetCustomProperty(client, contextId); + await testGetPrototypeProperties(client, contextId); + await testGetGetterSetterProperties(client, contextId); +}); + +async function testRuntimeEnable({ Runtime }) { + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +async function testGetOwnSimpleProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ bool: true, fun() {}, int: 1, object: {}, string: 'foo' })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is( + result2.length, + 5, + "ownProperties=true allows to iterate only over direct object properties (i.e. ignore prototype)" + ); + result2.sort((a, b) => a.name > b.name); + is(result2[0].name, "bool"); + is(result2[0].configurable, true); + is(result2[0].enumerable, true); + is(result2[0].writable, true); + is(result2[0].value.type, "boolean"); + is(result2[0].value.value, true); + is(result2[0].isOwn, true); + + is(result2[1].name, "fun"); + is(result2[1].configurable, true); + is(result2[1].enumerable, true); + is(result2[1].writable, true); + is(result2[1].value.type, "function"); + ok(!!result2[1].value.objectId); + is(result2[1].isOwn, true); + + is(result2[2].name, "int"); + is(result2[2].configurable, true); + is(result2[2].enumerable, true); + is(result2[2].writable, true); + is(result2[2].value.type, "number"); + is(result2[2].value.value, 1); + is(result2[2].isOwn, true); + + is(result2[3].name, "object"); + is(result2[3].configurable, true); + is(result2[3].enumerable, true); + is(result2[3].writable, true); + is(result2[3].value.type, "object"); + ok(!!result2[3].value.objectId); + is(result2[3].isOwn, true); + + is(result2[4].name, "string"); + is(result2[4].configurable, true); + is(result2[4].enumerable, true); + is(result2[4].writable, true); + is(result2[4].value.type, "string"); + is(result2[4].value.value, "foo"); + is(result2[4].isOwn, true); +} + +async function testGetPrototypeProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ foo: 42 })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: false, + }); + ok(result2.length > 1, "We have more properties than just the object one"); + const foo = result2.find(p => p.name == "foo"); + ok(foo, "The object property is described"); + ok(foo.isOwn, "and is reported as 'own' property"); + + const toString = result2.find(p => p.name == "toString"); + ok( + toString, + "Function from Object's prototype are also described like toString" + ); + ok(!toString.isOwn, "but are reported as not being an 'own' property"); +} + +async function testGetGetterSetterProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: + "({ get prop() { return this.x; }, set prop(v) { this.x = v; } })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is(result2.length, 1); + + is(result2[0].name, "prop"); + is(result2[0].configurable, true); + is(result2[0].enumerable, true); + is( + result2[0].writable, + undefined, + "writable is only set for data properties" + ); + + is(result2[0].get.type, "function"); + ok(!!result2[0].get.objectId); + is(result2[0].set.type, "function"); + ok(!!result2[0].set.objectId); + + is(result2[0].isOwn, true); + + const { result: result3 } = await Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "(set, get) => { set(42); return get(); }", + arguments: [ + { objectId: result2[0].set.objectId }, + { objectId: result2[0].get.objectId }, + ], + }); + is(result3.type, "number", "The type is correct"); + is(result3.subtype, undefined, "The subtype is undefined for numbers"); + is(result3.value, 42, "The getter returned the value set by the setter"); +} + +async function testGetCustomProperty({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: `const obj = {}; Object.defineProperty(obj, "prop", { value: 42 }); obj`, + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is(result2.length, 1, "We only get the one object's property"); + is(result2[0].name, "prop"); + is(result2[0].configurable, false); + is(result2[0].enumerable, false); + is(result2[0].writable, false); + is(result2[0].value.type, "number"); + is(result2[0].value.value, 42); + is(result2[0].isOwn, true); +} diff --git a/remote/cdp/test/browser/runtime/browser_remoteObjects.js b/remote/cdp/test/browser/runtime/browser_remoteObjects.js new file mode 100644 index 0000000000..bfe412e966 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_remoteObjects.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime remote object + +add_task(async function ({ client }) { + const firstContext = await testRuntimeEnable(client); + const contextId = firstContext.id; + + await testObjectRelease(client, contextId); +}); + +async function testRuntimeEnable({ Runtime }) { + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +async function testObjectRelease({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ foo: 42 })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "obj => JSON.stringify(obj)", + arguments: [{ objectId: result.objectId }], + }); + is(result2.type, "string", "The type is correct"); + is(result2.value, JSON.stringify({ foo: 42 }), "Got the object's JSON"); + + const { result: result3 } = await Runtime.callFunctionOn({ + objectId: result.objectId, + functionDeclaration: "function () { return this.foo; }", + }); + is(result3.type, "number", "The type is correct"); + is(result3.value, 42, "Got the object's foo attribute"); + + await Runtime.releaseObject({ + objectId: result.objectId, + }); + info("Object is released"); + + await Assert.rejects( + Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "() => {}", + arguments: [{ objectId: result.objectId }], + }), + err => err.message.includes("Could not find object with given id"), + "callFunctionOn throws on released argument" + ); + + await Assert.rejects( + Runtime.callFunctionOn({ + objectId: result.objectId, + functionDeclaration: "() => {}", + }), + err => err.message.includes("Cannot find context with specified id"), + "callFunctionOn throws on released target" + ); +} diff --git a/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js b/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js new file mode 100644 index 0000000000..20b8264f92 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function enableRuntime_noHangAfterNavigation({ client }) { + await loadURL(PAGE_URL); + await enableRuntime(client); +}); diff --git a/remote/cdp/test/browser/runtime/browser_with_default_prefs.toml b/remote/cdp/test/browser/runtime/browser_with_default_prefs.toml new file mode 100644 index 0000000000..cfb0a77352 --- /dev/null +++ b/remote/cdp/test/browser/runtime/browser_with_default_prefs.toml @@ -0,0 +1,15 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "doc_empty.html", + "head.js", +] + +["browser_withDefaultPrefs.js"] diff --git a/remote/cdp/test/browser/runtime/doc_console_events.html b/remote/cdp/test/browser/runtime/doc_console_events.html new file mode 100644 index 0000000000..f52fcfa555 --- /dev/null +++ b/remote/cdp/test/browser/runtime/doc_console_events.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Empty page</title> + <script> + function throwError() { + let foo = {}; + foo.click(); + } + + function runLogParent() { + function runLogCaller() { + console.trace("foo"); + } + + function runLogChild() { + runLogCaller(); + } + + runLogChild(); + } + </script> +</head> +<body> + <a id="console-log" href="javascript:console.log('foo')">console.log()</a><br/> + <a id="console-error" href="javascript:console.error('foo')">console.error()</a><br/> + <a id="js-error" onclick="throwError()">Javascript Error</a><br/> + <a id="log-wrapper" onclick="runLogParent()">console.log() in function wrappers</a><br/> +</body> +</html> diff --git a/remote/cdp/test/browser/runtime/doc_console_events_onload.html b/remote/cdp/test/browser/runtime/doc_console_events_onload.html new file mode 100644 index 0000000000..608a22991a --- /dev/null +++ b/remote/cdp/test/browser/runtime/doc_console_events_onload.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Console events onload</title> + <script> + console.log("foo"); + </script> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/runtime/doc_empty.html b/remote/cdp/test/browser/runtime/doc_empty.html new file mode 100644 index 0000000000..e59d2d8901 --- /dev/null +++ b/remote/cdp/test/browser/runtime/doc_empty.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Empty page</title> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/runtime/doc_frame.html b/remote/cdp/test/browser/runtime/doc_frame.html new file mode 100644 index 0000000000..e2efd61554 --- /dev/null +++ b/remote/cdp/test/browser/runtime/doc_frame.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frame page</title> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/runtime/doc_frameset_single.html b/remote/cdp/test/browser/runtime/doc_frameset_single.html new file mode 100644 index 0000000000..2ad56a140e --- /dev/null +++ b/remote/cdp/test/browser/runtime/doc_frameset_single.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Frameset with a single frame</title> +</head> +<body> + <iframe src="doc_frame.html"></iframe> +</body> +</html> diff --git a/remote/cdp/test/browser/runtime/head.js b/remote/cdp/test/browser/runtime/head.js new file mode 100644 index 0000000000..8f05f225ec --- /dev/null +++ b/remote/cdp/test/browser/runtime/head.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); + +const BASE_ORIGIN = "https://example.com"; +const BASE_PATH = `${BASE_ORIGIN}/browser/remote/cdp/test/browser/runtime`; +const FRAMESET_SINGLE_URL = `${BASE_PATH}/doc_frameset_single.html`; +const PAGE_FRAME_URL = `${BASE_PATH}/doc_frame.html`; +const PAGE_URL = `${BASE_PATH}/doc_empty.html`; diff --git a/remote/cdp/test/browser/security/browser.toml b/remote/cdp/test/browser/security/browser.toml new file mode 100644 index 0000000000..d57fe6a61d --- /dev/null +++ b/remote/cdp/test/browser/security/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_setIgnoreCertificateErrors.js"] diff --git a/remote/cdp/test/browser/security/browser_setIgnoreCertificateErrors.js b/remote/cdp/test/browser/security/browser_setIgnoreCertificateErrors.js new file mode 100644 index 0000000000..d21e737ebe --- /dev/null +++ b/remote/cdp/test/browser/security/browser_setIgnoreCertificateErrors.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { STATE_IS_SECURE, STATE_IS_BROKEN, STATE_IS_INSECURE } = + Ci.nsIWebProgressListener; + +// from ../../../build/pgo/server-locations.txt +const NO_CERT = "https://nocert.example.com:443"; +const SELF_SIGNED = "https://self-signed.example.com:443"; +const UNTRUSTED = "https://untrusted.example.com:443"; +const EXPIRED = "https://expired.example.com:443"; +const MISMATCH_EXPIRED = "https://mismatch.expired.example.com:443"; +const MISMATCH_UNTRUSTED = "https://mismatch.untrusted.example.com:443"; +const UNTRUSTED_EXPIRED = "https://untrusted-expired.example.com:443"; +const MISMATCH_UNTRUSTED_EXPIRED = + "https://mismatch.untrusted-expired.example.com:443"; + +const BAD_CERTS = [ + NO_CERT, + SELF_SIGNED, + UNTRUSTED, + EXPIRED, + MISMATCH_EXPIRED, + MISMATCH_UNTRUSTED, + UNTRUSTED_EXPIRED, + MISMATCH_UNTRUSTED_EXPIRED, +]; + +function getConnectionState() { + // prevents items that are being lazy loaded causing issues + document.getElementById("identity-icon-box").click(); + gIdentityHandler.refreshIdentityPopup(); + return document.getElementById("identity-popup").getAttribute("connection"); +} + +/** + * Compares the security state of the page with what is expected. + * Returns one of "secure", "broken", "insecure", or "unknown". + */ +function isSecurityState(browser, expectedState) { + const ui = browser.securityUI; + if (!ui) { + ok(false, "No security UI to get the security state"); + return; + } + + const isSecure = ui.state & STATE_IS_SECURE; + const isBroken = ui.state & STATE_IS_BROKEN; + const isInsecure = ui.state & STATE_IS_INSECURE; + + let actualState; + if (isSecure && !(isBroken || isInsecure)) { + actualState = "secure"; + } else if (isBroken && !(isSecure || isInsecure)) { + actualState = "broken"; + } else if (isInsecure && !(isSecure || isBroken)) { + actualState = "insecure"; + } else { + actualState = "unknown"; + } + + is( + expectedState, + actualState, + `Expected state is ${expectedState} and actual state is ${actualState}` + ); +} + +add_task(async function testDefault({ Security }) { + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + const loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present" + ); + isSecurityState(gBrowser, "insecure"); + } +}); + +add_task(async function testIgnore({ client }) { + const { Security } = client; + info("Enable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: true }); + + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + is( + getConnectionState(), + "secure-cert-user-overridden", + "Security certificate was overridden by user" + ); + isSecurityState(gBrowser, "secure"); + } +}); + +add_task(async function testUnignore({ client }) { + const { Security } = client; + info("Disable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: false }); + + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + const loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present" + ); + isSecurityState(gBrowser, "insecure"); + } +}); + +// smoke test for unignored -> ignored -> unignored +add_task(async function testToggle({ client }) { + const { Security } = client; + let loaded; + + info("Enable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: true }); + + info(`Navigating to ${UNTRUSTED} having set the override`); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, UNTRUSTED); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + is( + getConnectionState(), + "secure-cert-user-overridden", + "Security certificate was overridden by user" + ); + isSecurityState(gBrowser, "secure"); + + info("Disable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: false }); + + info(`Navigating to ${UNTRUSTED} having unset the override`); + loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, UNTRUSTED); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present by default" + ); + isSecurityState(gBrowser, "insecure"); +}); diff --git a/remote/cdp/test/browser/security/head.js b/remote/cdp/test/browser/security/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/security/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/systemInfo/browser.toml b/remote/cdp/test/browser/systemInfo/browser.toml new file mode 100644 index 0000000000..8d814c91ee --- /dev/null +++ b/remote/cdp/test/browser/systemInfo/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", +] + +["browser_getProcessInfo.js"] diff --git a/remote/cdp/test/browser/systemInfo/browser_getProcessInfo.js b/remote/cdp/test/browser/systemInfo/browser_getProcessInfo.js new file mode 100644 index 0000000000..fb491e248f --- /dev/null +++ b/remote/cdp/test/browser/systemInfo/browser_getProcessInfo.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function getProcessInfoDetails({ client }) { + const { SystemInfo } = client; + + const processInfo = await SystemInfo.getProcessInfo(); + assertProcesses(processInfo); + }, + { createTab: false } +); + +add_task( + async function getProcessInfoMultipleTabs({ client }) { + const { SystemInfo, Target } = client; + + const { newTab: newTab1 } = await openTab(Target); + const { newTab: newTab2 } = await openTab(Target); + const { newTab: newTab3 } = await openTab(Target); + const { newTab: newTab4 } = await openTab(Target); + + const processInfo = await SystemInfo.getProcessInfo(); + assertProcesses(processInfo, [newTab1, newTab2, newTab3, newTab4]); + }, + { createTab: false } +); + +add_task( + async function getProcessInfoMultipleWindows({ client }) { + const { SystemInfo, Target } = client; + + const { newWindow: newWindow1 } = await openWindow(Target); + const { newWindow: newWindow2 } = await openWindow(Target); + + const processInfo = await SystemInfo.getProcessInfo(); + assertProcesses(processInfo, [ + ...newWindow1.gBrowser.tabs, + ...newWindow2.gBrowser.tabs, + ]); + + await BrowserTestUtils.closeWindow(newWindow1); + await BrowserTestUtils.closeWindow(newWindow2); + }, + { createTab: false } +); + +function assertProcesses(processInfo, tabs) { + ok(Array.isArray(processInfo), "Process info is an array"); + + for (const info of processInfo) { + ok(typeof info.id === "number", "Info has a numeric id"); + ok(typeof info.type === "string", "Info has a string type"); + ok(typeof info.cpuTime === "number", "Info has a numeric cpuTime"); + } + + const getByType = type => processInfo.filter(info => info.type === type); + + is( + getByType("browser").length, + 1, + "Got expected amount of browser processes" + ); + ok(!!getByType("renderer").length, "Got at least one renderer process"); + + if (tabs) { + const rendererPids = new Set( + processInfo.filter(info => info.type === "renderer").map(info => info.id) + ); + + for (const tab of tabs) { + const pid = tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid; + ok(rendererPids.has(pid), `Found process info for pid (${pid})`); + } + } +} diff --git a/remote/cdp/test/browser/systemInfo/head.js b/remote/cdp/test/browser/systemInfo/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/systemInfo/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/browser/target/browser.toml b/remote/cdp/test/browser/target/browser.toml new file mode 100644 index 0000000000..2fe20cacf1 --- /dev/null +++ b/remote/cdp/test/browser/target/browser.toml @@ -0,0 +1,44 @@ +[DEFAULT] +tags = "cdp" +subsuite = "remote" +args = [ + "--remote-debugging-port", + "--remote-allow-origins=null", +] +prefs = [ # Bug 1600054: Make CDP Fission compatible + "fission.bfcacheInParent=false", + "fission.webContentIsolationStrategy=0", +] +skip-if = [ + "display == 'wayland'" # Bug 1861933: Timestamp unreliable due to worker setup +] +support-files = [ + "!/remote/cdp/test/browser/chrome-remote-interface.js", + "!/remote/cdp/test/browser/head.js", + "head.js", + "doc_test.html", +] + +["browser_activateTarget.js"] + +["browser_attachToTarget.js"] + +["browser_attachedToTarget.js"] +https_first_disabled = true + +["browser_browserContext.js"] + +["browser_closeTarget.js"] + +["browser_createTarget.js"] + +["browser_getTargets.js"] +https_first_disabled = true + +["browser_sendMessageToTarget.js"] + +["browser_setDiscoverTargets.js"] + +["browser_targetCreated.js"] + +["browser_targetDestroyed.js"] diff --git a/remote/cdp/test/browser/target/browser_activateTarget.js b/remote/cdp/test/browser/target/browser_activateTarget.js new file mode 100644 index 0000000000..03c5a96e07 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_activateTarget.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.activateTarget(), + err => err.message.includes(`Unable to find target with id`), + "activateTarget raised error without an argument" + ); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.activateTarget({ targetId: "-1" }), + err => err.message.includes(`Unable to find target with id`), + "activateTarget raised error with unkown target id" + ); +}); + +add_task(async function selectTabInOtherWindow({ client, tab }) { + const { Target, target } = client; + + const currentTargetId = target.id; + const targets = await getDiscoveredTargets(Target); + const filtered_targets = targets.filter(target => { + return target.targetId == currentTargetId; + }); + is(filtered_targets.length, 1, "The current target has been found"); + const initialTarget = filtered_targets[0]; + + is(tab.ownerGlobal, getFocusedNavigator(), "Initial window is focused"); + + // open some more tabs in the initial window + await openTab(Target); + await openTab(Target); + const lastTabFirstWindow = await openTab(Target); + is( + gBrowser.selectedTab, + lastTabFirstWindow.newTab, + "Last openend tab in initial window is the selected tab" + ); + + const { newWindow } = await openWindow(Target); + + const lastTabSecondWindow = await openTab(Target); + is( + gBrowser.selectedTab, + lastTabSecondWindow.newTab, + "Last openend tab in new window is the selected tab" + ); + + try { + is(newWindow, getFocusedNavigator(), "The new window is focused"); + await Target.activateTarget({ + targetId: initialTarget.targetId, + }); + is( + tab.ownerGlobal, + getFocusedNavigator(), + "Initial window is focused again" + ); + is(gBrowser.selectedTab, tab, "Selected tab is the initial tab again"); + } finally { + await BrowserTestUtils.closeWindow(newWindow); + } +}); + +function getFocusedNavigator() { + return Services.wm.getMostRecentWindow("navigator:browser"); +} diff --git a/remote/cdp/test/browser/target/browser_attachToTarget.js b/remote/cdp/test/browser/target/browser_attachToTarget.js new file mode 100644 index 0000000000..944d7a9984 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_attachToTarget.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.attachToTarget(), + err => err.message.includes(`Unable to find target with id`), + "attachToTarget raised error without an argument" + ); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.attachToTarget({ targetId: "-1" }), + err => err.message.includes(`Unable to find target with id`), + "attachToTarget raised error with unkown target id" + ); +}); + +add_task( + async function attachPageTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + ok(!targetInfo.attached, "New target is not attached"); + + info("Attach new target"); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + + is( + typeof sessionId, + "string", + "attachToTarget returns the session id as string" + ); + + const { targetInfos } = await Target.getTargets(); + const listedTarget = targetInfos.find( + info => info.targetId === targetInfo.targetId + ); + + ok(listedTarget.attached, "New target is attached"); + }, + { createTab: false } +); diff --git a/remote/cdp/test/browser/target/browser_attachedToTarget.js b/remote/cdp/test/browser/target/browser_attachedToTarget.js new file mode 100644 index 0000000000..4f25868f2a --- /dev/null +++ b/remote/cdp/test/browser/target/browser_attachedToTarget.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_TEST = + "https://example.com/browser/remote/cdp/test/browser/target/doc_test.html"; + +add_task( + async function attachedPageTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + ok( + !targetInfo.attached, + "Got expected target attached status before attaching" + ); + + await loadURL(PAGE_TEST); + + info("Attach new page target"); + const attachedToTarget = Target.attachedToTarget(); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + const { + targetInfo: eventTargetInfo, + sessionId: eventSessionId, + waitingForDebugger: eventWaitingForDebugger, + } = await attachedToTarget; + + is(eventTargetInfo.targetId, targetInfo.targetId, "Got expected target id"); + is(eventTargetInfo.type, "page", "Got expected target type"); + is(eventTargetInfo.title, "Test Page", "Got expected target title"); + is(eventTargetInfo.url, PAGE_TEST, "Got expected target URL"); + ok(eventTargetInfo.attached, "Got expected target attached status"); + + is( + eventSessionId, + sessionId, + "attachedToTarget and attachToTarget refer to the same session id" + ); + is( + typeof eventWaitingForDebugger, + "boolean", + "Got expected type for waitingForDebugger" + ); + }, + { createTab: false } +); diff --git a/remote/cdp/test/browser/target/browser_browserContext.js b/remote/cdp/test/browser/target/browser_browserContext.js new file mode 100644 index 0000000000..98c2ccee2e --- /dev/null +++ b/remote/cdp/test/browser/target/browser_browserContext.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function ({ CDP }) { + // Connect to the server + const { webSocketDebuggerUrl } = await CDP.Version(); + const client = await CDP({ target: webSocketDebuggerUrl }); + info("CDP client has been instantiated"); + + const { Target } = client; + await getDiscoveredTargets(Target); + + // Test if Target.getBrowserContexts is empty before creatinga ny + const { browserContextIds: browserContextIdsBefore } = + await Target.getBrowserContexts(); + + is( + browserContextIdsBefore.length, + 0, + "No browser context is open by default" + ); + + const { browserContextId } = await Target.createBrowserContext(); + + // Test if Target.getBrowserContexts includes the context we just created + const { browserContextIds } = await Target.getBrowserContexts(); + + is(browserContextIds.length, 1, "Got expected length of browser contexts"); + is( + browserContextIds[0], + browserContextId, + "Got expected browser context id from getBrowserContexts" + ); + + const targetCreated = Target.targetCreated(); + const { targetId } = await Target.createTarget({ + url: "about:blank", + browserContextId, + }); + ok(!!targetId, "Target.createTarget returns a non-empty target id"); + + const { targetInfo } = await targetCreated; + is( + targetId, + targetInfo.targetId, + "targetCreated refers to the same target id" + ); + is( + browserContextId, + targetInfo.browserContextId, + "targetCreated refers to the same browser context" + ); + is(targetInfo.type, "page", "The target is a page"); + + // Releasing the browser context is going to remove the tab opened when calling createTarget + await Target.disposeBrowserContext({ browserContextId }); + + // Test if Target.getBrowserContexts now is empty + const { browserContextIds: browserContextIdsAfter } = + await Target.getBrowserContexts(); + + is( + browserContextIdsAfter.length, + 0, + "After closing all browser contexts none is available anymore" + ); + + await client.close(); + info("The client is closed"); +}); diff --git a/remote/cdp/test/browser/target/browser_closeTarget.js b/remote/cdp/test/browser/target/browser_closeTarget.js new file mode 100644 index 0000000000..694994148b --- /dev/null +++ b/remote/cdp/test/browser/target/browser_closeTarget.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.closeTarget(), + err => err.message.includes(`Unable to find target with id `), + "closeTarget raised error without an argument" + ); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + + await Assert.rejects( + Target.closeTarget({ targetId: "-1" }), + err => err.message.includes(`Unable to find target with id `), + "closeTarget raised error with unkown target id" + ); +}); + +add_task(async function triggersTargetDestroyed({ client, tab }) { + const { Target } = client; + const { targetInfo, newTab } = await openTab(Target); + + const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose"); + const targetDestroyed = Target.targetDestroyed(); + + info("Closing the target"); + await Target.closeTarget({ targetId: targetInfo.targetId }); + + await tabClosed; + info("Tab was closed"); + + await targetDestroyed; + info("Received the Target.targetDestroyed event"); +}); diff --git a/remote/cdp/test/browser/target/browser_createTarget.js b/remote/cdp/test/browser/target/browser_createTarget.js new file mode 100644 index 0000000000..35fbe84b43 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_createTarget.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_TEST = + "https://example.com/browser/remote/cdp/test/browser/target/doc_test.html"; + +add_task( + async function raisesWithoutArguments({ client }) { + const { Target } = client; + + await Assert.rejects( + Target.createTarget(), + err => err.message.includes("url: string value expected"), + "createTarget raised error without a URL" + ); + }, + { createTab: false } +); + +add_task( + async function raisesWithInvalidUrlType({ client }) { + const { Target } = client; + + for (const url of [null, true, 1, [], {}]) { + info(`Checking url with invalid value: ${url}`); + + await Assert.rejects( + Target.createTarget({ + url, + }), + /url: string value expected/, + `URL fails for invalid type: ${url}` + ); + } + }, + { createTab: false } +); + +add_task( + async function invalidUrlDefaults({ client }) { + const { Target } = client; + const expectedUrl = "about:blank"; + + for (const url of ["", "example.com", "https://example[.com", "https:"]) { + // Here we cannot wait for browserLoaded, because the tab might already + // be on about:blank when `createTarget` resolves. + const onNewTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:blank", + true + ); + const { targetId } = await Target.createTarget({ url }); + is(typeof targetId, "string", "Got expected type for target id"); + + // Wait for the load to be done before checking the URL. + const tab = await onNewTabLoaded; + const browser = tab.linkedBrowser; + is(browser.currentURI.spec, expectedUrl, "Expected URL loaded"); + } + }, + { createTab: false } +); + +add_task( + async function opensTabWithCorrectInfo({ client }) { + const { Target } = client; + + const url = PAGE_TEST; + const onNewTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, url, true); + const { targetId } = await Target.createTarget({ url }); + + is(typeof targetId, "string", "Got expected type for target id"); + + const tab = await onNewTabLoaded; + const browser = tab.linkedBrowser; + is(browser.currentURI.spec, url, "Expected URL loaded"); + + const { targetInfos } = await Target.getTargets(); + const targetInfo = targetInfos.find(info => info.targetId === targetId); + ok(!!targetInfo, "Found target info with the same target id"); + is(targetInfo.url, url, "Target info refers to the same target URL"); + is( + targetInfo.type, + "page", + "Target info refers to the same target as page type" + ); + ok( + !targetInfo.attached, + "Target info refers to the same target as not attached" + ); + }, + { createTab: false } +); diff --git a/remote/cdp/test/browser/target/browser_getTargets.js b/remote/cdp/test/browser/target/browser_getTargets.js new file mode 100644 index 0000000000..0d56661fc2 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_getTargets.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_TEST = + "https://example.com/browser/remote/cdp/test/browser/target/doc_test.html"; + +add_task( + async function getTargetsDetails({ client }) { + const { Target, target } = client; + + await loadURL(PAGE_TEST); + + const { targetInfos } = await Target.getTargets(); + + Assert.equal(targetInfos.length, 1, "Got expected amount of targets"); + + const targetInfo = targetInfos[0]; + is(targetInfo.id, target.id, "Got expected target id"); + is(targetInfo.type, "page", "Got expected target type"); + is(targetInfo.title, "Test Page", "Got expected target title"); + is(targetInfo.url, PAGE_TEST, "Got expected target URL"); + ok(targetInfo.attached, "Got expected target attached status"); + }, + { createTab: false } +); + +add_task( + async function getTargetsCount({ client }) { + const { Target, target } = client; + const { targetInfo: newTabTargetInfo } = await openTab(Target); + + await loadURL(PAGE_TEST); + + const { targetInfos } = await Target.getTargets(); + + Assert.equal(targetInfos.length, 2, "Got expected amount of targets"); + const targetIds = targetInfos.map(info => info.id); + ok(targetIds.includes(target.id), "Got expected original target id"); + ok(targetIds.includes(newTabTargetInfo.id), "Got expected new target id"); + }, + { createTab: false } +); + +add_task( + async function getTargetsAttached({ client }) { + const { Target } = client; + await openTab(Target); + + await loadURL(PAGE_TEST); + + const { targetInfos } = await Target.getTargets(); + + ok(targetInfos[0].attached, "Current target is attached"); + ok(!targetInfos[1].attached, "New tab target is detached"); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterAllBlank({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Blank/all filter so all targets are returned, including main process + const { targetInfos } = await Target.getTargets({ + filter: [{}], + }); + + is( + targetInfos.length, + 2, + "Got expected amount of targets with all (blank) filter" + ); + + const pageTarget = targetInfos.find(info => info.type === "page"); + ok(!!pageTarget, "Found page target in targets with all (blank) filter"); + + const mainProcessTarget = targetInfos.find(info => info.type === "browser"); + ok( + !!mainProcessTarget, + "Found main process target in targets with all (blank) filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterAllExplicit({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Blank/all filter so all targets are returned, including main process + const { targetInfos } = await Target.getTargets({ + filter: [{ type: "browser" }, { type: "page" }], + }); + + is( + targetInfos.length, + 2, + "Got expected amount of targets with all (explicit) filter" + ); + + const pageTarget = targetInfos.find(info => info.type === "page"); + ok(!!pageTarget, "Found page target in targets with all (explicit) filter"); + + const mainProcessTarget = targetInfos.find(info => info.type === "browser"); + ok( + !!mainProcessTarget, + "Found main process target in targets with all (explicit) filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterPage({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Filter so only page targets are returned + // This returns same as default but pass our own custom filter to ensure + const { targetInfos } = await Target.getTargets({ + filter: [{ type: "page" }], + }); + + is( + targetInfos.length, + 1, + "Got expected amount of targets with page filter" + ); + is( + targetInfos[0].type, + "page", + "Got expected type 'page' of target from page filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterBrowser({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Filter so only main process target is returned + const { targetInfos } = await Target.getTargets({ + filter: [{ type: "browser" }], + }); + + is( + targetInfos.length, + 1, + "Got expected amount of targets with browser filter" + ); + is( + targetInfos[0].type, + "browser", + "Got expected type 'browser' of target from browser filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterExcludePage({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Filter so page targets are excluded (so only main process target is returned) + // A blank object ({}) means include everything else + const { targetInfos } = await Target.getTargets({ + filter: [{ type: "page", exclude: true }, {}], + }); + + is( + targetInfos.length, + 1, + "Got expected amount of targets with exclude page filter" + ); + is( + targetInfos[0].type, + "browser", + "Got expected type 'browser' of target from exclude page filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterExcludeBrowserIncludePage({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + // Filter so main process is excluded and only page types are returned explicitly + const { targetInfos } = await Target.getTargets({ + filter: [{ type: "browser", exclude: true }, { type: "page" }], + }); + + is( + targetInfos.length, + 1, + "Got expected amount of targets with exclude browser include page filter" + ); + is( + targetInfos[0].type, + "page", + "Got expected type 'page' of target from exclude browser include page filter" + ); + }, + { createTab: false } +); + +add_task( + async function getTargets_filterInvalid({ client }) { + const { Target } = client; + + for (const filter of [null, true, 1, "foo", {}]) { + info(`Checking filter with invalid value: ${filter}`); + + await Assert.rejects( + Target.getTargets({ + filter, + }), + /filter: array value expected/, + `Filter fails for invalid type: ${filter}` + ); + } + + for (const filterEntry of [null, true, 1, "foo", []]) { + info(`Checking filter entry with invalid value: ${filterEntry}`); + + await Assert.rejects( + Target.getTargets({ + filter: [filterEntry], + }), + /filter: object values expected in array/, + `Filter entry fails for invalid type: ${filterEntry}` + ); + } + + for (const type of [null, true, 1, [], {}]) { + info(`Checking filter entry with type as invalid value: ${type}`); + + await Assert.rejects( + Target.getTargets({ + filter: [{ type }], + }), + /filter: type: string value expected/, + `Filter entry type fails for invalid type: ${type}` + ); + } + + for (const exclude of [null, 1, "foo", [], {}]) { + info(`Checking filter entry with exclude as invalid value: ${exclude}`); + + await Assert.rejects( + Target.getTargets({ + filter: [{ exclude }], + }), + /filter: exclude: boolean value expected/, + `Filter entry exclude for invalid type: ${exclude}` + ); + } + }, + { createTab: false } +); diff --git a/remote/cdp/test/browser/target/browser_sendMessageToTarget.js b/remote/cdp/test/browser/target/browser_sendMessageToTarget.js new file mode 100644 index 0000000000..b440066178 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_sendMessageToTarget.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function sendToAttachedTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + const attachedToTarget = Target.attachedToTarget(); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + await attachedToTarget; + info("Target attached"); + + const id = 1; + const message = JSON.stringify({ + id, + method: "Page.navigate", + params: { + url: toDataURL("new-page"), + }, + }); + + info("Calling Target.sendMessageToTarget"); + const onResponse = Target.receivedMessageFromTarget(); + await Target.sendMessageToTarget({ sessionId, message }); + const response = await onResponse; + info("Message from target received"); + + ok(!!response, "The response is not empty"); + is(response.sessionId, sessionId, "The response is from the same session"); + + const responseMessage = JSON.parse(response.message); + is(responseMessage.id, id, "The response is from the same session"); + ok( + !!responseMessage.result.frameId, + "received the `frameId` out of `Page.navigate` request" + ); +}); diff --git a/remote/cdp/test/browser/target/browser_setDiscoverTargets.js b/remote/cdp/test/browser/target/browser_setDiscoverTargets.js new file mode 100644 index 0000000000..5a16115a4a --- /dev/null +++ b/remote/cdp/test/browser/target/browser_setDiscoverTargets.js @@ -0,0 +1,258 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// These tests are a near copy of the tests for Target.getTargets, but using +// the `setDiscoverTargets` method and `targetCreated` events instead. +// Calling `setDiscoverTargets` with `discover: true` will dispatch a +// `targetCreated` event for all already opened tabs and NOT the browser target +// with the default filter. + +const PAGE_TEST = + "https://example.com/browser/remote/cdp/test/browser/target/doc_test.html"; + +add_task( + async function discoverInvalidTypes({ client }) { + const { Target } = client; + + for (const discover of [null, undefined, 1, "foo", [], {}]) { + info(`Checking discover with invalid value: ${discover}`); + + await Assert.rejects( + Target.setDiscoverTargets({ discover }), + /discover: boolean value expected/, + `Discover fails for invalid type: ${discover}` + ); + } + }, + { createTab: false } +); + +add_task( + async function filterInvalid({ client }) { + const { Target } = client; + + for (const filter of [null, true, 1, "foo", {}]) { + info(`Checking filter with invalid value: ${filter}`); + + await Assert.rejects( + Target.setDiscoverTargets({ discover: true, filter }), + /filter: array value expected/, + `Filter fails for invalid type: ${filter}` + ); + } + + for (const filterEntry of [null, undefined, true, 1, "foo", []]) { + info(`Checking filter entry with invalid value: ${filterEntry}`); + + await Assert.rejects( + Target.setDiscoverTargets({ + discover: true, + filter: [filterEntry], + }), + /filter: object values expected in array/, + `Filter entry fails for invalid type: ${filterEntry}` + ); + } + + for (const type of [null, true, 1, [], {}]) { + info(`Checking filter entry with type as invalid value: ${type}`); + + await Assert.rejects( + Target.setDiscoverTargets({ + discover: true, + filter: [{ type }], + }), + /filter: type: string value expected/, + `Filter entry type fails for invalid type: ${type}` + ); + } + + for (const exclude of [null, 1, "foo", [], {}]) { + info(`Checking filter entry with exclude as invalid value: ${exclude}`); + + await Assert.rejects( + Target.setDiscoverTargets({ + discover: true, + filter: [{ exclude }], + }), + /filter: exclude: boolean value expected/, + `Filter entry exclude for invalid type: ${exclude}` + ); + } + }, + { createTab: false } +); + +add_task( + async function noFilterWithDiscoverFalse({ client }) { + const { Target } = client; + + // Check filter cannot be given with discover: false + + await Assert.rejects( + Target.setDiscoverTargets({ + discover: false, + filter: [{}], + }), + /filter: should not be present when discover is false/, + `Error throw when given filter with discover false` + ); + }, + { createTab: false } +); + +add_task( + async function noTargetsWithDiscoverFalse({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + const targets = await getDiscoveredTargets(Target, { discover: false }); + is(targets.length, 0, "Got 0 targets with discover false"); + }, + { createTab: false } +); + +add_task( + async function noEventsWithDiscoverFalse({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + const targets = []; + const unsubscribe = Target.targetCreated(target => { + targets.push(target.targetInfo); + }); + + await Target.setDiscoverTargets({ + discover: false, + }); + + // Cannot use openTab() helper as it relies on the event + await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Wait 1s for the event to possibly dispatch + await timeoutPromise(1000); + + unsubscribe(); + is(targets.length, 0, "Got 0 target created events with discover false"); + }, + { createTab: false } +); + +add_task( + async function targetInfoValues({ client }) { + const { Target, target } = client; + + await loadURL(PAGE_TEST); + + const targets = await getDiscoveredTargets(Target); + + Assert.equal(targets.length, 1, "Got expected amount of targets"); + + const targetInfo = targets[0]; + is(targetInfo.id, target.id, "Got expected target id"); + is(targetInfo.type, "page", "Got expected target type"); + is(targetInfo.title, "Test Page", "Got expected target title"); + is(targetInfo.url, PAGE_TEST, "Got expected target URL"); + }, + { createTab: false } +); + +add_task( + async function discoverEnabledAndMultipleTabs({ client }) { + const { Target, target } = client; + const { targetInfo: newTabTargetInfo } = await openTab(Target); + + await loadURL(PAGE_TEST); + + const targets = await getDiscoveredTargets(Target); + + Assert.equal(targets.length, 2, "Got expected amount of targets"); + const targetIds = targets.map(info => info.id); + ok(targetIds.includes(target.id), "Got expected original target id"); + ok(targetIds.includes(newTabTargetInfo.id), "Got expected new target id"); + }, + { createTab: false } +); + +add_task( + async function allFilters({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + for (const filter of [[{}], [{ type: "browser" }, { type: "page" }]]) { + // Blank/all filter so all targets are returned, including main process + const targets = await getDiscoveredTargets(Target, { filter }); + + is(targets.length, 2, "Got expected amount of targets with all filter"); + + const pageTarget = targets.find(info => info.type === "page"); + ok(!!pageTarget, "Found page target in targets with all filter"); + + const mainProcessTarget = targets.find(info => info.type === "browser"); + ok( + !!mainProcessTarget, + "Found main process target in targets with all filter" + ); + } + }, + { createTab: false } +); + +add_task( + async function pageOnlyFilters({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + for (const filter of [ + [{ type: "page" }], + [{ type: "browser", exclude: true }, { type: "page" }], + ]) { + // Filter so only page targets are returned + // This returns same as default but pass our own custom filter to ensure + // these filters still return what they should + const targets = await getDiscoveredTargets(Target, { filter }); + + is(targets.length, 1, "Got expected amount of targets with page filter"); + is( + targets[0].type, + "page", + "Got expected type 'page' of target from page filter" + ); + } + }, + { createTab: false } +); + +add_task( + async function browserOnlyFilters({ client }) { + const { Target } = client; + + await loadURL(PAGE_TEST); + + for (const filter of [ + [{ type: "browser" }], + [{ type: "page", exclude: true }, {}], + ]) { + // Filter so only main process target is returned + const targets = await getDiscoveredTargets(Target, { filter }); + + is( + targets.length, + 1, + "Got expected amount of targets with browser only filter" + ); + is( + targets[0].type, + "browser", + "Got expected type 'browser' of target from browser only filter" + ); + } + }, + { createTab: false } +); diff --git a/remote/cdp/test/browser/target/browser_targetCreated.js b/remote/cdp/test/browser/target/browser_targetCreated.js new file mode 100644 index 0000000000..bfb7287f87 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_targetCreated.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function eventFiredWhenTabIsCreated({ client }) { + const { Target } = client; + + const targetCreated = Target.targetCreated(); + await BrowserTestUtils.openNewForegroundTab(gBrowser); + const { targetInfo } = await targetCreated; + + is(typeof targetInfo.targetId, "string", "Got expected type for target id"); + is(targetInfo.type, "page", "Got expected target type"); + is(targetInfo.title, "", "Got expected target title"); + is(targetInfo.url, "about:blank", "Got expected target URL"); + is(targetInfo.attached, false, "Got expected attached status"); +}); diff --git a/remote/cdp/test/browser/target/browser_targetDestroyed.js b/remote/cdp/test/browser/target/browser_targetDestroyed.js new file mode 100644 index 0000000000..2ad657b135 --- /dev/null +++ b/remote/cdp/test/browser/target/browser_targetDestroyed.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function eventFiredWhenTabIsClosed({ client, tab }) { + const { Target } = client; + const { newTab } = await openTab(Target); + + const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose"); + const targetDestroyed = Target.targetDestroyed(); + + info("Closing the tab"); + BrowserTestUtils.removeTab(newTab); + + await tabClosed; + info("Tab was closed"); + + await targetDestroyed; + info("Received the Target.targetDestroyed event"); +}); diff --git a/remote/cdp/test/browser/target/doc_test.html b/remote/cdp/test/browser/target/doc_test.html new file mode 100644 index 0000000000..14d377f07a --- /dev/null +++ b/remote/cdp/test/browser/target/doc_test.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test Page</title> +</head> +<body> +</body> +</html> diff --git a/remote/cdp/test/browser/target/head.js b/remote/cdp/test/browser/target/head.js new file mode 100644 index 0000000000..1a1c90fbf6 --- /dev/null +++ b/remote/cdp/test/browser/target/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js", + this +); diff --git a/remote/cdp/test/xpcshell/test_CDPConnection.js b/remote/cdp/test/xpcshell/test_CDPConnection.js new file mode 100644 index 0000000000..848155633f --- /dev/null +++ b/remote/cdp/test/xpcshell/test_CDPConnection.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { splitMethod } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/CDPConnection.sys.mjs" +); + +add_task(function test_Connection_splitMethod() { + for (const t of [42, null, true, {}, [], undefined]) { + Assert.throws(() => splitMethod(t), /TypeError/, `${typeof t} throws`); + } + for (const s of ["", ".", "foo.", ".bar", "foo.bar.baz"]) { + Assert.throws( + () => splitMethod(s), + /Invalid method format: '.*'/, + `"${s}" throws` + ); + } + deepEqual(splitMethod("foo.bar"), { + domain: "foo", + command: "bar", + }); +}); diff --git a/remote/cdp/test/xpcshell/test_DomainCache.js b/remote/cdp/test/xpcshell/test_DomainCache.js new file mode 100644 index 0000000000..9ea53ccdd9 --- /dev/null +++ b/remote/cdp/test/xpcshell/test_DomainCache.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Domain } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/domains/Domain.sys.mjs" +); +const { DomainCache } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/domains/DomainCache.sys.mjs" +); + +class MockSession { + onEvent() {} +} + +const noopSession = new MockSession(); + +add_task(function test_DomainCache_constructor() { + new DomainCache(noopSession, {}); +}); + +add_task(function test_DomainCache_domainSupportsMethod() { + const modules = { + Foo: class extends Domain { + bar() {} + }, + }; + const domains = new DomainCache(noopSession, modules); + + ok(domains.domainSupportsMethod("Foo", "bar")); + ok(!domains.domainSupportsMethod("Foo", "baz")); + ok(!domains.domainSupportsMethod("foo", "bar")); +}); + +add_task(function test_DomainCache_get_invalidModule() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: undefined }); + domains.get("Foo"); + }, /UnknownMethodError/); +}); + +add_task(function test_DomainCache_get_missingConstructor() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: {} }); + domains.get("Foo"); + }, /TypeError/); +}); + +add_task(function test_DomainCache_get_superClassNotDomain() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: class {} }); + domains.get("Foo"); + }, /TypeError/); +}); + +add_task(function test_DomainCache_get_constructs() { + let eventFired; + class Session { + onEvent(event) { + eventFired = event; + } + } + + let constructed = false; + class Foo extends Domain { + constructor() { + super(); + constructed = true; + } + } + + const session = new Session(); + const domains = new DomainCache(session, { Foo }); + + const foo = domains.get("Foo"); + ok(constructed); + ok(foo instanceof Foo); + + const event = {}; + foo.emit(event); + equal(event, eventFired); +}); + +add_task(function test_DomainCache_size() { + class Foo extends Domain {} + const domains = new DomainCache(noopSession, { Foo }); + + equal(domains.size, 0); + domains.get("Foo"); + equal(domains.size, 1); +}); + +add_task(function test_DomainCache_clear() { + let dtorCalled = false; + class Foo extends Domain { + destructor() { + dtorCalled = true; + } + } + + const domains = new DomainCache(noopSession, { Foo }); + + equal(domains.size, 0); + domains.get("Foo"); + equal(domains.size, 1); + + domains.clear(); + equal(domains.size, 0); + ok(dtorCalled); +}); diff --git a/remote/cdp/test/xpcshell/test_Error.js b/remote/cdp/test/xpcshell/test_Error.js new file mode 100644 index 0000000000..1be7be18ed --- /dev/null +++ b/remote/cdp/test/xpcshell/test_Error.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* eslint-disable no-tabs */ + +const { RemoteAgentError, UnknownMethodError, UnsupportedError } = + ChromeUtils.importESModule("chrome://remote/content/cdp/Error.sys.mjs"); + +add_task(function test_RemoteAgentError_ctor() { + const e1 = new RemoteAgentError(); + equal(e1.name, "RemoteAgentError"); + equal(e1.message, ""); + equal(e1.cause, e1.message); + + const e2 = new RemoteAgentError("message"); + equal(e2.message, "message"); + equal(e2.cause, e2.message); + + const e3 = new RemoteAgentError("message", "cause"); + equal(e3.message, "message"); + equal(e3.cause, "cause"); +}); + +add_task(function test_RemoteAgentError_notify() { + // nothing much we can test, except test that it doesn't throw + new RemoteAgentError().notify(); +}); + +add_task(function test_RemoteAgentError_toString() { + const e = new RemoteAgentError("message"); + equal(e.toString(), RemoteAgentError.format(e)); + equal( + e.toString({ stack: true }), + RemoteAgentError.format(e, { stack: true }) + ); +}); + +add_task(function test_RemoteAgentError_format() { + const { format } = RemoteAgentError; + + equal(format({ name: "HippoError" }), "HippoError"); + equal(format({ name: "HorseError", message: "neigh" }), "HorseError: neigh"); + + const dog = { + name: "DogError", + message: "woof", + stack: " one\ntwo\nthree ", + }; + equal(format(dog), "DogError: woof"); + equal( + format(dog, { stack: true }), + `DogError: woof: + one + two + three` + ); + + const cat = { + name: "CatError", + message: "meow", + stack: "four\nfive\nsix", + cause: dog, + }; + equal(format(cat), "CatError: meow"); + equal( + format(cat, { stack: true }), + `CatError: meow: + four + five + six +caused by: DogError: woof: + one + two + three` + ); +}); + +add_task(function test_RemoteAgentError_fromJSON() { + const cdpErr = { + message: `TypeError: foo: + bar + baz`, + }; + const err = RemoteAgentError.fromJSON(cdpErr); + + equal(err.message, "TypeError: foo"); + equal(err.stack, "bar\nbaz"); + equal(err.cause, null); +}); + +add_task(function test_UnsupportedError() { + ok(new UnsupportedError() instanceof RemoteAgentError); +}); + +add_task(function test_UnknownMethodError() { + ok(new UnknownMethodError() instanceof RemoteAgentError); + ok(new UnknownMethodError("domain").message.endsWith("domain")); + ok( + new UnknownMethodError("domain", "command").message.endsWith( + "domain.command" + ) + ); +}); diff --git a/remote/cdp/test/xpcshell/test_Session.js b/remote/cdp/test/xpcshell/test_Session.js new file mode 100644 index 0000000000..3cd8adc7e3 --- /dev/null +++ b/remote/cdp/test/xpcshell/test_Session.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Session } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/sessions/Session.sys.mjs" +); + +const connection = { + registerSession: () => {}, + transport: { + on: () => {}, + }, +}; + +class MockTarget { + constructor() {} + + get browsingContext() { + return { id: 42 }; + } + + get mm() { + return { + addMessageListener() {}, + removeMessageListener() {}, + loadFrameScript() {}, + sendAsyncMessage() {}, + }; + } +} + +add_task(function test_Session_destructor() { + const session = new Session(connection, new MockTarget()); + session.domains.get("Browser"); + equal(session.domains.size, 1); + session.destructor(); + equal(session.domains.size, 0); +}); diff --git a/remote/cdp/test/xpcshell/test_StreamRegistry.js b/remote/cdp/test/xpcshell/test_StreamRegistry.js new file mode 100644 index 0000000000..329f33d275 --- /dev/null +++ b/remote/cdp/test/xpcshell/test_StreamRegistry.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Stream, StreamRegistry } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/StreamRegistry.sys.mjs" +); + +add_task(function test_constructor() { + const registry = new StreamRegistry(); + equal(registry.streams.size, 0); +}); + +add_task(async function test_destructor() { + const registry = new StreamRegistry(); + + const stream1 = await createFileStream("foo bar"); + const stream2 = await createFileStream("foo bar"); + + registry.add(stream1); + registry.add(stream2); + + equal(registry.streams.size, 2); + + await registry.destructor(); + equal(registry.streams.size, 0); + + ok(!(await IOUtils.exists(stream1.path)), "temporary file has been removed"); + ok(!(await IOUtils.exists(stream2.path)), "temporary file has been removed"); +}); + +add_task(async function test_addValidStreamType() { + const registry = new StreamRegistry(); + + const stream = await createFileStream("foo bar"); + const handle = registry.add(stream); + + equal(registry.streams.size, 1, "A single stream has been added"); + equal(typeof handle, "string", "Handle is of type string"); + ok(registry.streams.has(handle), "Handle has been found"); + + const rv = registry.streams.get(handle); + equal(rv, stream, "Expected stream found"); +}); + +add_task(async function test_addCreatesDifferentHandles() { + const registry = new StreamRegistry(); + const stream = await createFileStream("foo bar"); + + const handle1 = registry.add(stream); + equal(registry.streams.size, 1, "A single stream has been added"); + equal(typeof handle1, "string", "Handle is of type string"); + ok(registry.streams.has(handle1), "Handle has been found"); + equal(registry.streams.get(handle1), stream, "Expected stream found"); + + const handle2 = registry.add(stream); + equal(registry.streams.size, 2, "A single stream has been added"); + equal(typeof handle2, "string", "Handle is of type string"); + ok(registry.streams.has(handle2), "Handle has been found"); + equal(registry.streams.get(handle2), stream, "Expected stream found"); + + notEqual(handle1, handle2, "Different handles have been generated"); +}); + +add_task(async function test_addInvalidStreamType() { + const registry = new StreamRegistry(); + Assert.throws(() => registry.add(new Blob([])), /UnsupportedError/); +}); + +add_task(async function test_getForValidHandle() { + const registry = new StreamRegistry(); + const stream = await createFileStream("foo bar"); + const handle = registry.add(stream); + + equal(registry.streams.size, 1, "A single stream has been added"); + equal(registry.get(handle), stream, "Expected stream found"); +}); + +add_task(async function test_getForInvalidHandle() { + const registry = new StreamRegistry(); + const stream = await createFileStream("foo bar"); + registry.add(stream); + + equal(registry.streams.size, 1, "A single stream has been added"); + Assert.throws(() => registry.get("foo"), /TypeError/); +}); + +add_task(async function test_removeForValidHandle() { + const registry = new StreamRegistry(); + const stream1 = await createFileStream("foo bar"); + const stream2 = await createFileStream("foo bar"); + + const handle1 = registry.add(stream1); + const handle2 = registry.add(stream2); + + equal(registry.streams.size, 2); + + await registry.remove(handle1); + equal(registry.streams.size, 1); + equal(registry.get(handle2), stream2, "Second stream has not been closed"); + + ok( + !(await IOUtils.exists(stream1.path)), + "temporary file for first stream is removed" + ); + ok( + await IOUtils.exists(stream2.path), + "temporary file for second stream is not removed" + ); +}); + +add_task(async function test_removeForInvalidHandle() { + const registry = new StreamRegistry(); + const stream = await createFileStream("foo bar"); + registry.add(stream); + + equal(registry.streams.size, 1, "A single stream has been added"); + await Assert.rejects(registry.remove("foo"), /TypeError/); +}); + +/** + * Create a stream with the specified contents. + * + * @param {string} contents + * Contents of the file. + * @param {object} options + * @param {string=} options.path + * Path of the file. Defaults to the temporary directory. + * @param {boolean=} options.remove + * If true, automatically remove the file after the test. Defaults to true. + * + * @returns {Promise<Stream>} + */ +async function createFileStream(contents, options = {}) { + let { path = null, remove = true } = options; + + if (!path) { + path = await IOUtils.createUniqueFile( + PathUtils.tempDir, + "remote-agent.txt" + ); + } + + await IOUtils.writeUTF8(path, contents); + + const stream = new Stream(path); + if (remove) { + registerCleanupFunction(() => stream.destroy()); + } + + return stream; +} diff --git a/remote/cdp/test/xpcshell/xpcshell.toml b/remote/cdp/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..e853f32778 --- /dev/null +++ b/remote/cdp/test/xpcshell/xpcshell.toml @@ -0,0 +1,11 @@ +[DEFAULT] + +["test_CDPConnection.js"] + +["test_DomainCache.js"] + +["test_Error.js"] + +["test_Session.js"] + +["test_StreamRegistry.js"] |