From cb7253bd54d39b96597b6fc8a9b2edadde1e7fae Mon Sep 17 00:00:00 2001 From: Mathias Lykkegaard Lorenzen Date: Wed, 20 Mar 2019 21:22:51 +0100 Subject: [PATCH] stability improvements. (#24) --- ...PuppeteerWebAutomationFrameworkInstance.cs | 69 ++- .../Program.cs | 7 +- .../SeleniumWebAutomationFrameworkInstance.cs | 567 +++++++++--------- .../Dom/DomRectangle.cs | 50 +- .../Dom/IDomElement.cs | 24 +- .../Dom/IDomRectangle.cs | 26 +- .../Fluent/BaseMethodChainNode.cs | 20 +- .../Targets/BaseMouseTargetMethodChainNode.cs | 4 +- .../IWebAutomationFrameworkInstance.cs | 10 +- 9 files changed, 420 insertions(+), 357 deletions(-) diff --git a/src/FluffySpoon.Automation.Web.Puppeteer/PuppeteerWebAutomationFrameworkInstance.cs b/src/FluffySpoon.Automation.Web.Puppeteer/PuppeteerWebAutomationFrameworkInstance.cs index e544dba..500d06e 100644 --- a/src/FluffySpoon.Automation.Web.Puppeteer/PuppeteerWebAutomationFrameworkInstance.cs +++ b/src/FluffySpoon.Automation.Web.Puppeteer/PuppeteerWebAutomationFrameworkInstance.cs @@ -22,6 +22,8 @@ class PuppeteerWebAutomationFrameworkInstance : IWebAutomationFrameworkInstance private readonly Func> _driverConstructor; private readonly IJavaScriptTunnel _domTunnel; + private bool _isDisposed; + public PuppeteerWebAutomationFrameworkInstance( Func> driverConstructor, IJavaScriptTunnel domTunnel) @@ -44,18 +46,20 @@ private async Task GetElementHandlesFromDomElementsAsync(IReadO return await _page.QuerySelectorAllAsync(selector); } - public async Task ClickAsync(IReadOnlyList elements, int offsetX, int offsetY) + public async Task ClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY) { foreach (var element in elements) { await _page.Mouse.ClickAsync( - (int)Math.Ceiling(element.BoundingClientRectangle.Left) + offsetX, - (int)Math.Ceiling(element.BoundingClientRectangle.Top) + offsetY); + (element.BoundingClientRectangle.Left + offsetX) ?? element.BoundingClientRectangle.Center.X, + (element.BoundingClientRectangle.Top + offsetY) ?? element.BoundingClientRectangle.Center.Y); } } public Task DisposeAsync() { + _isDisposed = true; + if (_page != null) { _page.Request -= PageRequest; @@ -70,13 +74,13 @@ public Task DisposeAsync() return Task.CompletedTask; } - public async Task DoubleClickAsync(IReadOnlyList elements, int offsetX, int offsetY) + public async Task DoubleClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY) { foreach (var element in elements) { await _page.Mouse.ClickAsync( - (int)Math.Ceiling(element.BoundingClientRectangle.Left) + offsetX, - (int)Math.Ceiling(element.BoundingClientRectangle.Top) + offsetY, + (element.BoundingClientRectangle.Left + offsetX) ?? element.BoundingClientRectangle.Center.X, + (element.BoundingClientRectangle.Top + offsetY) ?? element.BoundingClientRectangle.Center.Y, new ClickOptions() { ClickCount = 2 @@ -86,11 +90,11 @@ await _page.Mouse.ClickAsync( public async Task DragDropAsync( IDomElement from, - int fromOffsetX, - int fromOffsetY, + int? fromOffsetX, + int? fromOffsetY, IDomElement to, - int toOffsetX, - int toOffsetY) + int? toOffsetX, + int? toOffsetY) { var javascriptScope = _domTunnel.DeclareScope(this); try @@ -98,8 +102,8 @@ public async Task DragDropAsync( var dataTransferObjectVariableName = await javascriptScope.CreateNewVariableAsync("new DataTransfer()"); await _page.Mouse.MoveAsync( - (int)Math.Ceiling(from.BoundingClientRectangle.Left) + fromOffsetX, - (int)Math.Ceiling(from.BoundingClientRectangle.Top) + fromOffsetY); + (from.BoundingClientRectangle.Left + fromOffsetX) ?? from.BoundingClientRectangle.Center.X, + (from.BoundingClientRectangle.Top + fromOffsetY) ?? from.BoundingClientRectangle.Center.Y); await _page.Mouse.DownAsync(); await _domTunnel.DispatchDomElementDragEventAsync( @@ -115,8 +119,8 @@ await _domTunnel.DispatchDomElementDragEventAsync( dataTransferObjectVariableName); await _page.Mouse.MoveAsync( - (int)Math.Ceiling(to.BoundingClientRectangle.Left) + toOffsetX, - (int)Math.Ceiling(to.BoundingClientRectangle.Top) + toOffsetY); + (to.BoundingClientRectangle.Left + toOffsetX) ?? to.BoundingClientRectangle.Center.X, + (to.BoundingClientRectangle.Top + toOffsetY) ?? to.BoundingClientRectangle.Center.Y); await _domTunnel.DispatchDomElementDragEventAsync( this, @@ -173,13 +177,30 @@ public async Task EnterTextInAsync(IReadOnlyList elements, string t public async Task EvaluateJavaScriptExpressionAsync(string code) { - var blob = await _page.EvaluateExpressionAsync(code); - return blob?.ToString(); + while (true) + { + try + { + while (IsNavigating) + await Task.Delay(100); + + var blob = await _page.EvaluateExpressionAsync(code); + return blob?.ToString(); + } + catch (PuppeteerException) + { + if(!IsNavigating) + throw; + } + } } - public Task> FindDomElementsBySelectorAsync(int methodChainOffset, string selector) + public async Task> FindDomElementsBySelectorAsync(int methodChainOffset, string selector) { - return _domTunnel.GetDomElementsFromSelector(this, + if (_isDisposed) + return new List(); + + return await _domTunnel.GetDomElementsFromSelector(this, methodChainOffset, selector); } @@ -204,11 +225,11 @@ private async Task GetElementHandleFromDomElementAsync(IDomElemen return await _page.QuerySelectorAsync(domElement.CssSelector); } - public async Task HoverAsync(IDomElement domElement, int offsetX, int offsetY) + public async Task HoverAsync(IDomElement domElement, int? offsetX, int? offsetY) { await _page.Mouse.MoveAsync( - (int)Math.Ceiling(domElement.BoundingClientRectangle.Left) + offsetX, - (int)Math.Ceiling(domElement.BoundingClientRectangle.Top) + offsetY); + (domElement.BoundingClientRectangle.Left + offsetX) ?? domElement.BoundingClientRectangle.Center.X, + (domElement.BoundingClientRectangle.Top + offsetY) ?? domElement.BoundingClientRectangle.Center.Y); } public async Task InitializeAsync() @@ -246,13 +267,13 @@ public async Task OpenAsync(string uri) await _page.GoToAsync(uri); } - public async Task RightClickAsync(IReadOnlyList elements, int offsetX, int offsetY) + public async Task RightClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY) { foreach (var element in elements) { await _page.Mouse.ClickAsync( - (int)Math.Ceiling(element.BoundingClientRectangle.Left) + offsetX, - (int)Math.Ceiling(element.BoundingClientRectangle.Top) + offsetY, + (element.BoundingClientRectangle.Left + offsetX) ?? element.BoundingClientRectangle.Center.X, + (element.BoundingClientRectangle.Top + offsetY) ?? element.BoundingClientRectangle.Center.Y, new ClickOptions() { Button = MouseButton.Right diff --git a/src/FluffySpoon.Automation.Web.Sample/Program.cs b/src/FluffySpoon.Automation.Web.Sample/Program.cs index 22a9617..4f85996 100644 --- a/src/FluffySpoon.Automation.Web.Sample/Program.cs +++ b/src/FluffySpoon.Automation.Web.Sample/Program.cs @@ -28,6 +28,7 @@ static async Task Main(string[] args) serviceCollection.AddPuppeteerWebAutomationFrameworkInstance(GetPuppeteerDriverAsync); serviceCollection.AddSeleniumWebAutomationFrameworkInstance(GetFirefoxDriverAsync); + //serviceCollection.AddSeleniumWebAutomationFrameworkInstance(GetEdgeDriverAsync); var serviceProvider = serviceCollection.BuildServiceProvider(); var automationEngine = serviceProvider.GetRequiredService(); @@ -41,10 +42,10 @@ await automationEngine await automationEngine .Enter("this is a very long test that works").In("input[type=text]:visible") .Wait(until => - until.Exists("input[type=submit]:visible")); + until.Exists("input[name=btnK][type=submit]:visible")); var elements = await automationEngine - .Click.On("input[type=submit]:visible:first") + .Click.On("input[name=btnK][type=submit]:visible:first") .Wait(until => until.Exists("#rso .g:visible")) .Expect @@ -78,7 +79,7 @@ private static async Task GetPuppeteerDriverAsync() DefaultViewport = new ViewPortOptions() { Width = 1100, - Height = 500 + Height = 700 } }); } diff --git a/src/FluffySpoon.Automation.Web.Selenium/SeleniumWebAutomationFrameworkInstance.cs b/src/FluffySpoon.Automation.Web.Selenium/SeleniumWebAutomationFrameworkInstance.cs index 6d2f5a4..7872e92 100644 --- a/src/FluffySpoon.Automation.Web.Selenium/SeleniumWebAutomationFrameworkInstance.cs +++ b/src/FluffySpoon.Automation.Web.Selenium/SeleniumWebAutomationFrameworkInstance.cs @@ -14,185 +14,196 @@ namespace FluffySpoon.Automation.Web.Selenium { - class SeleniumWebAutomationFrameworkInstance : IWebAutomationFrameworkInstance - { - private EventFiringWebDriver _driver; + class SeleniumWebAutomationFrameworkInstance : IWebAutomationFrameworkInstance + { + private EventFiringWebDriver _driver; - private readonly SemaphoreSlim _semaphore; + private readonly SemaphoreSlim _semaphore; - private readonly Func> _driverConstructor; + private readonly Func> _driverConstructor; - private readonly IJavaScriptTunnel _domTunnel; + private readonly IJavaScriptTunnel _domTunnel; - private Actions Actions => new Actions(_driver); + private Actions Actions => new Actions(_driver); - public string UserAgentName { get; private set; } + public string UserAgentName { get; private set; } - public bool IsNavigating { get; private set; } + public bool IsNavigating { get; private set; } - public SeleniumWebAutomationFrameworkInstance( - Func> driverConstructor, - IJavaScriptTunnel domTunnel) - { - _driverConstructor = driverConstructor; - _domTunnel = domTunnel; + public SeleniumWebAutomationFrameworkInstance( + Func> driverConstructor, + IJavaScriptTunnel domTunnel) + { + _driverConstructor = driverConstructor; + _domTunnel = domTunnel; - _semaphore = new SemaphoreSlim(1); - } + _semaphore = new SemaphoreSlim(1); + } - public Task FocusAsync(IDomElement domElement) - { - var nativeElement = GetWebDriverElementsFromDomElements(new[] { domElement }).Single(); - GetScriptExecutor().ExecuteScript("arguments[0].focus();", nativeElement); + public Task FocusAsync(IDomElement domElement) + { + var nativeElement = GetWebDriverElementsFromDomElements(new[] { domElement }).Single(); + GetScriptExecutor().ExecuteScript("arguments[0].focus();", nativeElement); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task DragDropAsync(IDomElement from, int fromOffsetX, int fromOffsetY, IDomElement to, int toOffsetX, int toOffsetY) - { - var nativeElements = GetWebDriverElementsFromDomElements(new[] { from, to }); - var nativeFromElement = nativeElements[0]; - var nativeToElement = nativeElements[1]; + public Task DragDropAsync(IDomElement from, int? fromOffsetX, int? fromOffsetY, IDomElement to, int? toOffsetX, int? toOffsetY) + { + var nativeElements = GetWebDriverElementsFromDomElements(new[] { from, to }); + var nativeFromElement = nativeElements[0]; + var nativeToElement = nativeElements[1]; Actions .MoveToElement( nativeFromElement, - fromOffsetX, - fromOffsetY) + (int)Math.Ceiling(fromOffsetX ?? from.BoundingClientRectangle.RelativeCenter.X), + (int)Math.Ceiling(fromOffsetY ?? from.BoundingClientRectangle.RelativeCenter.Y)) .ClickAndHold() .MoveToElement( nativeToElement, - toOffsetX, - toOffsetY) + (int)Math.Ceiling(toOffsetX ?? to.BoundingClientRectangle.RelativeCenter.X), + (int)Math.Ceiling(toOffsetY ?? to.BoundingClientRectangle.RelativeCenter.Y)) .Release() .Build() .Perform(); return Task.CompletedTask; - } - - public async Task HoverAsync(IDomElement element, int relativeX, int relativeY) - { - await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.MoveToElement(b), new[] { element }, relativeX, relativeY); - } - - public async Task ClickAsync(IReadOnlyList elements, int relativeX, int relativeY) - { - await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.Click(b), elements, relativeX, relativeY); - } - - public async Task DoubleClickAsync(IReadOnlyList elements, int relativeX, int relativeY) - { - await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.DoubleClick(b), elements, relativeX, relativeY); - } - - public async Task RightClickAsync(IReadOnlyList elements, int relativeX, int relativeY) - { - await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.ContextClick(b), elements, relativeX, relativeY); - } - - public Task DisposeAsync() - { - _driver?.Quit(); - _driver?.Dispose(); - - return Task.CompletedTask; - } - - public Task EnterTextInAsync(IReadOnlyList elements, string text) - { - var nativeElements = GetWebDriverElementsFromDomElements(elements); - foreach (var nativeElement in nativeElements) - { - nativeElement.Clear(); - nativeElement.SendKeys(text); - } - - return Task.CompletedTask; - } - - public Task EvaluateJavaScriptExpressionAsync(string code) - { - var scriptExecutor = GetScriptExecutor(); - var result = scriptExecutor.ExecuteScript("return " + code.Trim()); - - return Task.FromResult(result?.ToString()); - } - - public async Task> FindDomElementsBySelectorAsync( - int methodChainOffset, - string selector) - { - return await _domTunnel.GetDomElementsFromSelector(this, - methodChainOffset, - selector); - } - - public async Task OpenAsync(string uri) - { - await _semaphore.WaitAsync(); - - var navigatedWaitHandle = new SemaphoreSlim(0); - - async void DriverNavigated(object sender, WebDriverNavigationEventArgs e) - { - if (e.Url != uri) - return; - - _driver.Navigated -= DriverNavigated; - - while (GetReadyState() != "complete") - await Task.Delay(100); - - navigatedWaitHandle.Release(1); - } - - _driver.Navigated += DriverNavigated; - _driver.Navigate().GoToUrl(uri); - - await navigatedWaitHandle.WaitAsync(); - _semaphore.Release(); - } - - private string GetReadyState() - { - return _driver.ExecuteScript("return document.readyState")?.ToString(); - } - - private ITakesScreenshot GetScreenshotDriver() - { - if (!(_driver is ITakesScreenshot screenshotDriver)) - throw new InvalidOperationException("The given Selenium web driver does not support taking screenshots."); - - return screenshotDriver; - } - - private IJavaScriptExecutor GetScriptExecutor() - { - if (!(_driver is IJavaScriptExecutor scriptExecutor)) - throw new InvalidOperationException("The given Selenium web driver does not support JavaScript execution."); - - return scriptExecutor; - } - - private IWebElement[] GetWebDriverElementsFromDomElements(IReadOnlyList domElements) - { - var selector = domElements - .Select(x => x.CssSelector) - .Aggregate((a, b) => $"{a}, {b}"); - - return _driver - .FindElements(By.CssSelector(selector)) - .ToArray(); - } - - public async Task TakeScreenshotAsync() - { - var currentDriverDimensions = _driver.Manage().Window.Size; - - try - { - var bodyDimensionsBlob = await EvaluateJavaScriptExpressionAsync(@" + } + + public async Task HoverAsync(IDomElement element, int? relativeX, int? relativeY) + { + await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.MoveToElement(b), new[] { element }, relativeX, relativeY); + } + + public async Task ClickAsync(IReadOnlyList elements, int? relativeX, int? relativeY) + { + await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.Click(b), elements, relativeX, relativeY); + } + + public async Task DoubleClickAsync(IReadOnlyList elements, int? relativeX, int? relativeY) + { + await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.DoubleClick(b), elements, relativeX, relativeY); + } + + public async Task RightClickAsync(IReadOnlyList elements, int? relativeX, int? relativeY) + { + await PerformMouseOperationOnElementCoordinatesAsync((a, b) => a.ContextClick(b), elements, relativeX, relativeY); + } + + public Task DisposeAsync() + { + _driver?.Quit(); + _driver?.Dispose(); + + return Task.CompletedTask; + } + + public Task EnterTextInAsync(IReadOnlyList elements, string text) + { + var nativeElements = GetWebDriverElementsFromDomElements(elements); + foreach (var nativeElement in nativeElements) + { + nativeElement.Clear(); + nativeElement.SendKeys(text); + } + + return Task.CompletedTask; + } + + public async Task EvaluateJavaScriptExpressionAsync(string code) + { + while (true) + { + try + { + var scriptExecutor = GetScriptExecutor(); + var result = scriptExecutor.ExecuteScript("return " + code.Trim()); + + return result?.ToString(); + } + catch (WebDriverException) + { + if (!IsNavigating) + throw; + } + } + } + + public async Task> FindDomElementsBySelectorAsync( + int methodChainOffset, + string selector) + { + return await _domTunnel.GetDomElementsFromSelector(this, + methodChainOffset, + selector); + } + + public async Task OpenAsync(string uri) + { + await _semaphore.WaitAsync(); + + var navigatedWaitHandle = new SemaphoreSlim(0); + + async void DriverNavigated(object sender, WebDriverNavigationEventArgs e) + { + if (e.Url != uri) + return; + + _driver.Navigated -= DriverNavigated; + + while (GetReadyState() != "complete") + await Task.Delay(100); + + navigatedWaitHandle.Release(1); + } + + _driver.Navigated += DriverNavigated; + _driver.Navigate().GoToUrl(uri); + + await navigatedWaitHandle.WaitAsync(); + _semaphore.Release(); + } + + private string GetReadyState() + { + return _driver.ExecuteScript("return document.readyState")?.ToString(); + } + + private ITakesScreenshot GetScreenshotDriver() + { + if (!(_driver is ITakesScreenshot screenshotDriver)) + throw new InvalidOperationException("The given Selenium web driver does not support taking screenshots."); + + return screenshotDriver; + } + + private IJavaScriptExecutor GetScriptExecutor() + { + if (!(_driver is IJavaScriptExecutor scriptExecutor)) + throw new InvalidOperationException("The given Selenium web driver does not support JavaScript execution."); + + return scriptExecutor; + } + + private IWebElement[] GetWebDriverElementsFromDomElements(IReadOnlyList domElements) + { + var selector = domElements + .Select(x => x.CssSelector) + .Aggregate((a, b) => $"{a}, {b}"); + + return _driver + .FindElements(By.CssSelector(selector)) + .ToArray(); + } + + public async Task TakeScreenshotAsync() + { + var currentDriverDimensions = _driver.Manage().Window.Size; + + try + { + var bodyDimensionsBlob = await EvaluateJavaScriptExpressionAsync(@" JSON.stringify({ document: { width: Math.max( @@ -214,15 +225,15 @@ public async Task TakeScreenshotAsync() } }); "); - var bodyDimensions = JsonConvert.DeserializeObject(bodyDimensionsBlob); + var bodyDimensions = JsonConvert.DeserializeObject(bodyDimensionsBlob); - var newDriverDimensions = new Size() - { - Width = bodyDimensions.Document.Width + (currentDriverDimensions.Width - bodyDimensions.Window.Width), - Height = bodyDimensions.Document.Height + (currentDriverDimensions.Height - bodyDimensions.Window.Height) - }; + var newDriverDimensions = new Size() + { + Width = bodyDimensions.Document.Width + (currentDriverDimensions.Width - bodyDimensions.Window.Width), + Height = bodyDimensions.Document.Height + (currentDriverDimensions.Height - bodyDimensions.Window.Height) + }; - _driver.Manage().Window.Size = newDriverDimensions; + _driver.Manage().Window.Size = newDriverDimensions; var screenshotDriver = GetScreenshotDriver(); var screenshot = screenshotDriver.GetScreenshot(); @@ -236,115 +247,123 @@ public async Task TakeScreenshotAsync() SKFilterQuality.High); } } - finally - { - _driver.Manage().Window.Size = currentDriverDimensions; - } - } - - private async Task PerformMouseOperationOnElementCoordinatesAsync( - Func operation, - IReadOnlyList elements, - int relativeX, - int relativeY) - { - var nativeElements = GetWebDriverElementsFromDomElements(elements); - foreach (var nativeElement in nativeElements) - { - operation(Actions.MoveToElement(nativeElement, relativeX, relativeY), nativeElement) - .Build() - .Perform(); - } - } - - private void SelectAsync(IReadOnlyList elements, Action action) - { - var nativeElements = GetWebDriverElementsFromDomElements(elements); - foreach (var nativeElement in nativeElements) - { - var selectElement = new SelectElement(nativeElement); - if (selectElement.IsMultiple) - selectElement.DeselectAll(); - - action(selectElement); - } - } - - public Task SelectByIndicesAsync(IReadOnlyList elements, int[] indices) - { - SelectAsync(elements, selectElement => - { - foreach (var index in indices) - { - selectElement.SelectByIndex(index); - } - }); - return Task.CompletedTask; - } - - public Task SelectByTextsAsync(IReadOnlyList elements, string[] texts) - { - SelectAsync(elements, selectElement => - { - foreach (var text in texts) - { - selectElement.SelectByText(text); - } - }); - return Task.CompletedTask; - } - - public Task SelectByValuesAsync(IReadOnlyList elements, string[] values) - { - SelectAsync(elements, selectElement => - { - foreach (var value in values) - { - selectElement.SelectByValue(value); - } - }); - return Task.CompletedTask; - } - - public async Task> FindDomElementsByCssSelectorsAsync( - int methodChainOffset, - string[] selectors) - { - return await _domTunnel.FindDomElementsByCssSelectorsAsync(this, - methodChainOffset, - selectors); - } - - public async Task InitializeAsync() - { - var driver = await _driverConstructor(); - UserAgentName = driver.GetType().Name; - - _driver = new EventFiringWebDriver(driver); - _driver.Navigating += DriverNavigating; - _driver.Navigated += DriverNavigated; - } - - private void DriverNavigated(object sender, WebDriverNavigationEventArgs e) - { - IsNavigating = false; - } - - private void DriverNavigating(object sender, WebDriverNavigationEventArgs e) - { - IsNavigating = true; - } - - private class DimensionsWrapper - { - public int Width { get; set; } - public int Height { get; set; } - } - - private class GlobalDimensionsWrapper - { - public DimensionsWrapper Window { get; set; } - public DimensionsWrapper Document { get; set; } - } - } + finally + { + _driver.Manage().Window.Size = currentDriverDimensions; + } + } + + private async Task PerformMouseOperationOnElementCoordinatesAsync( + Func operation, + IReadOnlyList elements, + int? relativeX, + int? relativeY) + { + var nativeElements = GetWebDriverElementsFromDomElements(elements); + for (int i = 0; i < nativeElements.Length; i++) + { + var nativeElement = nativeElements[i]; + var element = elements[i]; + + operation( + Actions.MoveToElement( + nativeElement, + (int)Math.Ceiling(relativeX ?? element.BoundingClientRectangle.RelativeCenter.X), + (int)Math.Ceiling(relativeY ?? element.BoundingClientRectangle.RelativeCenter.Y)), + nativeElement) + .Build() + .Perform(); + } + } + + private void SelectAsync(IReadOnlyList elements, Action action) + { + var nativeElements = GetWebDriverElementsFromDomElements(elements); + foreach (var nativeElement in nativeElements) + { + var selectElement = new SelectElement(nativeElement); + if (selectElement.IsMultiple) + selectElement.DeselectAll(); + + action(selectElement); + } + } + + public Task SelectByIndicesAsync(IReadOnlyList elements, int[] indices) + { + SelectAsync(elements, selectElement => + { + foreach (var index in indices) + { + selectElement.SelectByIndex(index); + } + }); + return Task.CompletedTask; + } + + public Task SelectByTextsAsync(IReadOnlyList elements, string[] texts) + { + SelectAsync(elements, selectElement => + { + foreach (var text in texts) + { + selectElement.SelectByText(text); + } + }); + return Task.CompletedTask; + } + + public Task SelectByValuesAsync(IReadOnlyList elements, string[] values) + { + SelectAsync(elements, selectElement => + { + foreach (var value in values) + { + selectElement.SelectByValue(value); + } + }); + return Task.CompletedTask; + } + + public async Task> FindDomElementsByCssSelectorsAsync( + int methodChainOffset, + string[] selectors) + { + return await _domTunnel.FindDomElementsByCssSelectorsAsync(this, + methodChainOffset, + selectors); + } + + public async Task InitializeAsync() + { + var driver = await _driverConstructor(); + UserAgentName = driver.GetType().Name; + + _driver = new EventFiringWebDriver(driver); + _driver.Navigating += DriverNavigating; + _driver.Navigated += DriverNavigated; + } + + private void DriverNavigated(object sender, WebDriverNavigationEventArgs e) + { + IsNavigating = false; + } + + private void DriverNavigating(object sender, WebDriverNavigationEventArgs e) + { + IsNavigating = true; + } + + private class DimensionsWrapper + { + public int Width { get; set; } + public int Height { get; set; } + } + + private class GlobalDimensionsWrapper + { + public DimensionsWrapper Window { get; set; } + public DimensionsWrapper Document { get; set; } + } + } } diff --git a/src/FluffySpoon.Automation.Web/Dom/DomRectangle.cs b/src/FluffySpoon.Automation.Web/Dom/DomRectangle.cs index 9b7830a..297216f 100644 --- a/src/FluffySpoon.Automation.Web/Dom/DomRectangle.cs +++ b/src/FluffySpoon.Automation.Web/Dom/DomRectangle.cs @@ -1,12 +1,26 @@ namespace FluffySpoon.Automation.Web.Dom { - public class DomRectangle : IDomRectangle + public class DomCoordinate : IDomCoordinate + { + public decimal X { get; } + public decimal Y { get; } + + public DomCoordinate( + decimal x, + decimal y) + { + X = x; + Y = y; + } + } + + public class DomRectangle : IDomRectangle { public DomRectangle( - double left, - double top, - double right, - double bottom) + decimal left, + decimal top, + decimal right, + decimal bottom) { Left = left; Top = top; @@ -14,16 +28,24 @@ public DomRectangle( Bottom = bottom; } - public double Left { get; } - public double Top { get; } + public decimal Left { get; } + public decimal Top { get; } + + public decimal Right { get; } + public decimal Bottom { get; } + + public decimal X => Left; + public decimal Y => Top; - public double Right { get; } - public double Bottom { get; } + public decimal Width => Right - Left; + public decimal Height => Bottom - Top; - public double X => Left; - public double Y => Top; + public IDomCoordinate RelativeCenter => new DomCoordinate( + Width / 2, + Height / 2); - public double Width => Right - Left; - public double Height => Bottom - Top; - } + public IDomCoordinate Center => new DomCoordinate( + Left + Width / 2, + Top + Height / 2); + } } diff --git a/src/FluffySpoon.Automation.Web/Dom/IDomElement.cs b/src/FluffySpoon.Automation.Web/Dom/IDomElement.cs index 82869eb..c33504c 100644 --- a/src/FluffySpoon.Automation.Web/Dom/IDomElement.cs +++ b/src/FluffySpoon.Automation.Web/Dom/IDomElement.cs @@ -1,20 +1,20 @@ namespace FluffySpoon.Automation.Web.Dom { - public interface IDomElement - { - string CssSelector { get; } - string TextContent { get; } - string Value { get; } + public interface IDomElement + { + string CssSelector { get; } + string TextContent { get; } + string Value { get; } string TagName { get; } - int ClientLeft { get; } - int ClientTop { get; } - int ClientWidth { get; } - int ClientHeight { get; } + int ClientLeft { get; } + int ClientTop { get; } + int ClientWidth { get; } + int ClientHeight { get; } - IDomRectangle BoundingClientRectangle { get; } - IDomAttributes Attributes { get; } - IDomStyle ComputedStyle { get; } + IDomRectangle BoundingClientRectangle { get; } + IDomAttributes Attributes { get; } + IDomStyle ComputedStyle { get; } System.DateTime UpdatedAt { get; } } } \ No newline at end of file diff --git a/src/FluffySpoon.Automation.Web/Dom/IDomRectangle.cs b/src/FluffySpoon.Automation.Web/Dom/IDomRectangle.cs index 4dbb455..a0b892a 100644 --- a/src/FluffySpoon.Automation.Web/Dom/IDomRectangle.cs +++ b/src/FluffySpoon.Automation.Web/Dom/IDomRectangle.cs @@ -1,17 +1,23 @@ namespace FluffySpoon.Automation.Web.Dom { - public interface IDomRectangle + public interface IDomCoordinate + { + decimal X { get; } + decimal Y { get; } + } + + public interface IDomRectangle : IDomCoordinate { - double Left { get; } - double Top { get; } + decimal Left { get; } + decimal Top { get; } - double Right { get; } - double Bottom { get; } + decimal Right { get; } + decimal Bottom { get; } - double X { get; } - double Y { get; } + decimal Width { get; } + decimal Height { get; } - double Width { get; } - double Height { get; } - } + IDomCoordinate RelativeCenter { get; } + IDomCoordinate Center { get; } + } } diff --git a/src/FluffySpoon.Automation.Web/Fluent/BaseMethodChainNode.cs b/src/FluffySpoon.Automation.Web/Fluent/BaseMethodChainNode.cs index f2d6eb9..b844946 100644 --- a/src/FluffySpoon.Automation.Web/Fluent/BaseMethodChainNode.cs +++ b/src/FluffySpoon.Automation.Web/Fluent/BaseMethodChainNode.cs @@ -81,19 +81,13 @@ private async Task RefreshElements(IWebAutomationFrameworkInstance framework) if (Elements == null || Elements.Count <= 0) return; - try - { - var selectors = Elements - .Select(x => x.CssSelector) - .ToArray(); - var refreshedElements = await framework.FindDomElementsByCssSelectorsAsync( - MethodChainOffset, - selectors); - Elements = refreshedElements; - } catch(Exception) - { - //we ignore elements that may become detached. - } + var selectors = Elements + .Select(x => x.CssSelector) + .ToArray(); + var refreshedElements = await framework.FindDomElementsByCssSelectorsAsync( + MethodChainOffset, + selectors); + Elements = refreshedElements; } public void SetParent(IBaseMethodChainNode parent) diff --git a/src/FluffySpoon.Automation.Web/Fluent/Targets/BaseMouseTargetMethodChainNode.cs b/src/FluffySpoon.Automation.Web/Fluent/Targets/BaseMouseTargetMethodChainNode.cs index f91a3e8..b35d4f1 100644 --- a/src/FluffySpoon.Automation.Web/Fluent/Targets/BaseMouseTargetMethodChainNode.cs +++ b/src/FluffySpoon.Automation.Web/Fluent/Targets/BaseMouseTargetMethodChainNode.cs @@ -22,8 +22,8 @@ abstract class BaseMouseTargetMethodChainNode EvaluateJavaScriptExpressionAsync(string code); Task OpenAsync(string uri); - Task DragDropAsync(IDomElement from, int fromOffsetX, int fromOffsetY, IDomElement to, int toOffsetX, int toOffsetY); + Task DragDropAsync(IDomElement from, int? fromOffsetX, int? fromOffsetY, IDomElement to, int? toOffsetX, int? toOffsetY); Task FocusAsync(IDomElement domElement); Task EnterTextInAsync(IReadOnlyList elements, string text); - Task HoverAsync(IDomElement domElement, int offsetX, int offsetY); - Task ClickAsync(IReadOnlyList elements, int offsetX, int offsetY); + Task HoverAsync(IDomElement domElement, int? offsetX, int? offsetY); + Task ClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY); Task SelectByIndicesAsync(IReadOnlyList elements, int[] byIndices); Task SelectByTextsAsync(IReadOnlyList elements, string[] byTexts); Task SelectByValuesAsync(IReadOnlyList elements, string[] byValues); - Task RightClickAsync(IReadOnlyList elements, int offsetX, int offsetY); - Task DoubleClickAsync(IReadOnlyList elements, int offsetX, int offsetY); + Task RightClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY); + Task DoubleClickAsync(IReadOnlyList elements, int? offsetX, int? offsetY); Task TakeScreenshotAsync(); Task InitializeAsync();