From c345e520458bec7d88ea4373dd1cbef832e7c68f Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 14:02:25 +0530 Subject: [PATCH 01/10] [bidi][js] Add dom mutation handlers --- javascript/bidi-support/BUILD.bazel | 10 ++++ .../bidi-support/bidi-mutation-listener.js | 55 ++++++++++++++++++ .../node/selenium-webdriver/BUILD.bazel | 1 + .../selenium-webdriver/lib/atoms/BUILD.bazel | 7 +++ .../node/selenium-webdriver/lib/script.js | 56 +++++++++++++++++++ .../test/lib/webdriver_script_test.js | 38 +++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 javascript/bidi-support/BUILD.bazel create mode 100755 javascript/bidi-support/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..ff25115572fb7 --- /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. + +(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); + var 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..59a211355225a 100644 --- a/javascript/node/selenium-webdriver/BUILD.bazel +++ b/javascript/node/selenium-webdriver/BUILD.bazel @@ -55,6 +55,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/script.js b/javascript/node/selenium-webdriver/lib/script.js index cf23c525bd68b..cf8a629f782f6 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -16,10 +16,18 @@ // 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') +const { requireAtom } = require('./http') class Script { #driver #logInspector + #script constructor(driver) { this.#driver = driver @@ -38,6 +46,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 +73,47 @@ 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 = '../../../bazel-bin/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js' + + let mutationListener = fs.readFileSync(path.resolve(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..0bd36115149ec 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('../../index') 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] }, From 5e059756f6ab71ca7c6e89f551b6ef590ca6ef13 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 14:27:19 +0530 Subject: [PATCH 02/10] Fix formatting --- javascript/node/selenium-webdriver/lib/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/node/selenium-webdriver/lib/script.js b/javascript/node/selenium-webdriver/lib/script.js index cf8a629f782f6..673c8ed40c07f 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -82,7 +82,7 @@ class Script { argumentValues.push(value) const filePath = '../../../bazel-bin/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js' - + let mutationListener = fs.readFileSync(path.resolve(filePath), 'utf-8').toString() await this.#script.addPreloadScript(mutationListener, argumentValues) From 5fc14ca433026fd9f8fcc7fa081c54add8fdf4af Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 15:19:49 +0530 Subject: [PATCH 03/10] Fix import --- .../node/selenium-webdriver/test/lib/webdriver_script_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0bd36115149ec..f3803190cd92c 100644 --- a/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js +++ b/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js @@ -21,7 +21,7 @@ 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('../../index') +const until = require('selenium-webdriver/lib/until') suite( function (env) { From 9c72e8d7e526082d0603206a6ae75b84ea25d886 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 16:07:13 +0530 Subject: [PATCH 04/10] Update mutation-listener file location --- javascript/node/selenium-webdriver/lib/script.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/javascript/node/selenium-webdriver/lib/script.js b/javascript/node/selenium-webdriver/lib/script.js index 673c8ed40c07f..e56a11c37d5b9 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -80,10 +80,17 @@ class Script { let argumentValues = [] let value = new ArgumentValue(LocalValue.createChannelValue(new ChannelValue('channel_name'))) argumentValues.push(value) - - const filePath = '../../../bazel-bin/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js' - - let mutationListener = fs.readFileSync(path.resolve(filePath), 'utf-8').toString() + + let mutationListener = '' + try { + // Depending on what is running the code it could appear in 2 different places which is why we try + // here and then the other location + mutationListener = fs + .readFileSync('./javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener', 'utf-8') + .toString() + } catch { + mutationListener = fs.readFileSync(path.resolve(__dirname, './atoms/bidi-mutation-listener'), 'utf-8').toString() + } await this.#script.addPreloadScript(mutationListener, argumentValues) From 850c33c1ea31cfd736fa5ea67dce1bb25de74ae8 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 16:41:22 +0530 Subject: [PATCH 05/10] Fix mutation-listener file location --- .../lib/atoms/bidi-mutation-listener.js | 55 +++++++++++++++++++ .../node/selenium-webdriver/lib/script.js | 14 +---- 2 files changed, 58 insertions(+), 11 deletions(-) create mode 100755 javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js 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..ff25115572fb7 --- /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. + +(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); + var 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 e56a11c37d5b9..860e2379e2562 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -80,18 +80,10 @@ class Script { let argumentValues = [] let value = new ArgumentValue(LocalValue.createChannelValue(new ChannelValue('channel_name'))) argumentValues.push(value) - - let mutationListener = '' - try { - // Depending on what is running the code it could appear in 2 different places which is why we try - // here and then the other location - mutationListener = fs - .readFileSync('./javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener', 'utf-8') - .toString() - } catch { - mutationListener = fs.readFileSync(path.resolve(__dirname, './atoms/bidi-mutation-listener'), 'utf-8').toString() - } + 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) => { From 86dfc7af9422fc6bdf93f8891e16f639caffe66e Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 9 Jul 2024 17:42:23 +0530 Subject: [PATCH 06/10] Fix formatting --- .../lib/atoms/bidi-mutation-listener.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js index ff25115572fb7..c509b1991c841 100755 --- a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js +++ b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js @@ -15,41 +15,41 @@ // specific language governing permissions and limitations // under the License. -(channel) => { +;(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; + if (mutation.attributeName === 'data-__webdriver_id') { + break } - const curr = mutation.target.getAttribute(mutation.attributeName); + const curr = mutation.target.getAttribute(mutation.attributeName) var 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; + 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; + target: id, + name: mutation.attributeName, + value: curr, + oldValue: mutation.oldValue, + }) + channel(json) + break default: - break; + break } } - }); + }) observer.observe(document, { - 'attributes': true, - 'attributeOldValue': true, - 'characterData': true, - 'characterDataOldValue': true, - 'childList': true, - 'subtree': true - }); + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }) } From 4a2914ac58dc9f2127627e047e2f18f32c778645 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 10 Jul 2024 10:01:09 +0530 Subject: [PATCH 07/10] Remove unused import --- javascript/node/selenium-webdriver/lib/script.js | 1 - 1 file changed, 1 deletion(-) diff --git a/javascript/node/selenium-webdriver/lib/script.js b/javascript/node/selenium-webdriver/lib/script.js index 860e2379e2562..0b918c5f81fa0 100644 --- a/javascript/node/selenium-webdriver/lib/script.js +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -22,7 +22,6 @@ const { LocalValue, ChannelValue } = require('../bidi/protocolValue') const fs = require('node:fs') const path = require('node:path') const by = require('./by') -const { requireAtom } = require('./http') class Script { #driver From 44b9f953b5ada289c00c824ef5c92ce14f957752 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 10 Jul 2024 11:11:40 +0530 Subject: [PATCH 08/10] Ensure package has the mutation-listener script file --- javascript/node/selenium-webdriver/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/node/selenium-webdriver/BUILD.bazel b/javascript/node/selenium-webdriver/BUILD.bazel index 59a211355225a..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", From 132fe2ad329dbd3854700eaf692fc290659910eb Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 10 Jul 2024 15:58:01 +0530 Subject: [PATCH 09/10] Remove semicolon --- .../node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js index c509b1991c841..562cda6f27a5c 100755 --- a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js +++ b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -;(channel) => { +(channel) => { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { switch (mutation.type) { From 57f7d37ff76c827f6408ab08547c18b001bc4f0f Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 10 Jul 2024 18:50:11 +0530 Subject: [PATCH 10/10] Update bidi-mutation-listener to avoid linting issues --- .../bidi-support/bidi-mutation-listener.js | 46 +++++++++---------- .../lib/atoms/bidi-mutation-listener.js | 4 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/javascript/bidi-support/bidi-mutation-listener.js b/javascript/bidi-support/bidi-mutation-listener.js index ff25115572fb7..bf7fbe51ef15f 100755 --- a/javascript/bidi-support/bidi-mutation-listener.js +++ b/javascript/bidi-support/bidi-mutation-listener.js @@ -15,41 +15,41 @@ // specific language governing permissions and limitations // under the License. -(channel) => { +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; + if (mutation.attributeName === 'data-__webdriver_id') { + break } - const curr = mutation.target.getAttribute(mutation.attributeName); - var id = mutation.target.dataset.__webdriver_id + 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; + 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; + target: id, + name: mutation.attributeName, + value: curr, + oldValue: mutation.oldValue, + }) + channel(json) + break default: - break; + break } } - }); + }) observer.observe(document, { - 'attributes': true, - 'attributeOldValue': true, - 'characterData': true, - 'characterDataOldValue': true, - 'childList': true, - 'subtree': true - }); + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }) } diff --git a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js index 562cda6f27a5c..bf7fbe51ef15f 100755 --- a/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js +++ b/javascript/node/selenium-webdriver/lib/atoms/bidi-mutation-listener.js @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -(channel) => { +function observeMutations(channel) { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { switch (mutation.type) { @@ -25,7 +25,7 @@ break } const curr = mutation.target.getAttribute(mutation.attributeName) - var id = mutation.target.dataset.__webdriver_id + 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