Skip to content

Commit

Permalink
Bug 1855023 - [bidi] Implement basic "browsingContext.locateNodes" co…
Browse files Browse the repository at this point in the history
…mmand for CSS selector. r=webdriver-reviewers,jdescottes,whimboo

Differential Revision: https://phabricator.services.mozilla.com/D196740
  • Loading branch information
lutien committed Dec 20, 2023
1 parent 5441935 commit 3664943
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 1 deletion.
193 changes: 192 additions & 1 deletion remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/NavigationManager.sys.mjs",
NavigationListener:
"chrome://remote/content/shared/listeners/NavigationListener.sys.mjs",
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
print: "chrome://remote/content/shared/PDF.sys.mjs",
ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
PromptListener:
"chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
setDefaultAndAssertSerializationOptions:
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
waitForInitialNavigationCompleted:
"chrome://remote/content/shared/Navigate.sys.mjs",
Expand Down Expand Up @@ -75,6 +78,22 @@ const CreateType = {
window: "window",
};

/**
* @typedef {string} LocatorType
*/

/**
* Enum of types supported by the browsingContext.locateNodes command.
*
* @readonly
* @enum {LocatorType}
*/
export const LocatorType = {
css: "css",
innerText: "innerText",
xpath: "xpath",
};

/**
* @typedef {string} OriginType
*/
Expand Down Expand Up @@ -701,6 +720,175 @@ class BrowsingContextModule extends Module {
throw new lazy.error.NoSuchAlertError();
}

/**
* Used as an argument for browsingContext.locateNodes command, as one of the available variants
* {CssLocator}, {InnerTextLocator} or {XPathLocator}, to represent a way of how lookup of nodes
* is going to be performed.
*
* @typedef Locator
*/

/**
* Used as an argument for browsingContext.locateNodes command
* to represent a lookup by css selector.
*
* @typedef CssLocator
*
* @property {LocatorType} [type=LocatorType.css]
* @property {string} value
*/

/**
* Used as an argument for browsingContext.locateNodes command
* to represent a lookup by inner text.
*
* @typedef InnerTextLocator
*
* @property {LocatorType} [type=LocatorType.innerText]
* @property {string} value
* @property {boolean=} ignoreCase
* @property {("full"|"partial")=} matchType
* @property {number=} maxDepth
*/

/**
* Used as an argument for browsingContext.locateNodes command
* to represent a lookup by xpath.
*
* @typedef XPathLocator
*
* @property {LocatorType} [type=LocatorType.xpath]
* @property {string} value
*/

/**
* Returns a list of all nodes matching
* the specified locator.
*
* @param {object} options
* @param {string} options.context
* Id of the browsing context.
* @param {Locator} options.locator
* The type of lookup which is going to be used.
* @param {number=} options.maxNodeCount
* The maximum amount of nodes which is going to be returned.
* Defaults to return all the found nodes.
* @param {OwnershipModel=} options.ownership
* The ownership model to use for the serialization
* of the DOM nodes. Defaults to `OwnershipModel.None`.
* @property {string=} sandbox
* The name of the sandbox. If the value is null or empty
* string, the default realm will be used.
* @property {SerializationOptions=} serializationOptions
* An object which holds the information of how the DOM nodes
* should be serialized.
* @property {Array<SharedReference>=} startNodes
* A list of references to nodes, which are used as
* starting points for lookup.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {InvalidSelectorError}
* Raised if a locator value is invalid.
* @throws {NoSuchFrameError}
* If the browsing context cannot be found.
* @throws {UnsupportedOperationError}
* Raised when unsupported lookup types are used.
*/
async locateNodes(options = {}) {
const {
context: contextId,
locator,
maxNodeCount = null,
ownership = lazy.OwnershipModel.None,
sandbox = null,
serializationOptions,
startNodes = null,
} = options;

lazy.assert.string(
contextId,
`Expected "context" to be a string, got ${contextId}`
);

const context = this.#getBrowsingContext(contextId);

lazy.assert.object(
locator,
`Expected "locator" to be an object, got ${locator}`
);

const locatorTypes = Object.values(LocatorType);

lazy.assert.that(
locatorType => locatorTypes.includes(locatorType),
`Expected "locator.type" to be one of ${locatorTypes}, got ${locator.type}`
)(locator.type);

if (locator.type !== LocatorType.css) {
throw new lazy.error.UnsupportedOperationError(
`"locator.type" argument with value: ${locator.type} is not supported yet.`
);
}

if (maxNodeCount != null) {
const maxNodeCountErrorMsg = `Expected "maxNodeCount" to be an integer and greater than 0, got ${maxNodeCount}`;
lazy.assert.that(maxNodeCount => {
lazy.assert.integer(maxNodeCount, maxNodeCountErrorMsg);
return maxNodeCount > 0;
}, maxNodeCountErrorMsg)(maxNodeCount);
}

const ownershipTypes = Object.values(lazy.OwnershipModel);
lazy.assert.that(
ownership => ownershipTypes.includes(ownership),
`Expected "ownership" to be one of ${ownershipTypes}, got ${ownership}`
)(ownership);

if (sandbox != null) {
lazy.assert.string(
sandbox,
`Expected "sandbox" to be a string, got ${sandbox}`
);
}

const serializationOptionsWithDefaults =
lazy.setDefaultAndAssertSerializationOptions(serializationOptions);

if (startNodes != null) {
lazy.assert.that(startNodes => {
lazy.assert.array(
startNodes,
`Expected "startNodes" to be an array, got ${startNodes}`
);
return !!startNodes.length;
}, `Expected "startNodes" to have at least one element, got ${startNodes}`)(
startNodes
);
}

const result = await this.messageHandler.forwardCommand({
moduleName: "browsingContext",
commandName: "_locateNodes",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: {
locator,
maxNodeCount,
resultOwnership: ownership,
sandbox,
serializationOptions: serializationOptionsWithDefaults,
startNodes,
},
});

return {
nodes: result.serializedNodes,
};
}

/**
* An object that holds the WebDriver Bidi navigation information.
*
Expand Down Expand Up @@ -1117,7 +1305,10 @@ class BrowsingContextModule extends Module {

const context = this.#getBrowsingContext(contextId);

lazy.assert.integer(delta);
lazy.assert.integer(
delta,
`Expected "delta" to be an integer, got ${delta}`
);

const sessionHistory = context.sessionHistory;
const allSteps = sessionHistory.count;
Expand Down
105 changes: 105 additions & 0 deletions remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ 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;

class BrowsingContextModule extends WindowGlobalBiDiModule {
#loadListener;
#subscribedEvents;
Expand Down Expand Up @@ -143,6 +151,40 @@ class BrowsingContextModule extends WindowGlobalBiDiModule {
}
};

/**
* 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;
}

/**
* Normalize rectangle. This ensures that the resulting rect has
* positive width and height dimensions.
Expand Down Expand Up @@ -312,6 +354,69 @@ class BrowsingContextModule extends WindowGlobalBiDiModule {

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(realm, serializedStartNode);
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;
}
}

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;

0 comments on commit 3664943

Please sign in to comment.