diff options
Diffstat (limited to '')
-rw-r--r-- | remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs new file mode 100644 index 0000000000..8421445d2c --- /dev/null +++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs @@ -0,0 +1,475 @@ +/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs", + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + ClipRectangleType: + "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + LoadListener: "chrome://remote/content/shared/listeners/LoadListener.sys.mjs", + LocatorType: + "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs", + OriginType: + "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs", +}); + +const DOCUMENT_FRAGMENT_NODE = 11; +const DOCUMENT_NODE = 9; +const ELEMENT_NODE = 1; + +const ORDERED_NODE_SNAPSHOT_TYPE = 7; + +class BrowsingContextModule extends WindowGlobalBiDiModule { + #loadListener; + #subscribedEvents; + + constructor(messageHandler) { + super(messageHandler); + + // Setup the LoadListener as early as possible. + this.#loadListener = new lazy.LoadListener(this.messageHandler.window); + this.#loadListener.on("DOMContentLoaded", this.#onDOMContentLoaded); + this.#loadListener.on("load", this.#onLoad); + + // Set of event names which have active subscriptions. + this.#subscribedEvents = new Set(); + } + + destroy() { + this.#loadListener.destroy(); + this.#subscribedEvents = null; + } + + #getNavigationInfo(data) { + // Note: the navigation id is collected in the parent-process and will be + // added via event interception by the windowglobal-in-root module. + return { + context: this.messageHandler.context, + timestamp: Date.now(), + url: data.target.URL, + }; + } + + #getOriginRectangle(origin) { + const win = this.messageHandler.window; + + if (origin === lazy.OriginType.viewport) { + const viewport = win.visualViewport; + // Until it's clarified in the scope of the issue: + // https://github.com/w3c/webdriver-bidi/issues/592 + // if we should take into account scrollbar dimensions, when calculating + // the viewport size, we match the behavior of WebDriver Classic, + // meaning we include scrollbar dimensions. + return new DOMRect( + viewport.pageLeft, + viewport.pageTop, + win.innerWidth, + win.innerHeight + ); + } + + const documentElement = win.document.documentElement; + return new DOMRect( + 0, + 0, + documentElement.scrollWidth, + documentElement.scrollHeight + ); + } + + #startListening() { + if (this.#subscribedEvents.size == 0) { + this.#loadListener.startListening(); + } + } + + #stopListening() { + if (this.#subscribedEvents.size == 0) { + this.#loadListener.stopListening(); + } + } + + #subscribeEvent(event) { + switch (event) { + case "browsingContext._documentInteractive": + this.#startListening(); + this.#subscribedEvents.add("browsingContext._documentInteractive"); + break; + case "browsingContext.domContentLoaded": + this.#startListening(); + this.#subscribedEvents.add("browsingContext.domContentLoaded"); + break; + case "browsingContext.load": + this.#startListening(); + this.#subscribedEvents.add("browsingContext.load"); + break; + } + } + + #unsubscribeEvent(event) { + switch (event) { + case "browsingContext._documentInteractive": + this.#subscribedEvents.delete("browsingContext._documentInteractive"); + break; + case "browsingContext.domContentLoaded": + this.#subscribedEvents.delete("browsingContext.domContentLoaded"); + break; + case "browsingContext.load": + this.#subscribedEvents.delete("browsingContext.load"); + break; + } + + this.#stopListening(); + } + + #onDOMContentLoaded = (eventName, data) => { + if (this.#subscribedEvents.has("browsingContext._documentInteractive")) { + this.messageHandler.emitEvent("browsingContext._documentInteractive", { + baseURL: data.target.baseURI, + contextId: this.messageHandler.contextId, + documentURL: data.target.URL, + innerWindowId: this.messageHandler.innerWindowId, + readyState: data.target.readyState, + }); + } + + if (this.#subscribedEvents.has("browsingContext.domContentLoaded")) { + this.emitEvent( + "browsingContext.domContentLoaded", + this.#getNavigationInfo(data) + ); + } + }; + + #onLoad = (eventName, data) => { + if (this.#subscribedEvents.has("browsingContext.load")) { + this.emitEvent("browsingContext.load", this.#getNavigationInfo(data)); + } + }; + + /** + * Locate nodes using css selector. + * + * @see https://w3c.github.io/webdriver-bidi/#locate-nodes-using-css + */ + #locateNodesUsingCss(contextNodes, selector, maxReturnedNodeCount) { + const returnedNodes = []; + + for (const contextNode of contextNodes) { + let elements; + try { + elements = contextNode.querySelectorAll(selector); + } catch (e) { + throw new lazy.error.InvalidSelectorError( + `${e.message}: "${selector}"` + ); + } + + if (maxReturnedNodeCount === null) { + returnedNodes.push(...elements); + } else { + for (const element of elements) { + returnedNodes.push(element); + + if (returnedNodes.length === maxReturnedNodeCount) { + return returnedNodes; + } + } + } + } + + return returnedNodes; + } + + /** + * Locate nodes using XPath. + * + * @see https://w3c.github.io/webdriver-bidi/#locate-nodes-using-xpath + */ + #locateNodesUsingXPath(contextNodes, selector, maxReturnedNodeCount) { + const returnedNodes = []; + + for (const contextNode of contextNodes) { + let evaluationResult; + try { + evaluationResult = this.messageHandler.window.document.evaluate( + selector, + contextNode, + null, + ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + } catch (e) { + const errorMessage = `${e.message}: "${selector}"`; + if (DOMException.isInstance(e) && e.name === "SyntaxError") { + throw new lazy.error.InvalidSelectorError(errorMessage); + } + + throw new lazy.error.UnknownError(errorMessage); + } + + for (let index = 0; index < evaluationResult.snapshotLength; index++) { + const node = evaluationResult.snapshotItem(index); + returnedNodes.push(node); + + if ( + maxReturnedNodeCount !== null && + returnedNodes.length === maxReturnedNodeCount + ) { + return returnedNodes; + } + } + } + + return returnedNodes; + } + + /** + * Normalize rectangle. This ensures that the resulting rect has + * positive width and height dimensions. + * + * @see https://w3c.github.io/webdriver-bidi/#normalise-rect + * + * @param {DOMRect} rect + * An object which describes the size and position of a rectangle. + * + * @returns {DOMRect} Normalized rectangle. + */ + #normalizeRect(rect) { + let { x, y, width, height } = rect; + + if (width < 0) { + x += width; + width = -width; + } + + if (height < 0) { + y += height; + height = -height; + } + + return new DOMRect(x, y, width, height); + } + + /** + * Create a new rectangle which will be an intersection of + * rectangles specified as arguments. + * + * @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection + * + * @param {DOMRect} rect1 + * An object which describes the size and position of a rectangle. + * @param {DOMRect} rect2 + * An object which describes the size and position of a rectangle. + * + * @returns {DOMRect} Rectangle, representing an intersection of <var>rect1</var> and <var>rect2</var>. + */ + #rectangleIntersection(rect1, rect2) { + rect1 = this.#normalizeRect(rect1); + rect2 = this.#normalizeRect(rect2); + + const x_min = Math.max(rect1.x, rect2.x); + const x_max = Math.min(rect1.x + rect1.width, rect2.x + rect2.width); + + const y_min = Math.max(rect1.y, rect2.y); + const y_max = Math.min(rect1.y + rect1.height, rect2.y + rect2.height); + + const width = Math.max(x_max - x_min, 0); + const height = Math.max(y_max - y_min, 0); + + return new DOMRect(x_min, y_min, width, height); + } + + /** + * Internal commands + */ + + _applySessionData(params) { + // TODO: Bug 1775231. Move this logic to a shared module or an abstract + // class. + const { category } = params; + if (category === "event") { + const filteredSessionData = params.sessionData.filter(item => + this.messageHandler.matchesContext(item.contextDescriptor) + ); + for (const event of this.#subscribedEvents.values()) { + const hasSessionItem = filteredSessionData.some( + item => item.value === event + ); + // If there are no session items for this context, we should unsubscribe from the event. + if (!hasSessionItem) { + this.#unsubscribeEvent(event); + } + } + + // Subscribe to all events, which have an item in SessionData. + for (const { value } of filteredSessionData) { + this.#subscribeEvent(value); + } + } + } + + /** + * Waits until the viewport has reached the new dimensions. + * + * @param {object} options + * @param {number} options.height + * Expected height the viewport will resize to. + * @param {number} options.width + * Expected width the viewport will resize to. + * + * @returns {Promise} + * Promise that resolves when the viewport has been resized. + */ + async _awaitViewportDimensions(options) { + const { height, width } = options; + + const win = this.messageHandler.window; + let resized; + + // Updates for background tabs are throttled, and we also 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); + }); + } + + _getBaseURL() { + return this.messageHandler.window.document.baseURI; + } + + _getScreenshotRect(params = {}) { + const { clip, origin } = params; + + const originRect = this.#getOriginRectangle(origin); + let clipRect = originRect; + + if (clip !== null) { + switch (clip.type) { + case lazy.ClipRectangleType.Box: { + clipRect = new DOMRect( + clip.x + originRect.x, + clip.y + originRect.y, + clip.width, + clip.height + ); + break; + } + + case lazy.ClipRectangleType.Element: { + const realm = this.messageHandler.getRealm(); + const element = this.deserialize(clip.element, realm); + const viewportRect = this.#getOriginRectangle( + lazy.OriginType.viewport + ); + const elementRect = element.getBoundingClientRect(); + + clipRect = new DOMRect( + elementRect.x + viewportRect.x, + elementRect.y + viewportRect.y, + elementRect.width, + elementRect.height + ); + break; + } + } + } + + return this.#rectangleIntersection(originRect, clipRect); + } + + _locateNodes(params = {}) { + const { + locator, + maxNodeCount, + resultOwnership, + sandbox, + serializationOptions, + startNodes, + } = params; + + const realm = this.messageHandler.getRealm({ sandboxName: sandbox }); + + const contextNodes = []; + if (startNodes === null) { + contextNodes.push(this.messageHandler.window.document.documentElement); + } else { + for (const serializedStartNode of startNodes) { + const startNode = this.deserialize(serializedStartNode, realm); + lazy.assert.that( + startNode => + Node.isInstance(startNode) && + [DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, ELEMENT_NODE].includes( + startNode.nodeType + ), + `Expected an item of "startNodes" to be an Element, got ${startNode}` + )(startNode); + + contextNodes.push(startNode); + } + } + + let returnedNodes; + switch (locator.type) { + case lazy.LocatorType.css: { + returnedNodes = this.#locateNodesUsingCss( + contextNodes, + locator.value, + maxNodeCount + ); + break; + } + case lazy.LocatorType.xpath: { + returnedNodes = this.#locateNodesUsingXPath( + contextNodes, + locator.value, + maxNodeCount + ); + break; + } + } + + const serializedNodes = []; + const seenNodeIds = new Map(); + for (const returnedNode of returnedNodes) { + serializedNodes.push( + this.serialize( + returnedNode, + serializationOptions, + resultOwnership, + realm, + { seenNodeIds } + ) + ); + } + + return { + serializedNodes, + _extraData: { seenNodeIds }, + }; + } +} + +export const browsingContext = BrowsingContextModule; |