diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0a44541 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = True + +# Default settings +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 + +# Xml project files +[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +[*.yml] +indent_size = 2 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..419b121 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased](https://github.com/maik-hasler/SeleniumSharper/compare/v1.1.0...HEAD) + +## [v1.1.0](https://github.com/maik-hasler/SeleniumSharper/releases/tag/v1.1.0) +**Published:** 21th April 2023 +### Added +- `IWebDriverManager` and first implementation for Chrome to automatically install driver binaries +- Helper classes related to `IWebDriverManager` +- Enum and extension method to fire JavaScript event +### Changed +- Upgrade dependencies: Selenium.WebDriver, Selenium.Support etc. +### Removed +- Custom result classes + +## [v1.0.0](https://github.com/maik-hasler/SeleniumSharper/releases/tag/v1.0.0) +**Published:** 19th April 2023 +### Added +- `WebElementsConditionBuilder` to build wait conditions for a `ReadOnlyCollection` +- `WebElementConditionBuilder` to build wait conditions for a `IWebElement` +- `ClassConditionBuilder` to build wait conditions for a `IEquatable` +- Custom result classes `WebElementsVisibilityResult` and `WebElementVisibilityResult` +### Changed +- Changed `Waiter` to `ContextualWait`, which supports more generic method chaining +### Removed +- Collection of commonly used selenium wait conditions + +## [v1.0.0-preview.0](https://github.com/maik-hasler/SeleniumSharper/releases/tag/v1.0.0-preview.0) +**Published:** 18th April 2023 +
+**Disclaimer:** This is a pre-release. It was published in order to verify, that the nuget.yml workflow works fine. +### Added +- `Waiter` to wait for a specified condition to be satisfied +- Collection of commonly used selenium wait conditions +- Extension method to create a `Waiter` object from an `ISearchContext` +- Various `IJavaScriptExecutor` extension methods diff --git a/src/ClassConditionBuilder.cs b/src/Conditions/StringConditionBuilder.cs similarity index 71% rename from src/ClassConditionBuilder.cs rename to src/Conditions/StringConditionBuilder.cs index ff47106..bd838e7 100644 --- a/src/ClassConditionBuilder.cs +++ b/src/Conditions/StringConditionBuilder.cs @@ -1,8 +1,8 @@ using OpenQA.Selenium; -namespace SeleniumSharper; +namespace SeleniumSharper.Conditions; -public class ClassConditionBuilder +public class StringConditionBuilder where TSearchContext : ISearchContext where TSearchResult : IEquatable { @@ -10,7 +10,7 @@ public class ClassConditionBuilder private readonly Func _action; - public ClassConditionBuilder(ContextualWait fluentWait, Func action) + public StringConditionBuilder(ContextualWait fluentWait, Func action) { _contextualWait = fluentWait; _action = action; diff --git a/src/WebElementConditionBuilder.cs b/src/Conditions/WebElementConditionBuilder.cs similarity index 66% rename from src/WebElementConditionBuilder.cs rename to src/Conditions/WebElementConditionBuilder.cs index b47b8f0..eb9ee49 100644 --- a/src/WebElementConditionBuilder.cs +++ b/src/Conditions/WebElementConditionBuilder.cs @@ -1,6 +1,6 @@ using OpenQA.Selenium; -namespace SeleniumSharper; +namespace SeleniumSharper.Conditions; public class WebElementConditionBuilder where TSearchContext : ISearchContext @@ -16,32 +16,24 @@ public WebElementConditionBuilder(ContextualWait fluentWait, Fun _action = action; } - public WebElementVisibilityResult IsVisible() + public IWebElement? IsVisible() { + IWebElement? webElement = null; + try { - IWebElement? webElement = null; - - var isDisplayed = _contextualWait.Wait.Until(ctx => + _contextualWait.Wait.Until(ctx => { webElement = _action.Invoke(ctx); return webElement.Displayed; }); - return new WebElementVisibilityResult - { - WebElement = webElement, - IsVisible = isDisplayed - }; + return webElement; } catch (WebDriverTimeoutException) { - return new WebElementVisibilityResult - { - WebElement = null, - IsVisible = false - }; + return null; } } diff --git a/src/WebElementsConditionBuilder.cs b/src/Conditions/WebElementsConditionBuilder.cs similarity index 66% rename from src/WebElementsConditionBuilder.cs rename to src/Conditions/WebElementsConditionBuilder.cs index b254d1c..d764c68 100644 --- a/src/WebElementsConditionBuilder.cs +++ b/src/Conditions/WebElementsConditionBuilder.cs @@ -1,8 +1,7 @@ using OpenQA.Selenium; -using SeleniumSharper.Models; using System.Collections.ObjectModel; -namespace SeleniumSharper; +namespace SeleniumSharper.Conditions; public sealed class WebElementsConditionBuilder where TSearchContext : ISearchContext @@ -18,32 +17,24 @@ public WebElementsConditionBuilder(ContextualWait fluentWait, Fu _action = action; } - public WebElementsVisibilityResult AreVisible() + public ReadOnlyCollection? AreVisible() { + ReadOnlyCollection? webElements = null; + try { - ReadOnlyCollection? webElements = null; - - var areDisplayed = _contextualWait.Wait.Until(ctx => + _contextualWait.Wait.Until(ctx => { webElements = _action.Invoke(ctx); return webElements.All(e => e.Displayed); }); - return new WebElementsVisibilityResult - { - WebElements = webElements, - AreDisplayed = areDisplayed - }; + return webElements; } catch (WebDriverTimeoutException) { - return new WebElementsVisibilityResult - { - WebElements = null, - AreDisplayed = false - }; + return null; } } diff --git a/src/ContextualWait.cs b/src/ContextualWait.cs index 20a46e3..279061f 100644 --- a/src/ContextualWait.cs +++ b/src/ContextualWait.cs @@ -1,5 +1,6 @@ using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; +using SeleniumSharper.Conditions; namespace SeleniumSharper; @@ -27,8 +28,8 @@ public ContextualWait(TSearchContext searchContext, TimeSpan timeout) return new WebElementConditionBuilder(this, action); } - public ClassConditionBuilder Until(Func action) + public StringConditionBuilder Until(Func action) { - return new ClassConditionBuilder(this, action); + return new StringConditionBuilder(this, action); } } \ No newline at end of file diff --git a/src/DomEvent.cs b/src/DomEvent.cs new file mode 100644 index 0000000..da760af --- /dev/null +++ b/src/DomEvent.cs @@ -0,0 +1,11 @@ +namespace SeleniumSharper; + +public enum DomEvent +{ + Click, + DoubleClick, + MouseDown, + MouseUp, + KeyDown, + KeyUp +} \ No newline at end of file diff --git a/src/JavaScriptExecutorExtensions.cs b/src/JavaScriptExecutorExtensions.cs index 517edba..8db9281 100644 --- a/src/JavaScriptExecutorExtensions.cs +++ b/src/JavaScriptExecutorExtensions.cs @@ -8,4 +8,11 @@ public static void Click(this IJavaScriptExecutor executor, IWebElement elementT { executor.ExecuteScript("arguments[0].click();", elementToBeClicked); } + + public static void DispatchEvent(this IJavaScriptExecutor executor, IWebElement webElement, DomEvent domEvent) + { + var domEventName = domEvent.ToString().ToLower(); + + executor.ExecuteScript("[0].dispatchEvent(new Event('[1]'));", webElement, domEventName); + } } \ No newline at end of file diff --git a/src/Managers/ChromeDriverManager.cs b/src/Managers/ChromeDriverManager.cs new file mode 100644 index 0000000..ecb1f0d --- /dev/null +++ b/src/Managers/ChromeDriverManager.cs @@ -0,0 +1,101 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using SeleniumSharper.Managers.Interfaces; +using SeleniumSharper.Managers.Services; +using System.Runtime.InteropServices; + +namespace SeleniumSharper.Managers; + +public sealed class ChromeDriverManager : IWebDriverManager +{ + public IWebDriver Setup() + { + var driverPath = InstallBinary(); + + var chromeDriverService = ChromeDriverService.CreateDefaultService(driverPath); + + return new ChromeDriver(chromeDriverService); + } + + public IWebDriver Setup(ChromeOptions chromeOptions) + { + var driverPath = InstallBinary(); + + var chromeDriverService = ChromeDriverService.CreateDefaultService(driverPath); + + return new ChromeDriver(chromeDriverService, chromeOptions); + } + + private static string GetBinaryName() + { + var suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; + + return $"chromedriver{suffix}"; + } + + private static Uri GetDownloadUrl(string version, string fileName) + { + var url = $"https://chromedriver.storage.googleapis.com/{version}/{fileName}"; + + return new Uri(url); + } + + private static string GetFileName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var architectureExtension = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "_arm64" : "64"; + + return $"chromedriver_mac{architectureExtension}.zip"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "chromedriver_linux64.zip"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "chromedriver_win32.zip"; + } + + throw new PlatformNotSupportedException(); + } + + private static string GetLatestVersion() + { + var url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"; + + using var httpClient = new HttpClient(); + + var response = httpClient.GetStringAsync(url).Result; + + return response.Trim(); + } + + private static string GetBinaryPath(string version) + { + var architecture = Environment.Is64BitOperatingSystem ? "64" : "32"; + + return Path.Combine( + Directory.GetCurrentDirectory(), + "Binaries", + "Chrome", + version, + architecture, + GetBinaryName()); + } + + private static string InstallBinary() + { + var version = GetLatestVersion(); + + var fileName = GetFileName(); + + var downloadUrl = GetDownloadUrl(version, fileName); + + var binaryPath = GetBinaryPath(version); + + return WebDriverManagerUtils.InstallBinary(fileName, downloadUrl, binaryPath, GetBinaryName()); + } +} diff --git a/src/Managers/Interfaces/IWebDriverManager.cs b/src/Managers/Interfaces/IWebDriverManager.cs new file mode 100644 index 0000000..4a6a41a --- /dev/null +++ b/src/Managers/Interfaces/IWebDriverManager.cs @@ -0,0 +1,11 @@ +using OpenQA.Selenium; + +namespace SeleniumSharper.Managers.Interfaces; + +public interface IWebDriverManager + where TOptions : DriverOptions +{ + public IWebDriver Setup(); + + public IWebDriver Setup(TOptions options); +} diff --git a/src/Managers/Services/WebDriverManagerUtils.cs b/src/Managers/Services/WebDriverManagerUtils.cs new file mode 100644 index 0000000..a89bde1 --- /dev/null +++ b/src/Managers/Services/WebDriverManagerUtils.cs @@ -0,0 +1,88 @@ +using SharpCompress.Archives; +using SharpCompress.Common; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SeleniumSharper.Managers.Services; + +public static class WebDriverManagerUtils +{ + public static string InstallBinary(string fileName, Uri downloadUrl, string binaryPath, string binaryName) + { + var temporaryDirectory = WebDriverManagerUtils.CreateTemporaryDirectory(); + + var archivePath = Path.Combine(temporaryDirectory.FullName, fileName); + + WebDriverManagerUtils.DownloadBinary(downloadUrl, archivePath); + + Directory.CreateDirectory(binaryPath); + + WebDriverManagerUtils.ExtractArchive(archivePath, binaryPath, binaryName); + + temporaryDirectory.Delete(true); + + WebDriverManagerUtils.GrantPermissionsIfNeeded(binaryPath); + + return binaryPath; + } + + public static void ExtractArchive(string archivePath, string destinationPath, string binaryName) + { + var suffix = Path.GetExtension(archivePath); + + if (suffix.Equals(".exe", StringComparison.OrdinalIgnoreCase)) + { + File.Copy(archivePath, destinationPath); + + return; + } + + if (suffix.Equals(".zip", StringComparison.OrdinalIgnoreCase) || suffix.Equals(".tar.gz", StringComparison.OrdinalIgnoreCase)) + { + using var archive = ArchiveFactory.Open(archivePath); + + foreach (var entry in archive.Entries.Where(entry => Path.GetFileName(entry.Key).Equals(binaryName, StringComparison.OrdinalIgnoreCase))) + { + entry.WriteToDirectory(destinationPath, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + } + + public static void GrantPermissionsIfNeeded(string binaryPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var chmod = Process.Start("chmod", $"+x {binaryPath}"); + + chmod?.WaitForExit(); + } + } + + public static DirectoryInfo CreateTemporaryDirectory() + { + var temporaryPath = Path.Combine( + Path.GetTempPath(), + Guid.NewGuid().ToString()); + + return Directory.CreateDirectory(temporaryPath); + } + + public static void DownloadBinary(Uri uri, string path) + { + using var httpClient = new HttpClient(); + + var response = httpClient.GetAsync(uri).Result; + + response.EnsureSuccessStatusCode(); + + using var stream = response.Content.ReadAsStreamAsync().Result; + + using var fileStream = new FileStream(path, FileMode.Create); + + stream.CopyTo(fileStream); + } +} diff --git a/src/Models/WebElementVisibilityResult.cs b/src/Models/WebElementVisibilityResult.cs deleted file mode 100644 index 3192853..0000000 --- a/src/Models/WebElementVisibilityResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -using OpenQA.Selenium; - -namespace SeleniumSharper; - -public sealed class WebElementVisibilityResult -{ - public IWebElement? WebElement { get; set; } - - public bool IsVisible { get; set; } -} \ No newline at end of file diff --git a/src/Models/WebElementsVisibilityResult.cs b/src/Models/WebElementsVisibilityResult.cs deleted file mode 100644 index 47d6b0a..0000000 --- a/src/Models/WebElementsVisibilityResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using OpenQA.Selenium; -using System.Collections.ObjectModel; - -namespace SeleniumSharper.Models; - -public sealed class WebElementsVisibilityResult -{ - public ReadOnlyCollection? WebElements { get; set; } - - public bool AreDisplayed { get; set; } -} \ No newline at end of file diff --git a/src/SeleniumSharper.csproj b/src/SeleniumSharper.csproj index 8c394d4..bffbee9 100644 --- a/src/SeleniumSharper.csproj +++ b/src/SeleniumSharper.csproj @@ -7,8 +7,18 @@ - - + + + + + maik-hasler + git + https://github.com/maik-hasler/SeleniumSharper + https://github.com/maik-hasler/SeleniumSharper + SeleniumSharper is a powerful, lightweight tool that makes working with Selenium much easier. By offering a set of useful extensions and tools, it enhances the experience of working with Selenium. + SeleniumSharper + + diff --git a/tests/ContextualWaitTests.cs b/tests/ContextualWaitTests.cs index de91644..fc3d174 100644 --- a/tests/ContextualWaitTests.cs +++ b/tests/ContextualWaitTests.cs @@ -1,15 +1,16 @@ using FluentAssertions; using Moq; using OpenQA.Selenium; +using SeleniumSharper.Conditions; using Xunit; namespace SeleniumSharper.Test; public sealed class ContextualWaitTests { - private Mock _searchContext; + private readonly Mock _searchContext; - private TimeSpan _timeout; + private readonly TimeSpan _timeout; public ContextualWaitTests() { @@ -42,6 +43,6 @@ public void Until_ReturnsClassConditionBuilder() // Assert result.Should().NotBeNull(); - result.Should().BeOfType>(); + result.Should().BeOfType>(); } -} \ No newline at end of file +} diff --git a/tests/SeleniumSharper.Test.csproj b/tests/SeleniumSharper.Test.csproj index 8baa0a6..1d7cb67 100644 --- a/tests/SeleniumSharper.Test.csproj +++ b/tests/SeleniumSharper.Test.csproj @@ -1,10 +1,9 @@ - + net7.0 enable enable - false true @@ -12,18 +11,12 @@ - - + + - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + diff --git a/tests/ClassConditionBuilderTests.cs b/tests/StringConditionBuilderTests.cs similarity index 81% rename from tests/ClassConditionBuilderTests.cs rename to tests/StringConditionBuilderTests.cs index b27b9cb..9735876 100644 --- a/tests/ClassConditionBuilderTests.cs +++ b/tests/StringConditionBuilderTests.cs @@ -1,16 +1,16 @@ -using Moq; +using FluentAssertions; +using Moq; using OpenQA.Selenium; +using SeleniumSharper.Conditions; using Xunit; -using FluentAssertions; -using SeleniumSharper; -namespace Selenium.Sharp.Test; +namespace SeleniumSharper.Test; -public sealed class ClassConditionBuilderTests +public sealed class StringConditionBuilderTests { private readonly Mock _webDriver; - public ClassConditionBuilderTests() + public StringConditionBuilderTests() { _webDriver = new Mock(); } @@ -25,7 +25,7 @@ public void Satisfies_ShouldReturnTrue_WhenConditionIsSatisfied() { return expectedValue; }); - var builder = new ClassConditionBuilder(fluentWait, action); + var builder = new StringConditionBuilder(fluentWait, action); // Act var result = builder.Satisfies(value => value.Equals(expectedValue)); @@ -44,7 +44,7 @@ public void Satisfies_ShouldThrowTimeoutException_WhenConditionIsNotSatisfied() { return "Another Value"; }); - var builder = new ClassConditionBuilder(fluentWait, action); + var builder = new StringConditionBuilder(fluentWait, action); // Act & Assert Assert.Throws(() => @@ -65,7 +65,7 @@ public void Satisfies_ShouldReturnTrue_WhenConditionBecomesSatisfied() return titleAccessCount == 1 ? "Wrong Title" : expectedTitle; }); - var builder = new ClassConditionBuilder( + var builder = new StringConditionBuilder( new ContextualWait(_webDriver.Object, TimeSpan.FromSeconds(30)), context => context.Title ); diff --git a/tests/WebElementConditionBuilderTests.cs b/tests/WebElementConditionBuilderTests.cs index f04612b..f85f333 100644 --- a/tests/WebElementConditionBuilderTests.cs +++ b/tests/WebElementConditionBuilderTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using Moq; using OpenQA.Selenium; -using Selenium.Sharp; +using SeleniumSharper.Conditions; using Xunit; namespace SeleniumSharper.Test; @@ -28,7 +28,7 @@ public void IsVisible_ShouldReturnTrue_WhenElementIsDisplayed() var result = builder.IsVisible(); // Assert - result.IsVisible.Should().BeTrue(); + result.Should().NotBeNull(); } [Fact] @@ -44,7 +44,7 @@ public void IsVisible_ShouldReturnFalse_WhenElementIsNotDisplayed() var result = builder.IsVisible(); // Assert - result.IsVisible.Should().BeFalse(); + result.Should().BeNull(); } [Fact] @@ -62,7 +62,7 @@ public void IsVisible_ShouldReturnTrue_WhenElementBecomesDisplayed() var result = builder.IsVisible(); // Assert - result.IsVisible.Should().BeTrue(); + result.Should().NotBeNull(); element.VerifyGet(e => e.Displayed, Times.Exactly(2)); } } \ No newline at end of file diff --git a/tests/WebElementsConditionBuilder.cs b/tests/WebElementsConditionBuilder.cs index d94f96c..f8402e9 100644 --- a/tests/WebElementsConditionBuilder.cs +++ b/tests/WebElementsConditionBuilder.cs @@ -1,7 +1,7 @@ using FluentAssertions; using Moq; using OpenQA.Selenium; -using Selenium.Sharp; +using SeleniumSharper.Conditions; using System.Collections.ObjectModel; using Xunit; @@ -33,7 +33,7 @@ public void AreVisible_ShouldReturnTrue_WhenAllElementsAreDisplayed() var result = builder.AreVisible(); // Assert - result.AreDisplayed.Should().BeTrue(); + result.Should().NotBeNull(); } [Fact] @@ -53,7 +53,7 @@ public void AreVisible_ShouldReturnFalse_WhenAnyElementIsNotDisplayed() var result = builder.AreVisible(); // Assert - result.AreDisplayed.Should().BeFalse(); + result.Should().BeNull(); } [Fact] @@ -89,6 +89,6 @@ public void AreVisible_ShouldReturnTrue_WhenElementBecomesVisible() var result = builder.AreVisible(); // Assert - result.AreDisplayed.Should().BeTrue(); + result.Should().NotBeNull(); } } \ No newline at end of file