From a676cdc6fe85632f5443654d496c1b73db91378d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20Miale=C5=A1ka?= Date: Wed, 23 Feb 2022 10:40:07 +0200 Subject: [PATCH] [BiDi] JavaScript Handling (#213) +semver: feature * Add JavaScripHandling wrapper over JavaScriptEngine functionality * Fix the compilation error, update the code documentation * - Add SetAttribute JsAction - Implement example test to handle DOM mutations * Add localized logging values for JavaScriptHandling methods * - Add PinnedScript extensions and element's JsActions - Add localization logger values for new methods - Implemented tests for PinnedScripts * Implement all tests for JavaScriptHandling functionality: - for adding script callback bindings - for adding initialization scripts - for subscribing to JS exceptions and console API calls Also fix ScriptCallbackBindings property of IJavaScriptEngine --- .../Aquality.Selenium.csproj | 2 + .../Aquality.Selenium/Aquality.Selenium.xml | 210 ++++++++++++- .../src/Aquality.Selenium/Browsers/Browser.cs | 6 + .../Aquality.Selenium/Browsers/JavaScript.cs | 1 + .../Browsers/JavaScriptHandling.cs | 280 ++++++++++++++++++ .../Browsers/NetworkHandling.cs | 2 +- .../Browsers/PinnedScriptExtensions.cs | 79 +++++ .../Elements/Actions/JsActions.cs | 38 +++ .../Resources/JavaScripts/SetAttribute.js | 1 + .../Resources/Localization/be.json | 31 +- .../Resources/Localization/en.json | 27 ++ .../Resources/Localization/ru.json | 27 ++ .../Integration/JavaScriptHandlingTests.cs | 207 +++++++++++++ 13 files changed, 907 insertions(+), 4 deletions(-) create mode 100644 Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScriptHandling.cs create mode 100644 Aquality.Selenium/src/Aquality.Selenium/Browsers/PinnedScriptExtensions.cs create mode 100644 Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/SetAttribute.js create mode 100644 Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/JavaScriptHandlingTests.cs diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj index 239de7cb..2c11fac7 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj @@ -25,6 +25,7 @@ + @@ -62,6 +63,7 @@ + diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml index b9605f34..04162f87 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml @@ -83,6 +83,11 @@ Provides Network Handling functionality + + + Provides JavaScript Monitoring functionality + + Gets name of desired browser from configuration. @@ -433,6 +438,146 @@ Desired JS script name. String representation of script. + + + Wrap over implementation of Selenium WebDriver IJavaScriptEngine. + + + + + Initializes a new instance of the class. + + The instance on which the JavaScript events should be monitored. + + + + Gets the read-only list of initialization scripts added for this JavaScript engine. + + + + + Gets the read-only list of binding callbacks added for this JavaScript engine. + Note: Selenium functionality here is broken, + no value added to this list in , so we store these values in read-only field. + + + + + Occurs when a JavaScript callback with a named binding is executed. + + + + + Occurs when an exception is thrown by JavaScript being executed in the browser. + + + + + Occurs when methods on the JavaScript console are called. + + + + + Occurs when a value of an attribute in an element is being changed. + + + + + Asynchronously adds JavaScript to be loaded on every document load. + + The friendly name by which to refer to this initialization script. + The JavaScript to be loaded on every page. + A task containing an object representing the script to be loaded on each page. + + + + Asynchronously removes JavaScript from being loaded on every document load. + + The friendly name of the initialization script to be removed. + A task that represents the asynchronous operation. + + + + Asynchronously removes all initialization scripts from being + loaded on every document load. + + A task that represents the asynchronous operation. + + + + Asynchronously adds a binding to a callback method that will raise + an event when the named binding is called by JavaScript executing + in the browser. + + The name of the callback that will trigger events when called by JavaScript executing in the browser. + A task that represents the asynchronous operation. + + + + Asynchronously removes a binding to a JavaScript callback. + + The name of the callback to be removed. + A task that represents the asynchronous operation. + + + + Asynchronously removes all bindings to JavaScript callbacks. + + A task that represents the asynchronous operation. + + + + Enables monitoring for DOM changes. + + A task that represents the asynchronous operation. + + + + Disables monitoring for DOM changes. + + A task that represents the asynchronous operation. + + + + Pins a JavaScript snippet for execution in the browser without transmitting the + entire script across the wire for every execution. + + The JavaScript to pin + A task containing a object to use to execute the script. + + + + Unpins a previously pinned script from the browser. + + The object to unpin. + A task that represents the asynchronous operation. + + + + Asynchronously starts monitoring for events from the browser's JavaScript engine. + + A task that represents the asynchronous operation. + + + + Stops monitoring for events from the browser's JavaScript engine. + + + + + Asynchronously removes all bindings to JavaScript callbacks and all + initialization scripts from being loaded for each document. + + A task that represents the asynchronous operation. + + + + Asynchronously removes all bindings to JavaScript callbacks, all + initialization scripts from being loaded for each document, and + stops listening for events. + + A task that represents the asynchronous operation. + Factory that creates instance of local Browser. @@ -443,7 +588,7 @@ Wrap over implementation of Selenium WebDriver INetwork. - + Initializes a new instance of the class. @@ -504,6 +649,45 @@ A task to be awaited. + + + Extensions for scripts pinned with . + + + + + Executes pinned JS script. + + Instance of script pinned with . + Script arguments. + + + + Executes pinned JS script against the element. + + Instance of script pinned with . + Instance of element created with . + Script arguments. + + + + Executes pinned JS script against the element and gets result value. + + Instance of script pinned with . + Instance of element created with . + Script arguments. + Type of return value. + Script execution result. + + + + Executes pinned JS script and gets result value. + + Instance of script pinned with . + Script arguments. + Type of return value. + Script execution result. + Factory that creates instance of remote Browser. @@ -819,6 +1003,13 @@ Set focus on element. + + + Setting attribute value. + + Attribute name + Value to set + Checks whether element on screen or not. @@ -848,6 +1039,23 @@ Point object. + + + Executed pinned script against element. + + Instance of script pinned with . + Script arguments. + Type of return value. + Script execution result. + + + + Executed pinned script against element. + + Instance of script pinned with . + Script arguments. + Script execution result. + Allows to perform actions on elements via Selenium Actions class. diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/Browser.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/Browser.cs index 8f56e666..d7f1a84c 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Browsers/Browser.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/Browser.cs @@ -30,6 +30,7 @@ public Browser(WebDriver webDriver) { Driver = webDriver; Network = new NetworkHandling(webDriver); + JavaScriptEngine = new JavaScriptHandling(webDriver); Logger = AqualityServices.LocalizedLogger; LocalizationManager = AqualityServices.Get(); browserProfile = AqualityServices.Get(); @@ -55,6 +56,11 @@ public Browser(WebDriver webDriver) /// public INetwork Network { get; } + /// + /// Provides JavaScript Monitoring functionality + /// + public IJavaScriptEngine JavaScriptEngine { get; } + /// /// Gets name of desired browser from configuration. /// diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs index 3c138541..be986b4c 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs @@ -28,6 +28,7 @@ public enum JavaScript ScrollToTop, ScrollWindowBy, SelectComboBoxValueByText, + SetAttribute, SetFocus, SetInnerHTML, SetValue, diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScriptHandling.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScriptHandling.cs new file mode 100644 index 00000000..ee2aa125 --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScriptHandling.cs @@ -0,0 +1,280 @@ +using Aquality.Selenium.Core.Localization; +using OpenQA.Selenium; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Aquality.Selenium.Browsers +{ + /// + /// Wrap over implementation of Selenium WebDriver IJavaScriptEngine. + /// + public class JavaScriptHandling : IJavaScriptEngine + { + private readonly IList bindings = new List(); + private readonly IJavaScriptEngine javaScriptEngine; + + /// + /// Initializes a new instance of the class. + /// + /// The instance on which the JavaScript events should be monitored. + public JavaScriptHandling(IWebDriver driver) + { + javaScriptEngine = new JavaScriptEngine(driver); + } + + private ILocalizedLogger Logger => AqualityServices.LocalizedLogger; + + /// + /// Gets the read-only list of initialization scripts added for this JavaScript engine. + /// + public IReadOnlyList InitializationScripts + { + get + { + Logger.Info("loc.browser.javascript.initializationscripts.get"); + return javaScriptEngine.InitializationScripts; + } + } + + /// + /// Gets the read-only list of binding callbacks added for this JavaScript engine. + /// Note: Selenium functionality here is broken, + /// no value added to this list in , so we store these values in read-only field. + /// + public IReadOnlyList ScriptCallbackBindings + { + get + { + Logger.Info("loc.browser.javascript.scriptcallbackbindings.get"); + return new List(bindings); + } + } + + /// + /// Occurs when a JavaScript callback with a named binding is executed. + /// + public event EventHandler JavaScriptCallbackExecuted + { + add + { + Logger.Info("loc.browser.javascript.event.callbackexecuted.add"); + javaScriptEngine.JavaScriptCallbackExecuted += value; + } + remove + { + Logger.Info("loc.browser.javascript.event.callbackexecuted.remove"); + javaScriptEngine.JavaScriptCallbackExecuted -= value; + } + } + + /// + /// Occurs when an exception is thrown by JavaScript being executed in the browser. + /// + public event EventHandler JavaScriptExceptionThrown + { + add + { + Logger.Info("loc.browser.javascript.event.exceptionthrown.add"); + javaScriptEngine.JavaScriptExceptionThrown += value; + } + remove + { + Logger.Info("loc.browser.javascript.event.exceptionthrown.remove"); + javaScriptEngine.JavaScriptExceptionThrown -= value; + } + } + + /// + /// Occurs when methods on the JavaScript console are called. + /// + public event EventHandler JavaScriptConsoleApiCalled + { + add + { + Logger.Info("loc.browser.javascript.event.consoleapicalled.add"); + javaScriptEngine.JavaScriptConsoleApiCalled += value; + } + remove + { + Logger.Info("loc.browser.javascript.event.consoleapicalled.remove"); + javaScriptEngine.JavaScriptConsoleApiCalled -= value; + } + } + + /// + /// Occurs when a value of an attribute in an element is being changed. + /// + public event EventHandler DomMutated + { + add + { + Logger.Info("loc.browser.javascript.event.dommutated.add"); + javaScriptEngine.DomMutated += value; + } + remove + { + Logger.Info("loc.browser.javascript.event.dommutated.remove"); + javaScriptEngine.DomMutated -= value; + } + } + + /// + /// Asynchronously adds JavaScript to be loaded on every document load. + /// + /// The friendly name by which to refer to this initialization script. + /// The JavaScript to be loaded on every page. + /// A task containing an object representing the script to be loaded on each page. + public async Task AddInitializationScript(string scriptName, string script) + { + Logger.Info("loc.browser.javascript.initializationscript.add", scriptName); + return await javaScriptEngine.AddInitializationScript(scriptName, script); + } + + /// + /// Asynchronously removes JavaScript from being loaded on every document load. + /// + /// The friendly name of the initialization script to be removed. + /// A task that represents the asynchronous operation. + public async Task RemoveInitializationScript(string scriptName) + { + Logger.Info("loc.browser.javascript.initializationscript.remove", scriptName); + await javaScriptEngine.RemoveInitializationScript(scriptName); + } + + /// + /// Asynchronously removes all initialization scripts from being + /// loaded on every document load. + /// + /// A task that represents the asynchronous operation. + public async Task ClearInitializationScripts() + { + Logger.Info("loc.browser.javascript.initializationscripts.clear"); + await javaScriptEngine.ClearInitializationScripts(); + } + + /// + /// Asynchronously adds a binding to a callback method that will raise + /// an event when the named binding is called by JavaScript executing + /// in the browser. + /// + /// The name of the callback that will trigger events when called by JavaScript executing in the browser. + /// A task that represents the asynchronous operation. + public async Task AddScriptCallbackBinding(string bindingName) + { + Logger.Info("loc.browser.javascript.scriptcallbackbinding.add", bindingName); + await javaScriptEngine.AddScriptCallbackBinding(bindingName); + bindings.Add(bindingName); + } + + /// + /// Asynchronously removes a binding to a JavaScript callback. + /// + /// The name of the callback to be removed. + /// A task that represents the asynchronous operation. + public async Task RemoveScriptCallbackBinding(string bindingName) + { + Logger.Info("loc.browser.javascript.scriptcallbackbinding.remove", bindingName); + await javaScriptEngine.RemoveScriptCallbackBinding(bindingName); + bindings.Remove(bindingName); + } + + /// + /// Asynchronously removes all bindings to JavaScript callbacks. + /// + /// A task that represents the asynchronous operation. + public async Task ClearScriptCallbackBindings() + { + Logger.Info("loc.browser.javascript.scriptcallbackbindings.clear"); + await javaScriptEngine.ClearScriptCallbackBindings(); + bindings.Clear(); + } + + /// + /// Enables monitoring for DOM changes. + /// + /// A task that represents the asynchronous operation. + public async Task EnableDomMutationMonitoring() + { + Logger.Info("loc.browser.javascript.dommutation.monitoring.enable"); + await javaScriptEngine.EnableDomMutationMonitoring(); + } + + /// + /// Disables monitoring for DOM changes. + /// + /// A task that represents the asynchronous operation. + public async Task DisableDomMutationMonitoring() + { + Logger.Info("loc.browser.javascript.dommutation.monitoring.disable"); + await javaScriptEngine.DisableDomMutationMonitoring(); + } + + /// + /// Pins a JavaScript snippet for execution in the browser without transmitting the + /// entire script across the wire for every execution. + /// + /// The JavaScript to pin + /// A task containing a object to use to execute the script. + public async Task PinScript(string script) + { + Logger.Info("loc.browser.javascript.snippet.pin"); + return await javaScriptEngine.PinScript(script); + } + + /// + /// Unpins a previously pinned script from the browser. + /// + /// The object to unpin. + /// A task that represents the asynchronous operation. + public async Task UnpinScript(PinnedScript script) + { + Logger.Info("loc.browser.javascript.snippet.unpin"); + await javaScriptEngine.UnpinScript(script); + } + + /// + /// Asynchronously starts monitoring for events from the browser's JavaScript engine. + /// + /// A task that represents the asynchronous operation. + public async Task StartEventMonitoring() + { + Logger.Info("loc.browser.javascript.event.monitoring.start"); + await javaScriptEngine.StartEventMonitoring(); + } + + /// + /// Stops monitoring for events from the browser's JavaScript engine. + /// + public void StopEventMonitoring() + { + Logger.Info("loc.browser.javascript.event.monitoring.stop"); + javaScriptEngine.StopEventMonitoring(); + } + + /// + /// Asynchronously removes all bindings to JavaScript callbacks and all + /// initialization scripts from being loaded for each document. + /// + /// A task that represents the asynchronous operation. + public async Task ClearAll() + { + Logger.Info("loc.browser.javascript.clearall"); + await javaScriptEngine.ClearAll(); + bindings.Clear(); + } + + /// + /// Asynchronously removes all bindings to JavaScript callbacks, all + /// initialization scripts from being loaded for each document, and + /// stops listening for events. + /// + /// A task that represents the asynchronous operation. + public async Task Reset() + { + Logger.Info("loc.browser.javascript.reset"); + await javaScriptEngine.Reset(); + bindings.Clear(); + } + } +} diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/NetworkHandling.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/NetworkHandling.cs index 53efdc83..98840f37 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Browsers/NetworkHandling.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/NetworkHandling.cs @@ -16,7 +16,7 @@ public class NetworkHandling : INetwork /// Initializes a new instance of the class. /// /// The instance on which the network should be monitored. - public NetworkHandling(WebDriver driver) + public NetworkHandling(IWebDriver driver) { network = driver.Manage().Network; } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/PinnedScriptExtensions.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/PinnedScriptExtensions.cs new file mode 100644 index 00000000..1e53c04c --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/PinnedScriptExtensions.cs @@ -0,0 +1,79 @@ +using Aquality.Selenium.Elements.Interfaces; +using Aquality.Selenium.Forms; +using OpenQA.Selenium; +using System; + +namespace Aquality.Selenium.Browsers +{ + /// + /// Extensions for scripts pinned with . + /// + public static class PinnedScriptExtensions + { + private static Browser Browser => AqualityServices.Browser; + + /// + /// Executes pinned JS script. + /// + /// Instance of script pinned with . + /// Script arguments. + public static void ExecuteScript(this PinnedScript pinnedScript, params object[] arguments) + { + Browser.Driver.ExecuteScript(pinnedScript, arguments); + } + /// + /// Executes pinned JS script against the element. + /// + /// Instance of script pinned with . + /// Instance of element created with . + /// Script arguments. + public static void ExecuteScript(this PinnedScript pinnedScript, IElement element, params object[] arguments) + { + element.JsActions.ExecuteScript(pinnedScript, arguments); + } + + /// + /// Executes pinned JS script against the element and gets result value. + /// + /// Instance of script pinned with . + /// Instance of element created with . + /// Script arguments. + /// Type of return value. + /// Script execution result. + public static T ExecuteScript(this PinnedScript pinnedScript, IElement element, params object[] arguments) + { + return element.JsActions.ExecuteScript(pinnedScript, arguments); + } + + /// + /// Executes pinned JS script and gets result value. + /// + /// Instance of script pinned with . + /// Script arguments. + /// Type of return value. + /// Script execution result. + public static T ExecuteScript(this PinnedScript pinnedScript, params object[] arguments) + { + var value = Browser.Driver.ExecuteScript(pinnedScript, arguments); + var result = default(T); + Type type = typeof(T); + if (value == null) + { + if (type.IsValueType && (Nullable.GetUnderlyingType(type) == null)) + { + throw new WebDriverException("Script returned null, but desired type is a value type"); + } + } + else if (!type.IsInstanceOfType(value)) + { + throw new WebDriverException("Script returned a value, but the result could not be cast to the desired type"); + } + else + { + result = (T)value; + } + + return result; + } + } +} diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs index 79312a57..61768994 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs @@ -145,6 +145,17 @@ public void SetFocus() ExecuteScript(JavaScript.SetFocus); } + /// + /// Setting attribute value. + /// + /// Attribute name + /// Value to set + public void SetAttribute(string name, string value) + { + LogElementAction("loc.el.attr.set", name, value); + ExecuteScript(JavaScript.SetAttribute, name, value); + } + /// /// Checks whether element on screen or not. /// @@ -202,6 +213,33 @@ public Point GetViewPortCoordinates() return new Point((int)Math.Round(coordinates[0]), (int)Math.Round(coordinates[1])); } + /// + /// Executed pinned script against element. + /// + /// Instance of script pinned with . + /// Script arguments. + /// Type of return value. + /// Script execution result. + public T ExecuteScript(PinnedScript pinnedScript, params object[] arguments) + { + LogElementAction("loc.el.execute.pinnedjs"); + var result = ActionRetrier.DoWithRetry(() => pinnedScript.ExecuteScript(ResolveArguments(arguments))); + LogElementAction("loc.el.execute.pinnedjs.result", result); + return result; + } + + /// + /// Executed pinned script against element. + /// + /// Instance of script pinned with . + /// Script arguments. + /// Script execution result. + public void ExecuteScript(PinnedScript pinnedScript, params object[] arguments) + { + LogElementAction("loc.el.execute.pinnedjs"); + ActionRetrier.DoWithRetry(() => pinnedScript.ExecuteScript(ResolveArguments(arguments))); + } + protected T ExecuteScript(JavaScript scriptName, params object[] arguments) { return ActionRetrier.DoWithRetry(() => Browser.ExecuteScript(scriptName, ResolveArguments(arguments))); diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/SetAttribute.js b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/SetAttribute.js new file mode 100644 index 00000000..8d040b7d --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/SetAttribute.js @@ -0,0 +1 @@ +arguments[0].setAttribute(arguments[1], arguments[2]); diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json index b5bed493..a8f4330e 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json @@ -25,7 +25,7 @@ "loc.checkable.state": "Стан: [{0}]", "loc.clicking": "Націскаем", "loc.clicking.double": "Падвойна націскаем", - "loc.clicking.js": "Націскаем праз Javascript", + "loc.clicking.js": "Націскаем праз JavaScript", "loc.clicking.right": "Націскаем правай кнопкай", "loc.combobox": "Камбабокс", "loc.combobox.getting.selected.text": "Атрымліваем выбраны тэкст", @@ -42,11 +42,14 @@ "loc.combobox.values": "Спіс значэнняў: [{0}]", "loc.el.getattr": "Атрымліваем атрыбут '{0}'", "loc.el.attr.value": "Значэнне атрыбута '{0}': [{1}]", + "loc.el.attr.set": "Задаем значэнне атрыбута '{0}': [{1}]", "loc.el.cssvalue": "Атрымліваем значэнне css '{0}'", + "loc.el.execute.pinnedjs": "Выконваем замацаваны JavaScript", + "loc.el.execute.pinnedjs.result": "Вынік выканання замацаванага JavaScript: [{0}]", "loc.focusing": "Факусуемся", "loc.get.text": "Атрымліваем тэкст элемента", "loc.text.value": "Тэкст элемента: [{0}]", - "loc.get.text.js": "Атрымліваем тэкст элемента праз Javascript", + "loc.get.text.js": "Атрымліваем тэкст элемента праз JavaScript", "loc.hover.js": "Наводзім курсор мышы на элемент праз JavaScript", "loc.is.present.js": "Вызначаем, ці прысутны элемент на экране, праз JavaScript", "loc.is.present.value": "Ці прысутны элемент на экране: [{0}]", @@ -86,6 +89,30 @@ "loc.browser.network.handler.response.clear": "Ачышчаем апрацоўшчыкі сеткавых адказаў", "loc.browser.network.monitoring.start": "Пачынаем сеткавы маніторынг", "loc.browser.network.monitoring.stop": "Спыняем сеткавы маніторынг", + "loc.browser.javascript.initializationscripts.get": "Атрымліваем ініцыялізацыйныя скрыпты", + "loc.browser.javascript.scriptcallbackbindings.get": "Атрымліваем прывязкі зваротнага выкліку JavaScript", + "loc.browser.javascript.event.callbackexecuted.add": "Падпісваемся на падзею выканання іменаванай прывязкі зваротнага выкліку JavaScript", + "loc.browser.javascript.event.callbackexecuted.remove": "Адпісваемся ад падзеі выканання іменаванай прывязкі зваротнага выкліку JavaScript", + "loc.browser.javascript.event.exceptionthrown.add": "Падпісваемся на падзею падзення памылкі JavaScript", + "loc.browser.javascript.event.exceptionthrown.remove": "Адпісваемся ад падзеі падзення памылкі JavaScript", + "loc.browser.javascript.event.consoleapicalled.add": "Падпісваемся на падзею выкліку API кансолі JavaScript", + "loc.browser.javascript.event.consoleapicalled.remove": "Адпісваемся ад падзеі выкліку API кансолі JavaScript", + "loc.browser.javascript.event.dommutated.add": "Падпісваемся на падзею зменаў у DOM", + "loc.browser.javascript.event.dommutated.remove": "Адпісваемся ад падзеі зменаў у DOM", + "loc.browser.javascript.initializationscript.add": "Дадаем ініцыялізацыйны JavaScript з прыязным іменем '{0}'", + "loc.browser.javascript.initializationscript.remove": "Выдаляем ініцыялізацыйны JavaScript з прыязным іменем '{0}'", + "loc.browser.javascript.initializationscripts.clear": "Выдаляем усе ініцыялізацыйныя скрыпты", + "loc.browser.javascript.scriptcallbackbinding.add": "Дадаем прывязку зваротнага выкліку JavaScript з іменем '{0}'", + "loc.browser.javascript.scriptcallbackbinding.remove": "Выдаляем прывязку зваротнага выкліку JavaScript з іменем '{0}'", + "loc.browser.javascript.scriptcallbackbindings.clear": "Выдаляем усе прывязкі зваротнага выкліку JavaScript", + "loc.browser.javascript.dommutation.monitoring.enable": "Уключаем маніторынг зменаў у DOM", + "loc.browser.javascript.dommutation.monitoring.disable": "Выключаем маніторынг зменаў у DOM", + "loc.browser.javascript.snippet.pin": "Замацоўваем фрагмент JavaScript", + "loc.browser.javascript.snippet.unpin": "Адмацоўваем фрагмент JavaScript", + "loc.browser.javascript.event.monitoring.start": "Пачынаем маніторынг падзеяў JavaScript", + "loc.browser.javascript.event.monitoring.stop": "Спыняем маніторынг падзеяў JavaScript", + "loc.browser.javascript.clearall": "Выдаляем усе прывязкі зваротнага выкліку JavaScript і ініцыялізацыйныя скрыпты", + "loc.browser.javascript.reset": "Выдаляем усе прывязкі зваротнага выкліку JavaScript і ініцыялізацыйныя скрыпты, і спыняем чаканне падзеяў", "loc.shadowroot.expand": "Разварочваем дрэва схаваных элементаў", "loc.shadowroot.expand.js": "Разварочваем дрэва схаваных элементаў праз JavaScript" } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json index 033a23bc..a8c83d64 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json @@ -42,7 +42,10 @@ "loc.combobox.values": "Option values: [{0}]", "loc.el.getattr": "Getting attribute '{0}'", "loc.el.attr.value": "Value of attribute '{0}': [{1}]", + "loc.el.attr.set": "Setting value of attribute '{0}': [{1}]", "loc.el.cssvalue": "Getting css value '{0}'", + "loc.el.execute.pinnedjs": "Executing pinned JavaScript", + "loc.el.execute.pinnedjs.result": "Result of pinned JavaScript execution: [{0}]", "loc.focusing": "Focusing", "loc.get.text": "Getting text from element", "loc.text.value": "Element's text: [{0}]", @@ -86,6 +89,30 @@ "loc.browser.network.handler.response.clear": "Clearing Network Response handler", "loc.browser.network.monitoring.start": "Starting Network Monitoring", "loc.browser.network.monitoring.stop": "Stopping Network Monitoring", + "loc.browser.javascript.initializationscripts.get": "Getting initialization JavaScripts", + "loc.browser.javascript.scriptcallbackbindings.get": "Getting JavaScript callback bindings", + "loc.browser.javascript.event.callbackexecuted.add": "Subscribing to JavaScript Callback With A Named Binding Is Executed event", + "loc.browser.javascript.event.callbackexecuted.remove": "Unsubscribing from JavaScript Callback With A Named Binding Is Executed event", + "loc.browser.javascript.event.exceptionthrown.add": "Subscribing to JavaScript Exception Is Thrown event", + "loc.browser.javascript.event.exceptionthrown.remove": "Unsubscribing from JavaScript Exception Is Thrown event", + "loc.browser.javascript.event.consoleapicalled.add": "Subscribing to JavaScript Console API Is Called event", + "loc.browser.javascript.event.consoleapicalled.remove": "Unsubscribing from JavaScript Console API Is Called event", + "loc.browser.javascript.event.dommutated.add": "Subscribing to DOM mutated event", + "loc.browser.javascript.event.dommutated.remove": "Unsubscribing from DOM mutated event", + "loc.browser.javascript.initializationscript.add": "Adding initialization JavaScript with friendly name '{0}'", + "loc.browser.javascript.initializationscript.remove": "Removing initialization JavaScript by friendly name '{0}'", + "loc.browser.javascript.initializationscripts.clear": "Removing all initialization JavaScripts", + "loc.browser.javascript.scriptcallbackbinding.add": "Adding JavaScript callback binding with the name '{0}'", + "loc.browser.javascript.scriptcallbackbinding.remove": "Removing JavaScript callback binding with the name '{0}'", + "loc.browser.javascript.scriptcallbackbindings.clear": "Removing all JavaScript callback bindings", + "loc.browser.javascript.dommutation.monitoring.enable": "Enabling DOM mutation monitoring", + "loc.browser.javascript.dommutation.monitoring.disable": "Disabling DOM mutation monitoring", + "loc.browser.javascript.snippet.pin": "Pinning JavaScript snippet", + "loc.browser.javascript.snippet.unpin": "Unpinning JavaScript snippet", + "loc.browser.javascript.event.monitoring.start": "Starting JavaScript Event Monitoring", + "loc.browser.javascript.event.monitoring.stop": "Stopping JavaScript Event Monitoring", + "loc.browser.javascript.clearall": "Removing all JavaScript callback bindings and initialization JavaScripts", + "loc.browser.javascript.reset": "Removing all JavaScript callback bindings and initialization JavaScripts, and stopping listening for events", "loc.shadowroot.expand": "Expanding the Shadow Root", "loc.shadowroot.expand.js": "Expanding the Shadow Root via JavaScript" } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json index ae6d35a2..b19997da 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json @@ -42,7 +42,10 @@ "loc.combobox.values": "Список значений: [{0}]", "loc.el.getattr": "Получение аттрибута '{0}'", "loc.el.attr.value": "Значение аттрибута '{0}': [{1}]", + "loc.el.attr.set": "Установка значения аттрибута '{0}': [{1}]", "loc.el.cssvalue": "Получение значения css '{0}'", + "loc.el.execute.pinnedjs": "Исполнение закреплённого JavaScript", + "loc.el.execute.pinnedjs.result": "Результат исполнения закреплённого JavaScript: [{0}]", "loc.focusing": "Взятие элемента в фокус", "loc.get.text": "Получение текста элемента", "loc.text.value": "Текст элемента: [{0}]", @@ -86,6 +89,30 @@ "loc.browser.network.handler.response.clear": "Очистка обработчиков сетевых ответов", "loc.browser.network.monitoring.start": "Начинаем сетевой мониторинг", "loc.browser.network.monitoring.stop": "Останавливаем сетевой мониторинг", + "loc.browser.javascript.initializationscripts.get": "Получение инициализационных скриптов", + "loc.browser.javascript.scriptcallbackbindings.get": "Получение привязок обратного вызова JavaScript", + "loc.browser.javascript.event.callbackexecuted.add": "Подписываемся на событие исполнения именованной привязки обратного вызова JavaScript", + "loc.browser.javascript.event.callbackexecuted.remove": "Отписываемся от события исполнения именованной привязки обратного вызова JavaScript", + "loc.browser.javascript.event.exceptionthrown.add": "Подписываемся на событие падения ошибки JavaScript", + "loc.browser.javascript.event.exceptionthrown.remove": "Отписываемся от события падения ошибки JavaScript", + "loc.browser.javascript.event.consoleapicalled.add": "Подписываемся на событие вызова API консоли JavaScript", + "loc.browser.javascript.event.consoleapicalled.remove": "Отписываемся от события вызова API консоли JavaScript", + "loc.browser.javascript.event.dommutated.add": "Подписываемся на событие изменений в DOM", + "loc.browser.javascript.event.dommutated.remove": "Отписываемся от события изменений в DOM", + "loc.browser.javascript.initializationscript.add": "Добавление инициализационного JavaScript с дружелюбным именем '{0}'", + "loc.browser.javascript.initializationscript.remove": "Удаление инициализационного JavaScript с дружелюбным именем '{0}'", + "loc.browser.javascript.initializationscripts.clear": "Удаление всех инициализационных скриптов", + "loc.browser.javascript.scriptcallbackbinding.add": "Добавление привязки обратного вызова JavaScript с именем '{0}'", + "loc.browser.javascript.scriptcallbackbinding.remove": "Удаление привязки обратного вызова JavaScript с именем'{0}'", + "loc.browser.javascript.scriptcallbackbindings.clear": "Удаление всех привязок обратного вызова JavaScript", + "loc.browser.javascript.dommutation.monitoring.enable": "Включение мониторинга изменений в DOM", + "loc.browser.javascript.dommutation.monitoring.disable": "Отключение мониторинга изменений в DOM", + "loc.browser.javascript.snippet.pin": "Закрепление фрагмента JavaScript", + "loc.browser.javascript.snippet.unpin": "Открепление фрагмента JavaScript", + "loc.browser.javascript.event.monitoring.start": "Начинаем мониторинг событий JavaScript", + "loc.browser.javascript.event.monitoring.stop": "Останавливаем мониторинг событий JavaScript", + "loc.browser.javascript.clearall": "Удаляем все привязки обратного вызова JavaScript и инициализационные скрипты", + "loc.browser.javascript.reset": "Удаляем все привязки обратного вызова JavaScript и инициализационные скрипты, и останавливаем прослушивание событий", "loc.shadowroot.expand": "Разворачиваем дерево скрытых элементов", "loc.shadowroot.expand.js": "Разворачиваем дерево скрытых элементов посредством JavaScript" } diff --git a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/JavaScriptHandlingTests.cs b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/JavaScriptHandlingTests.cs new file mode 100644 index 00000000..c74e1ec2 --- /dev/null +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/JavaScriptHandlingTests.cs @@ -0,0 +1,207 @@ +using Aquality.Selenium.Browsers; +using Aquality.Selenium.Tests.Integration.TestApp.TheInternet.Forms; +using NUnit.Framework; +using OpenQA.Selenium; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace Aquality.Selenium.Tests.Integration +{ + internal class JavaScriptHandlingTests : UITest + { + private static readonly TimeSpan NegativeConditionTimeout = TimeSpan.FromSeconds(5); + private static IJavaScriptEngine JavaScriptEngine => AqualityServices.Browser.JavaScriptEngine; + + [Test] + public void Should_BePossibleTo_SubscribeToDomMutationEvent_AndUnsubscribeFromIt() + { + const string attributeName = "cheese"; + const string attributeValue = "Gouda"; + var welcomeForm = new WelcomeForm(); + + var attributeValueChanges = new List(); + void eventHandler(object sender, DomMutatedEventArgs e) => attributeValueChanges.Add(e.AttributeData); + JavaScriptEngine.DomMutated += eventHandler; + Assert.DoesNotThrowAsync(() => JavaScriptEngine.StartEventMonitoring(), "Should be possible to start event monitoring"); + + welcomeForm.Open(); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.EnableDomMutationMonitoring(), "Should be possible to enable DOM mutation monitoring"); + + welcomeForm.SubTitleLabel.JsActions.SetAttribute(attributeName, attributeValue); + AqualityServices.ConditionalWait.WaitForTrue(() => attributeValueChanges.Count > 0, + message: "Some mutation events should be found, should be possible to subscribe to DOM mutation event"); + Assert.AreEqual(1, attributeValueChanges.Count, "Exactly one change in DOM is expected"); + var record = attributeValueChanges.Single(); + Assert.AreEqual(attributeName, record.AttributeName, "Attribute name should match to expected"); + Assert.AreEqual(attributeValue, record.AttributeValue, "Attribute value should match to expected"); + + JavaScriptEngine.DomMutated -= eventHandler; + welcomeForm.SubTitleLabel.JsActions.SetAttribute(attributeName, attributeName); + AqualityServices.ConditionalWait.WaitFor(() => attributeValueChanges.Count > 1, timeout: NegativeConditionTimeout); + Assert.AreEqual(1, attributeValueChanges.Count, "No more changes in DOM is expected, should be possible to unsubscribe from DOM mutation event"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.DisableDomMutationMonitoring(), "Should be possible to disable DOM mutation monitoring"); + Assert.DoesNotThrow(() => JavaScriptEngine.StopEventMonitoring(), "Should be possible to stop event monitoring"); + } + + [Test] + public void Should_BePossibleTo_PinScript_AndUnpinIt() + { + var script = JavaScript.GetElementXPath.GetScript(); + var welcomeForm = new WelcomeForm(); + var pinnedScript = JavaScriptEngine.PinScript(script).Result; + + welcomeForm.Open(); + + var xpath = pinnedScript.ExecuteScript(welcomeForm.SubTitleLabel); + Assert.IsNotEmpty(xpath, "Pinned script should be possible to execute"); + var expectedValue = welcomeForm.SubTitleLabel.JsActions.GetXPath(); + Assert.AreEqual(expectedValue, xpath, "Pinned script should return the same value"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.UnpinScript(pinnedScript), "Should be possible to unpin the script"); + Assert.Throws( + () => pinnedScript.ExecuteScript(welcomeForm.SubTitleLabel), + "Unpinned script should not return the value"); + Assert.DoesNotThrowAsync(() => JavaScriptEngine.Reset(), "Should be possible to reset JavaScript monitoring"); + } + + [Test] + public void Should_BePossibleTo_PinScript_WithoutReturnedValue_AndUnpinIt() + { + const string text = "text"; + var script = JavaScript.SetValue.GetScript(); + var pinnedScript = JavaScriptEngine.PinScript(script).Result; + + var keyPressesForm = new KeyPressesForm(); + keyPressesForm.Open(); + + Assert.DoesNotThrow(() => pinnedScript.ExecuteScript(keyPressesForm.InputTextBox, text), "Should be possible to execute pinned script without return value"); + + var actualText = keyPressesForm.InputTextBox.Value; + Assert.AreEqual(text, actualText, $"Text should be '{text}' after setting value via pinned JS"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.UnpinScript(pinnedScript), "Should be possible to unpin the script"); + Assert.Throws(() => pinnedScript.ExecuteScript(keyPressesForm.InputTextBox, text), "Unpinned script should not be executed"); + } + + [Test] + public void Should_BePossibleTo_SubscribeToJavaScriptConsoleApiCalledEvent_AndUnsubscribeFromIt() + { + const string consoleApiScript = "console.log('Hello world!')"; + var apiCalledMessages = new List(); + void eventHandler(object sender, JavaScriptConsoleApiCalledEventArgs e) => apiCalledMessages.Add(e.MessageContent); + JavaScriptEngine.JavaScriptConsoleApiCalled += eventHandler; + Assert.DoesNotThrowAsync(() => JavaScriptEngine.StartEventMonitoring(), "Should be possible to start event monitoring"); + + AqualityServices.Browser.ExecuteScript(consoleApiScript); + + var hasCountIncreased = AqualityServices.ConditionalWait.WaitFor(() => apiCalledMessages.Count > 0); + Assert.That(hasCountIncreased, "Some JS console API events should have been recorded, should be possible to subscribe to JS Console API called event"); + + var previousCount = apiCalledMessages.Count; + JavaScriptEngine.JavaScriptConsoleApiCalled -= eventHandler; + AqualityServices.Browser.ExecuteScript(consoleApiScript); + AqualityServices.ConditionalWait.WaitFor(() => apiCalledMessages.Count > previousCount, timeout: NegativeConditionTimeout); + Assert.AreEqual(previousCount, apiCalledMessages.Count, "No more JS console API events should be recorded, should be possible to unsubscribe from JS Console API called event"); + } + + [Test] + public void Should_BePossibleTo_SubscribeToJavaScriptExceptionThrownEvent_AndUnsubscribeFromIt() + { + var welcomeForm = new WelcomeForm(); + var errorMessages = new List(); + void eventHandler(object sender, JavaScriptExceptionThrownEventArgs e) => errorMessages.Add(e.Message); + JavaScriptEngine.JavaScriptExceptionThrown += eventHandler; + Assert.DoesNotThrowAsync(() => JavaScriptEngine.StartEventMonitoring(), "Should be possible to start event monitoring"); + welcomeForm.Open(); + welcomeForm.SubTitleLabel.JsActions.SetAttribute("onclick", "throw new Error('Hello, world!')"); + welcomeForm.SubTitleLabel.Click(); + AqualityServices.ConditionalWait.WaitFor(() => errorMessages.Count > 0); + Assert.That(errorMessages, Has.Count.GreaterThan(0), "Some JS exceptions events should have been recorded, should be possible to subscribe to JS Exceptions thrown event"); + + var previousCount = errorMessages.Count; + JavaScriptEngine.JavaScriptExceptionThrown -= eventHandler; + welcomeForm.SubTitleLabel.Click(); + AqualityServices.ConditionalWait.WaitFor(() => errorMessages.Count > previousCount, timeout: NegativeConditionTimeout); + Assert.AreEqual(previousCount, errorMessages.Count, "No more JS exceptions should be recorded, should be possible to unsubscribe from JS Exceptions thrown event"); + } + + [Test] + public void Should_BePossibleTo_AddInitializationScript_GetIt_ThenRemove_OrClear() + { + const string script = "alert('Hello world')"; + const string name = "alert"; + InitializationScript initScript = null; + Assert.DoesNotThrowAsync(async () => initScript = await JavaScriptEngine.AddInitializationScript(name, script), "Should be possible to add initialization script"); + Assert.IsNotNull(initScript, "Some initialization script model should be returned"); + Assert.AreEqual(script, initScript.ScriptSource, "Saved script source should match to expected"); + Assert.AreEqual(name, initScript.ScriptName, "Saved script name should match to expected"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.StartEventMonitoring(), "Should be possible to start event monitoring"); + AqualityServices.Browser.Refresh(); + Assert.DoesNotThrow(() => AqualityServices.Browser.HandleAlert(AlertAction.Accept), "Alert should appear and be possible to handle"); + Assert.DoesNotThrow(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Alert should appear after the refresh and be possible to handle"); + + Assert.That(JavaScriptEngine.InitializationScripts, Has.Member(initScript), "Should be possible to read initialization scripts"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.RemoveInitializationScript(name), "Should be possible to remove initialization script"); + AqualityServices.Browser.Refresh(); + Assert.Throws(() => AqualityServices.Browser.HandleAlert(AlertAction.Accept), "Initialization script should not be executed after the remove"); + Assert.That(JavaScriptEngine.InitializationScripts, Is.Empty, "Should be possible to read initialization scripts after remove"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.AddInitializationScript(name, script), "Should be possible to add the same initialization script again"); + Assert.DoesNotThrow(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Alert should appear and be possible to handle"); + Assert.That(JavaScriptEngine.InitializationScripts, Has.One.Items, "Exactly one script should be among initialization scripts"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.ClearInitializationScripts(), "Should be possible to clear initialization scripts"); + Assert.Throws(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Initialization script should not be executed after the clear"); + Assert.That(JavaScriptEngine.InitializationScripts, Is.Empty, "Should be possible to read initialization scripts after clear"); + + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.AddInitializationScript(name, script), "Should be possible to add the same initialization script again"); + Assert.DoesNotThrow(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Alert should appear and be possible to handle"); + Assert.DoesNotThrowAsync(() => JavaScriptEngine.ClearAll(), "Should be possible to clear all JavaScript monitoring"); + Assert.Throws(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Initialization script should not be executed after the clear all"); + Assert.That(JavaScriptEngine.InitializationScripts, Is.Empty, "Should be possible to read initialization scripts after clear all"); + + } + + [Test] + public void Should_BePossibleTo_AddScriptCallbackBinding_SubscribeAndUnsubscribe_GetIt_ThenRemove_OrClear() + { + const string script = "alert('Hello world')"; + const string scriptName = "alert"; + + var executedBindings = new List(); + void eventHandler(object sender, JavaScriptCallbackExecutedEventArgs e) => executedBindings.Add(e.BindingName); + JavaScriptEngine.JavaScriptCallbackExecuted += eventHandler; + Assert.DoesNotThrowAsync(() => JavaScriptEngine.AddInitializationScript(scriptName, script), "Should be possible to add initialization script"); + Assert.DoesNotThrowAsync(() => JavaScriptEngine.StartEventMonitoring(), "Should be possible to start event monitoring"); + Assert.DoesNotThrow(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), "Alert should appear and be possible to handle"); + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.AddScriptCallbackBinding(scriptName), "Should be possible to add script callback binding"); + Assert.Throws(() => AqualityServices.Browser.RefreshPageWithAlert(AlertAction.Accept), + "Callback binding should prevent from initialization script execution"); + AqualityServices.ConditionalWait.WaitForTrue(() => executedBindings.Contains(scriptName), message: "Subscription to JavaScriptCallbackExecuted event should work"); + var oldCount = executedBindings.Count; + AqualityServices.Browser.Refresh(); + Assert.That(executedBindings, Has.Count.GreaterThan(oldCount), "Another event should be noticed"); + Assert.That(JavaScriptEngine.ScriptCallbackBindings, Has.Member(scriptName), "Should be possible to read script callback bindings"); + oldCount = executedBindings.Count; + + Assert.DoesNotThrowAsync(() => JavaScriptEngine.RemoveScriptCallbackBinding(scriptName), "Should be possible to remove script callback binding"); + Assert.That(JavaScriptEngine.ScriptCallbackBindings, Is.Empty, "Should be possible to read script callback bindings after remove"); + Assert.DoesNotThrowAsync(() => JavaScriptEngine.AddScriptCallbackBinding(scriptName), "Should be possible to add script callback binding again"); + Assert.That(JavaScriptEngine.ScriptCallbackBindings, Has.Member(scriptName), "Should be possible to read script callback bindings"); + Assert.DoesNotThrowAsync(() => JavaScriptEngine.ClearScriptCallbackBindings(), "Should be possible to clear script callback bindings"); + Assert.That(JavaScriptEngine.ScriptCallbackBindings, Is.Empty, "Should be possible to read script callback bindings after remove"); + + JavaScriptEngine.JavaScriptCallbackExecuted -= eventHandler; + AqualityServices.Browser.Refresh(); + Assert.That(executedBindings, Has.Count.EqualTo(oldCount), "Another event should not be noticed, should be possible to unsubscribe from JavaScriptCallbackExecuted event"); + } + } +}