From 0f6884149f1c60f03538c84473f2e48b1fcd47db Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Thu, 11 Jul 2024 09:47:50 +0530 Subject: [PATCH] [bidi][js] Add dom mutation handlers (#14238) Related to #13992. --- javascript/bidi-support/BUILD.bazel | 10 ++++ .../bidi-support/bidi-mutation-listener.js | 55 +++++++++++++++++++ .../node/selenium-webdriver/BUILD.bazel | 2 + .../selenium-webdriver/lib/atoms/BUILD.bazel | 7 +++ .../lib/atoms/bidi-mutation-listener.js | 55 +++++++++++++++++++ .../node/selenium-webdriver/lib/script.js | 54 ++++++++++++++++++ .../test/lib/webdriver_script_test.js | 38 +++++++++++++ 7 files changed, 221 insertions(+) create mode 100644 javascript/bidi-support/BUILD.bazel create mode 100755 javascript/bidi-support/bidi-mutation-listener.js create mode 100755 javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js diff --git a/javascript/bidi-support/BUILD.bazel b/javascript/bidi-support/BUILD.bazel new file mode 100644 index 0000000000000..2c023ea6098f3 --- /dev/null +++ b/javascript/bidi-support/BUILD.bazel @@ -0,0 +1,10 @@ +package(default_visibility = [ + "//dotnet/src/webdriver:__pkg__", + "//java/src/org/openqa/selenium/bidi:__pkg__", + "//java/src/org/openqa/selenium/remote:__pkg__", + "//javascript/node/selenium-webdriver:__pkg__", +]) + +exports_files([ + "bidi-mutation-listener.js", +]) diff --git a/javascript/bidi-support/bidi-mutation-listener.js b/javascript/bidi-support/bidi-mutation-listener.js new file mode 100755 index 0000000000000..bf7fbe51ef15f --- /dev/null +++ b/javascript/bidi-support/bidi-mutation-listener.js @@ -0,0 +1,55 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +function observeMutations(channel) { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + switch (mutation.type) { + case 'attributes': + // Don't report our own attribute has changed. + if (mutation.attributeName === 'data-__webdriver_id') { + break + } + const curr = mutation.target.getAttribute(mutation.attributeName) + let id = mutation.target.dataset.__webdriver_id + if (!id) { + id = Math.random().toString(36).substring(2) + Date.now().toString(36) + mutation.target.dataset.__webdriver_id = id + } + const json = JSON.stringify({ + target: id, + name: mutation.attributeName, + value: curr, + oldValue: mutation.oldValue, + }) + channel(json) + break + default: + break + } + } + }) + + observer.observe(document, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }) +} diff --git a/javascript/node/selenium-webdriver/BUILD.bazel b/javascript/node/selenium-webdriver/BUILD.bazel index bb9c42802c0fd..f95b17ae246d5 100644 --- a/javascript/node/selenium-webdriver/BUILD.bazel +++ b/javascript/node/selenium-webdriver/BUILD.bazel @@ -32,6 +32,7 @@ js_library( "http/*.js", "io/*.js", "lib/*.js", + "lib/atoms/bidi-mutation-listener.js", "net/*.js", "remote/*.js", "testing/*.js", @@ -55,6 +56,7 @@ npm_package( ":manager-macos", ":manager-windows", ":prod-src-files", + "//javascript/node/selenium-webdriver/lib/atoms:bidi-mutation-listener", "//javascript/node/selenium-webdriver/lib/atoms:find-elements", "//javascript/node/selenium-webdriver/lib/atoms:get_attribute", "//javascript/node/selenium-webdriver/lib/atoms:is_displayed", diff --git a/javascript/node/selenium-webdriver/lib/atoms/BUILD.bazel b/javascript/node/selenium-webdriver/lib/atoms/BUILD.bazel index ad1dd6c2158ed..1decb84661dac 100644 --- a/javascript/node/selenium-webdriver/lib/atoms/BUILD.bazel +++ b/javascript/node/selenium-webdriver/lib/atoms/BUILD.bazel @@ -49,3 +49,10 @@ copy_file( out = "mutation-listener.js", visibility = ["//javascript/node/selenium-webdriver:__pkg__"], ) + +copy_file( + name = "bidi-mutation-listener", + src = "//javascript/bidi-support:bidi-mutation-listener.js", + out = "bidi-mutation-listener.js", + visibility = ["//javascript/node/selenium-webdriver:__pkg__"], +) diff --git a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js new file mode 100755 index 0000000000000..bf7fbe51ef15f --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js @@ -0,0 +1,55 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +function observeMutations(channel) { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + switch (mutation.type) { + case 'attributes': + // Don't report our own attribute has changed. + if (mutation.attributeName === 'data-__webdriver_id') { + break + } + const curr = mutation.target.getAttribute(mutation.attributeName) + let id = mutation.target.dataset.__webdriver_id + if (!id) { + id = Math.random().toString(36).substring(2) + Date.now().toString(36) + mutation.target.dataset.__webdriver_id = id + } + const json = JSON.stringify({ + target: id, + name: mutation.attributeName, + value: curr, + oldValue: mutation.oldValue, + }) + channel(json) + break + default: + break + } + } + }) + + observer.observe(document, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }) +} diff --git a/javascript/node/selenium-webdriver/lib/script.js b/javascript/node/selenium-webdriver/lib/script.js index cf23c525bd68b..0b918c5f81fa0 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -16,10 +16,17 @@ // under the License. const logInspector = require('../bidi/logInspector') +const scriptManager = require('../bidi//scriptManager') +const { ArgumentValue } = require('../bidi/argumentValue') +const { LocalValue, ChannelValue } = require('../bidi/protocolValue') +const fs = require('node:fs') +const path = require('node:path') +const by = require('./by') class Script { #driver #logInspector + #script constructor(driver) { this.#driver = driver @@ -38,6 +45,13 @@ class Script { this.#logInspector = await logInspector(this.#driver) } + async #initScript() { + if (this.#script !== undefined) { + return + } + this.#script = await scriptManager([], this.#driver) + } + async addJavaScriptErrorHandler(callback) { await this.#init() return await this.#logInspector.onJavascriptException(callback) @@ -58,6 +72,46 @@ class Script { await this.#logInspector.removeCallback(id) } + + async addDomMutationHandler(callback) { + await this.#initScript() + + let argumentValues = [] + let value = new ArgumentValue(LocalValue.createChannelValue(new ChannelValue('channel_name'))) + argumentValues.push(value) + + const filePath = path.join(__dirname, 'atoms', 'bidi-mutation-listener.js') + + let mutationListener = fs.readFileSync(filePath, 'utf-8').toString() + await this.#script.addPreloadScript(mutationListener, argumentValues) + + let id = await this.#script.onMessage(async (message) => { + let payload = JSON.parse(message['data']['value']) + let elements = await this.#driver.findElements({ + css: '*[data-__webdriver_id=' + by.escapeCss(payload['target']) + ']', + }) + + if (elements.length === 0) { + return + } + + let event = { + element: elements[0], + attribute_name: payload['name'], + current_value: payload['value'], + old_value: payload['oldValue'], + } + callback(event) + }) + + return id + } + + async removeDomMutationHandler(id) { + await this.#initScript() + + await this.#script.removeCallback(id) + } } module.exports = Script diff --git a/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js b/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js index fb37a07dbd398..f3803190cd92c 100644 --- a/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js +++ b/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js @@ -20,6 +20,8 @@ const assert = require('node:assert') const { Browser } = require('selenium-webdriver') const { Pages, suite } = require('../../lib/test') +const fileServer = require('../../lib/test/fileserver') +const until = require('selenium-webdriver/lib/until') suite( function (env) { @@ -84,6 +86,42 @@ suite( assert.strictEqual(e.message, 'Callback with id 10 not found') } }) + + it('can listen to dom mutations', async function () { + let message = null + await driver.script().addDomMutationHandler((m) => { + message = m + }) + + await driver.get(fileServer.Pages.dynamicPage) + + let element = driver.findElement({ id: 'reveal' }) + await element.click() + let revealed = driver.findElement({ id: 'revealed' }) + await driver.wait(until.elementIsVisible(revealed), 5000) + + assert.strictEqual(message['attribute_name'], 'style') + assert.strictEqual(message['current_value'], '') + assert.strictEqual(message['old_value'], 'display:none;') + }) + + it('can remove to dom mutation handler', async function () { + let message = null + let id = await driver.script().addDomMutationHandler((m) => { + message = m + }) + + await driver.get(fileServer.Pages.dynamicPage) + + await driver.script().removeDomMutationHandler(id) + + let element = driver.findElement({ id: 'reveal' }) + await element.click() + let revealed = driver.findElement({ id: 'revealed' }) + await driver.wait(until.elementIsVisible(revealed), 5000) + + assert.strictEqual(message, null) + }) }) }, { browsers: [Browser.FIREFOX, Browser.CHROME, Browser.EDGE] },