├── Build.ps1 ├── Coverage.ps1 ├── Version.ps1 ├── src └── Nito.BrowserBoss │ ├── linqpad-samples │ └── Basic Usage.linq │ ├── Loggers │ ├── NullLogger.cs │ ├── ConsoleLogger.cs │ └── ILogger.cs │ ├── Finders │ ├── FindByXPath.cs │ ├── FindByValue.cs │ ├── FindByText.cs │ ├── FindByNormalizeSpaceText.cs │ ├── IFind.cs │ ├── FindByLabel.cs │ ├── FindByJQueryCss.cs │ └── FindExtensions.cs │ ├── WebDrivers │ ├── IWebDriverSetup.cs │ ├── LocalDirectory.cs │ ├── FirefoxWebDriverSetup.cs │ ├── BrowserUtility.cs │ ├── InternetExplorerWebDriverSetup.cs │ ├── ChromeWebDriverSetup.cs │ └── WebDriverSetupBase.cs │ ├── EnumerableExtensions.cs │ ├── Nito.BrowserBoss.csproj │ ├── Utility.cs │ ├── Browser.cs │ ├── Element.cs │ ├── Boss.cs │ └── Session.cs ├── appveyor.yml ├── examples └── Example │ ├── App.config │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── Example.csproj ├── test └── UnitTests │ ├── UtilityUnitTests.cs │ └── UnitTests.csproj ├── LICENSE ├── Nito.BrowserBoss.sln ├── .gitattributes ├── .gitignore └── README.md /Build.ps1: -------------------------------------------------------------------------------- 1 | iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/StephenCleary/BuildTools/6efa9f040c0622d60dc167947b7d35b55d071f25/Build.ps1')) 2 | -------------------------------------------------------------------------------- /Coverage.ps1: -------------------------------------------------------------------------------- 1 | iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/StephenCleary/BuildTools/6efa9f040c0622d60dc167947b7d35b55d071f25/Coverage.ps1')) 2 | -------------------------------------------------------------------------------- /Version.ps1: -------------------------------------------------------------------------------- 1 | param([String]$oldVersion="", [String]$newVersion="") 2 | iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/StephenCleary/BuildTools/f27313f986ce2b26b091b87649771b0bcee0c30c/Version.ps1')) 3 | 4 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/linqpad-samples/Basic Usage.linq: -------------------------------------------------------------------------------- 1 | 2 | Nito.BrowserBoss 3 | static Nito.BrowserBoss.Boss 4 | 5 | 6 | Url = "https://github.com/StephenCleary/BrowserBoss"; 7 | 8 | Click("README.md"); -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Loggers/NullLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Nito.BrowserBoss.Loggers 2 | { 3 | /// 4 | /// A logger that ignores everything. 5 | /// 6 | public sealed class NullLogger : ILogger 7 | { 8 | void ILogger.WriteLine(string text) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Loggers/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Nito.BrowserBoss.Loggers 4 | { 5 | /// 6 | /// A logger that writes all messages to the console window. 7 | /// 8 | public sealed class ConsoleLogger : ILogger 9 | { 10 | void ILogger.WriteLine(string text) 11 | { 12 | Console.WriteLine(text); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Loggers/ILogger.cs: -------------------------------------------------------------------------------- 1 | namespace Nito.BrowserBoss.Loggers 2 | { 3 | /// 4 | /// A simple tracing logger. 5 | /// 6 | public interface ILogger 7 | { 8 | /// 9 | /// Writes a line of text to the logger. 10 | /// 11 | /// The text to write. 12 | void WriteLine(string text); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByXPath.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenQA.Selenium; 3 | 4 | namespace Nito.BrowserBoss.Finders 5 | { 6 | /// 7 | /// Finds elements by XPath strings. 8 | /// 9 | public sealed class FindByXPath : IFind 10 | { 11 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 12 | { 13 | return context.FindElements(By.XPath(searchText)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByValue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenQA.Selenium; 3 | 4 | namespace Nito.BrowserBoss.Finders 5 | { 6 | /// 7 | /// Finds elements by their value. 8 | /// 9 | public sealed class FindByValue : IFind 10 | { 11 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 12 | { 13 | return context.FindElements(By.CssSelector("*[value=" + Utility.CssString(searchText) + "]")); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByText.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenQA.Selenium; 3 | 4 | namespace Nito.BrowserBoss.Finders 5 | { 6 | /// 7 | /// Finds elements by their text value. 8 | /// 9 | public sealed class FindByText : IFind 10 | { 11 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 12 | { 13 | return context.FindElements(By.XPath(".//*[text() = " + Utility.XPathString(searchText) + "]")); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | os: Visual Studio 2017 3 | configuration: Debug 4 | environment: 5 | COVERALLS_REPO_TOKEN: KYm0YEBE2t3vOQTiWu9fTZGQndd3WgsMt 6 | skip_branch_with_pr: true 7 | build_script: 8 | - ps: ./Build.ps1 9 | test_script: 10 | - ps: ./Coverage.ps1 11 | artifacts: 12 | - path: 'src\**\*.nupkg' 13 | name: NuGet Packages 14 | - path: 'src\**\*.snupkg' 15 | name: NuGet Symbol Packages 16 | deploy: 17 | provider: NuGet 18 | api_key: 19 | secure: QeC34B7ohkvqbwCOKmavQWhitZNYLX/EFdgK8CfL5jEujWw2L85qrzuME8CQRBEb 20 | on: 21 | APPVEYOR_REPO_TAG: true 22 | -------------------------------------------------------------------------------- /examples/Example/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByNormalizeSpaceText.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenQA.Selenium; 3 | 4 | namespace Nito.BrowserBoss.Finders 5 | { 6 | /// 7 | /// Finds elements by their text value. 8 | /// 9 | public sealed class FindByNormalizeSpaceText : IFind 10 | { 11 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 12 | { 13 | return context.FindElements(By.XPath(".//*[normalize-space(text()) = normalize-space(" + Utility.XPathString(searchText) + ")]")); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/IWebDriverSetup.cs: -------------------------------------------------------------------------------- 1 | using OpenQA.Selenium; 2 | 3 | namespace Nito.BrowserBoss.WebDrivers 4 | { 5 | /// 6 | /// Handles automatic WebDriver installation/updating. 7 | /// 8 | public interface IWebDriverSetup 9 | { 10 | /// 11 | /// Starts a new instance of the web driver, installing or updating it as necessary. 12 | /// 13 | /// Whether to hide the Selenium command window. 14 | IWebDriver Start(bool hideCommandWindow = true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Nito.BrowserBoss 5 | { 6 | /// 7 | /// Utility methods for sequences. 8 | /// 9 | public static class EnumerableExtensions 10 | { 11 | /// 12 | /// Applies an action to all elements. 13 | /// 14 | /// The source sequence of elements. 15 | /// The action to perform on each element. 16 | public static void Apply(this IEnumerable elements, Action action) 17 | { 18 | foreach (var e in elements) 19 | action(e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/IFind.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenQA.Selenium; 3 | 4 | namespace Nito.BrowserBoss.Finders 5 | { 6 | /// 7 | /// A "finder" is an object that knows how to search for matching elements given a search string. 8 | /// 9 | public interface IFind 10 | { 11 | /// 12 | /// Finds all matching elements. May throw exceptions on error. 13 | /// 14 | /// The context of the search. All results should be within this context. 15 | /// The search string used to match the elements. 16 | IReadOnlyCollection Find(ISearchContext context, string searchText); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/LocalDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Nito.BrowserBoss.WebDrivers 5 | { 6 | /// 7 | /// The local directories used by BrowserBoss. 8 | /// 9 | public static class LocalDirectories 10 | { 11 | /// 12 | /// Gets the private local directory that web drivers should use for their installations. 13 | /// 14 | /// The name of the web driver. 15 | public static string WebDriverPath(string webDriverName) 16 | { 17 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Nito", "BrowserBoss", "WebDrivers", webDriverName); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/FirefoxWebDriverSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using OpenQA.Selenium; 7 | using OpenQA.Selenium.Firefox; 8 | 9 | namespace Nito.BrowserBoss.WebDrivers 10 | { 11 | /// 12 | /// Installs/updates the Chrome web driver. 13 | /// 14 | public sealed class FirefoxWebDriverSetup : IWebDriverSetup 15 | { 16 | /// 17 | /// Starts a new instance of the web driver, installing or updating it as necessary. 18 | /// 19 | /// Whether to hide the Selenium command window. 20 | public IWebDriver Start(bool hideCommandWindow = true) 21 | { 22 | return new FirefoxDriver(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/UnitTests/UtilityUnitTests.cs: -------------------------------------------------------------------------------- 1 | using Nito.BrowserBoss; 2 | using System; 3 | using Xunit; 4 | 5 | namespace UnitTests 6 | { 7 | public class UtilityUnitTests 8 | { 9 | [Theory] 10 | [InlineData("a", "'a'")] 11 | [InlineData("a'", "\"a'\"")] 12 | [InlineData("a\"", "\'a\"'")] 13 | [InlineData("a\"'", "\"a\\\"'\"")] 14 | [InlineData("a\"'\\", "\"a\\\"'\\\\\"")] 15 | public void CssString_QuotesAndEscapes(string input, string expected) 16 | { 17 | Assert.Equal(expected, Utility.CssString(input)); 18 | } 19 | 20 | [Theory] 21 | [InlineData("a", "'a'")] 22 | [InlineData("a'", "\"a'\"")] 23 | [InlineData("a\"", "'a\"'")] 24 | public void XPathString_QuotesAndEscapes(string input, string expected) 25 | { 26 | Assert.Equal(expected, Utility.XPathString(input)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | all 10 | runtime; build; native; contentfiles; analyzers 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stephen Cleary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Nito.BrowserBoss; 7 | 8 | namespace Example 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | try 15 | { 16 | // This is a sample site designed to be difficult to script. 17 | Boss.Url = "http://newtours.demoaut.com/"; 18 | 19 | Boss.Click("REGISTER"); // You can click links by text 20 | Boss.Write("#email", "StephenCleary"); // CSS selectors are commonly used 21 | Boss.Write("//input[@type='password']", "password"); // XPath also works fine 22 | // Note that the last line matched both password inputs, and filled them both in. 23 | Boss.Click("input[name='register']"); 24 | 25 | Console.WriteLine("Done"); 26 | } 27 | catch (Exception ex) 28 | { 29 | Console.WriteLine(ex); 30 | } 31 | Console.ReadKey(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/Example/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Example")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Example")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3a719aed-f090-4ecb-b7a4-5c95e6537ce1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByLabel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using OpenQA.Selenium; 4 | 5 | namespace Nito.BrowserBoss.Finders 6 | { 7 | /// 8 | /// Finds elements by a matching label. The search string is the label text, and the returned element is the form element that label is for. 9 | /// 10 | public sealed class FindByLabel : IFind 11 | { 12 | private static IEnumerable DoFind(ISearchContext context, string searchText) 13 | { 14 | foreach (var label in context.FindElements(By.XPath(".//label[text() = " + Utility.XPathString(searchText) + "]"))) 15 | { 16 | var forAttribute = label.GetAttribute("for"); 17 | if (forAttribute != null) 18 | { 19 | foreach (var e in context.FindElements(By.Id(forAttribute))) 20 | yield return e; 21 | } 22 | else 23 | { 24 | foreach (var e in label.FindElements(By.XPath("./following-sibling::*[1]"))) 25 | { 26 | if (e.TagName == "select" || e.TagName == "textarea" || (e.TagName == "input" && e.GetAttribute("type") != "hidden")) 27 | yield return e; 28 | } 29 | } 30 | } 31 | } 32 | 33 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 34 | { 35 | return DoFind(context, searchText).ToArray(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/BrowserUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Win32; 3 | 4 | namespace Nito.BrowserBoss.WebDrivers 5 | { 6 | /// 7 | /// Helper methods for working with browsers. 8 | /// 9 | public static class BrowserUtility 10 | { 11 | /// 12 | /// Gets the WebDriver setup for the user's default browser. 13 | /// 14 | public static IWebDriverSetup GetSetupForDefaultBrowser() 15 | { 16 | var defaultBrowserIdentifier = GetDefaultBrowserIdentifier(); 17 | if (defaultBrowserIdentifier.IndexOf("IE", StringComparison.InvariantCultureIgnoreCase) != -1) 18 | return new InternetExplorerWebDriverSetup(); 19 | if (defaultBrowserIdentifier.IndexOf("Chrome", StringComparison.InvariantCultureIgnoreCase) != -1) 20 | return new ChromeWebDriverSetup(); 21 | if (defaultBrowserIdentifier.IndexOf("Firefox", StringComparison.InvariantCultureIgnoreCase) != -1) 22 | return new FirefoxWebDriverSetup(); 23 | 24 | // If it's an unrecognized browser type, just punt and start IE. 25 | return new InternetExplorerWebDriverSetup(); 26 | } 27 | 28 | private static string GetDefaultBrowserIdentifier() 29 | { 30 | using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice")) 31 | { 32 | if (key == null) 33 | return string.Empty; 34 | var identifier = key.GetValue("Progid") as string; 35 | return identifier ?? string.Empty; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Nito.BrowserBoss.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net461 5 | Browser automation toolkit. 6 | 3.0.1 7 | 8 | Stephen Cleary 9 | true 10 | true 11 | browser;automation;linqpad-samples 12 | https://github.com/StephenCleary/BrowserBoss 13 | MIT 14 | true 15 | true 16 | snupkg 17 | Nito 18 | latest 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | true 30 | linqpad-samples 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Nito.BrowserBoss.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nito.BrowserBoss", "src\Nito.BrowserBoss\Nito.BrowserBoss.csproj", "{0733220D-463C-4596-BFA5-93FD5DB168B0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "examples\Example\Example.csproj", "{29B6977D-9F63-434B-B2DF-4550A743DD3F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{CD25A8D3-95DD-4634-B2D0-0299A28F2BEF}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {0733220D-463C-4596-BFA5-93FD5DB168B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {0733220D-463C-4596-BFA5-93FD5DB168B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {0733220D-463C-4596-BFA5-93FD5DB168B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {0733220D-463C-4596-BFA5-93FD5DB168B0}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {29B6977D-9F63-434B-B2DF-4550A743DD3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {29B6977D-9F63-434B-B2DF-4550A743DD3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {29B6977D-9F63-434B-B2DF-4550A743DD3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {29B6977D-9F63-434B-B2DF-4550A743DD3F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {CD25A8D3-95DD-4634-B2D0-0299A28F2BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {CD25A8D3-95DD-4634-B2D0-0299A28F2BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {CD25A8D3-95DD-4634-B2D0-0299A28F2BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {CD25A8D3-95DD-4634-B2D0-0299A28F2BEF}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {24D30368-AE77-492F-83F0-02F0BACB4B0F} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/InternetExplorerWebDriverSetup.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Net; 4 | using OpenQA.Selenium; 5 | using OpenQA.Selenium.IE; 6 | 7 | namespace Nito.BrowserBoss.WebDrivers 8 | { 9 | /// 10 | /// Manages the installation/updates for the IE web driver. 11 | /// 12 | public sealed class InternetExplorerWebDriverSetup : WebDriverSetupBase 13 | { 14 | /// 15 | /// Creates an instance responsible for installing/updating the IE web driver. 16 | /// 17 | public InternetExplorerWebDriverSetup() 18 | : base("IE") 19 | { 20 | } 21 | 22 | /// 23 | /// Downloads the specified web driver version. 24 | /// 25 | /// The version to download. 26 | protected override void Update(string version) 27 | { 28 | using (var client = new WebClient()) 29 | using (var stream = client.OpenRead("http://selenium-release.storage.googleapis.com/" + version + "/IEDriverServer_Win32_" + version + ".0.zip")) 30 | using (var archive = new ZipArchive(stream)) 31 | archive.ExtractToDirectory(Path.Combine(ParentPath, version)); 32 | } 33 | 34 | /// 35 | /// Returns the newest version available for download. Currently always returns "2.45". 36 | /// 37 | protected override string AvailableVersion() 38 | { 39 | // TODO: better implementation 40 | return "2.47"; 41 | } 42 | 43 | /// 44 | /// Starts a new instance of the web driver, installing or updating it as necessary. 45 | /// 46 | /// Whether to hide the Selenium command window. 47 | public override IWebDriver Start(bool hideCommandWindow = true) 48 | { 49 | var path = Install(); 50 | var driverService = InternetExplorerDriverService.CreateDefaultService(path); 51 | driverService.HideCommandPromptWindow = hideCommandWindow; 52 | return new InternetExplorerDriver(driverService); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/ChromeWebDriverSetup.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Net; 4 | using OpenQA.Selenium; 5 | using OpenQA.Selenium.Chrome; 6 | 7 | namespace Nito.BrowserBoss.WebDrivers 8 | { 9 | /// 10 | /// Installs/updates the Chrome web driver. 11 | /// 12 | public sealed class ChromeWebDriverSetup : WebDriverSetupBase 13 | { 14 | /// 15 | /// Creates an instance responsible for installing/updating the Chrome web driver. 16 | /// 17 | public ChromeWebDriverSetup() 18 | : base("Chrome") 19 | { 20 | } 21 | 22 | /// 23 | /// Downloads the specified web driver version. 24 | /// 25 | /// The version to download. 26 | protected override void Update(string version) 27 | { 28 | using (var client = new WebClient()) 29 | using (var stream = client.OpenRead("http://chromedriver.storage.googleapis.com/" + version + "/chromedriver_win32.zip")) 30 | using (var archive = new ZipArchive(stream)) 31 | archive.ExtractToDirectory(Path.Combine(ParentPath, version)); 32 | } 33 | 34 | /// 35 | /// Returns the newest version available for download. 36 | /// 37 | protected override string AvailableVersion() 38 | { 39 | using (var client = new WebClient()) 40 | { 41 | var versionString = client.DownloadString("http://chromedriver.storage.googleapis.com/LATEST_RELEASE"); 42 | return System.Text.RegularExpressions.Regex.Replace(versionString, @"\s", ""); 43 | } 44 | } 45 | 46 | /// 47 | /// Starts a new instance of the web driver, installing or updating it as necessary. 48 | /// 49 | /// Whether to hide the Selenium command window. 50 | public override IWebDriver Start(bool hideCommandWindow = true) 51 | { 52 | var path = Install(); 53 | var driverService = ChromeDriverService.CreateDefaultService(path); 54 | driverService.HideCommandPromptWindow = hideCommandWindow; 55 | return new ChromeDriver(driverService); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /examples/Example/Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {29B6977D-9F63-434B-B2DF-4550A743DD3F} 8 | Exe 9 | Properties 10 | Example 11 | Example 12 | v4.7.2 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {0733220d-463c-4596-bfa5-93fd5db168b0} 54 | Nito.BrowserBoss 55 | 56 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Nito.BrowserBoss 4 | { 5 | /// 6 | /// String utility methods. 7 | /// 8 | public static class Utility 9 | { 10 | /// 11 | /// Surrounds the string value with single or double quotes, returning an expression if necessary. 12 | /// 13 | /// The string value. 14 | public static string XPathString(string value) 15 | { 16 | // Quickly handle the common cases. 17 | if (!value.Contains("'")) 18 | return "'" + value + "'"; 19 | if (!value.Contains("\"")) 20 | return "\"" + value + "\""; 21 | 22 | // TODO: unit testing. http://stackoverflow.com/questions/1341847/special-character-in-xpath-query 23 | var sb = new StringBuilder(); 24 | sb.Append("concat("); 25 | var index = 0; 26 | while (true) 27 | { 28 | var nextDoubleQuote = value.IndexOf('"', index); 29 | var nextSingleQuote = value.IndexOf('\'', index); 30 | 31 | if (nextDoubleQuote == -1) 32 | { 33 | sb.Append("\"" + value.Substring(index) + "\")"); 34 | return sb.ToString(); 35 | } 36 | 37 | if (nextSingleQuote == -1) 38 | { 39 | sb.Append("'" + value.Substring(index) + "'"); 40 | return sb.ToString(); 41 | } 42 | 43 | if (index != 0) 44 | sb.Append(","); 45 | if (nextDoubleQuote > nextSingleQuote) 46 | { 47 | sb.Append("\"" + value.Substring(index, nextDoubleQuote - index - 1) + "\","); 48 | index = nextDoubleQuote; 49 | } 50 | else 51 | { 52 | sb.Append("'" + value.Substring(index, nextSingleQuote - index - 1) + "',"); 53 | index = nextSingleQuote; 54 | } 55 | } 56 | } 57 | 58 | /// 59 | /// Escapes the string value as necessary and surrounds it with single or double quotes. 60 | /// 61 | /// The string value. 62 | public static string CssString(string value) 63 | { 64 | if (!value.Contains("\\")) 65 | { 66 | if (!value.Contains("'")) 67 | return "'" + value + "'"; 68 | if (!value.Contains("\"")) 69 | return "\"" + value + "\""; 70 | } 71 | 72 | // TODO: unit tests 73 | var sb = new StringBuilder(); 74 | foreach (var ch in value) 75 | { 76 | if (ch == '\\') 77 | sb.Append("\\\\"); 78 | else if (ch == '\"') 79 | sb.Append("\\\""); 80 | else 81 | sb.Append(ch); 82 | } 83 | return "\"" + sb + "\""; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Browser.cs: -------------------------------------------------------------------------------- 1 | using Nito.BrowserBoss.WebDrivers; 2 | using OpenQA.Selenium; 3 | using OpenQA.Selenium.Chrome; 4 | using OpenQA.Selenium.Firefox; 5 | using OpenQA.Selenium.IE; 6 | 7 | namespace Nito.BrowserBoss 8 | { 9 | /// 10 | /// Represents a browser instance. 11 | /// 12 | public sealed class Browser 13 | { 14 | /// 15 | /// Creates a browser wrapper for the specified web driver. 16 | /// 17 | /// The web driver to wrap. 18 | public Browser(IWebDriver webDriver) 19 | { 20 | WebDriver = webDriver; 21 | } 22 | 23 | /// 24 | /// The underlying Selenium web driver. 25 | /// 26 | public IWebDriver WebDriver { get; private set; } 27 | 28 | /// 29 | /// Gets the current URL or navigates to a new URL. 30 | /// 31 | public string Url 32 | { 33 | get { return WebDriver.Url; } 34 | set { WebDriver.Navigate().GoToUrl(value); } 35 | } 36 | 37 | /// 38 | /// Starts the user's default browser, updating the web driver if necessary. 39 | /// 40 | /// Whether to hide the Selenium command window. 41 | public static Browser StartDefault(bool hideCommandWindow = true) 42 | { 43 | return new Browser(BrowserUtility.GetSetupForDefaultBrowser().Start(hideCommandWindow)); 44 | } 45 | 46 | /// 47 | /// Starts the Chrome browser, updating the web driver if necessary. 48 | /// 49 | /// Whether to hide the Selenium command window. 50 | public static Browser StartChrome(bool hideCommandWindow = true) 51 | { 52 | return new Browser(new ChromeWebDriverSetup().Start(hideCommandWindow)); 53 | } 54 | 55 | /// 56 | /// Starts the Internet Explorer browser, updating the web driver if necessary. 57 | /// 58 | /// Whether to hide the Selenium command window. 59 | public static Browser StartInternetExplorer(bool hideCommandWindow = true) 60 | { 61 | return new Browser(new InternetExplorerWebDriverSetup().Start(hideCommandWindow)); 62 | } 63 | 64 | /// 65 | /// Starts the Firefox browser. 66 | /// 67 | /// Whether to hide the Selenium command window. 68 | public static Browser StartFirefox(bool hideCommandWindow = true) 69 | { 70 | return new Browser(new FirefoxWebDriverSetup().Start(hideCommandWindow)); 71 | } 72 | 73 | /// 74 | /// Executes JavaScript in this browser. 75 | /// 76 | /// The JavaScript to execute. 77 | /// The arguments to pass to . 78 | public dynamic Script(string script, params object[] args) 79 | { 80 | return ((IJavaScriptExecutor)WebDriver).ExecuteScript(script, args); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindByJQueryCss.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Net; 6 | using Newtonsoft.Json; 7 | using OpenQA.Selenium; 8 | 9 | namespace Nito.BrowserBoss.Finders 10 | { 11 | /// 12 | /// Finds elements by JQuery-style CSS selectors. This supports JQuery extensions to CSS selectors: https://api.jquery.com/category/selectors/jquery-selector-extensions/ 13 | /// 14 | public sealed class FindByJQueryCss : IFind 15 | { 16 | /// 17 | /// The URI used to download jQuery, if jQuery is not already loaded on the page. 18 | /// 19 | public string JQueryUri { get; set; } = "https://code.jquery.com/jquery-3.4.1.slim.min.js"; 20 | 21 | IReadOnlyCollection IFind.Find(ISearchContext context, string searchText) 22 | { 23 | return context.FindElements(new ByJQuery(JQueryUri, searchText)); 24 | } 25 | 26 | private sealed class ByJQuery : By 27 | { 28 | private readonly string _jQueryUri; 29 | private readonly string _selector; 30 | 31 | public ByJQuery(string jQueryUri, string selector) 32 | { 33 | _jQueryUri = jQueryUri; 34 | _selector = JsonConvert.SerializeObject(selector); 35 | Description = $"ByJQuery: {selector}"; 36 | } 37 | 38 | public override ReadOnlyCollection FindElements(ISearchContext context) 39 | { 40 | var scriptExecutor = (IJavaScriptExecutor)context; 41 | EnsureJQueryIsLoaded(scriptExecutor); 42 | 43 | IEnumerable result; 44 | if (context is IWebElement) 45 | result = (IEnumerable) scriptExecutor.ExecuteScript($"return jQuery.makeArray(jQuery({_selector}, arguments[0]))", context); 46 | else 47 | result = (IEnumerable) scriptExecutor.ExecuteScript($"return jQuery.makeArray(jQuery({_selector}))"); 48 | 49 | return new ReadOnlyCollection(result.Cast().ToList()); 50 | } 51 | 52 | public override IWebElement FindElement(ISearchContext context) 53 | { 54 | var found = FindElements(context); 55 | if (found.Count == 0) 56 | throw new NoSuchElementException($"Could not find element matching jQuery selector {_selector}."); 57 | 58 | return found[0]; 59 | } 60 | 61 | void EnsureJQueryIsLoaded(IJavaScriptExecutor scriptExecutor) 62 | { 63 | if (IsJQueryLoaded(scriptExecutor)) 64 | return; 65 | 66 | var script = new WebClient().DownloadString(_jQueryUri); 67 | scriptExecutor.ExecuteScript(script); 68 | } 69 | 70 | bool IsJQueryLoaded(IJavaScriptExecutor scriptExecutor) 71 | { 72 | try 73 | { 74 | return (bool) scriptExecutor.ExecuteScript("return typeof(jQuery)==='function'"); 75 | } 76 | catch 77 | { 78 | return false; 79 | } 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | 198 | /tools 199 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Finders/FindExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using OpenQA.Selenium; 4 | 5 | namespace Nito.BrowserBoss.Finders 6 | { 7 | /// 8 | /// Extension methods for finders. 9 | /// 10 | public static class FindExtensions 11 | { 12 | /// 13 | /// Finds all matching elements. Does not throw exceptions; an empty enumeration is returned if any errors occur. 14 | /// 15 | /// The finder. 16 | /// The context of the search. All results should be within this context. 17 | /// The search string used to match the elements. 18 | public static IReadOnlyCollection TryFind(this IFind @this, ISearchContext context, string searchText) 19 | { 20 | try 21 | { 22 | return @this.Find(context, searchText).Where(x => x.Displayed).ToList(); 23 | } 24 | catch 25 | { 26 | return new IWebElement[0]; 27 | } 28 | } 29 | 30 | /// 31 | /// Finds all matching elements. Only returns results from the first finder that returns results. Does not throw exceptions; an empty enumeration is returned if any errors occur. 32 | /// 33 | /// The finders. 34 | /// The context of the search. All results should be within this context. 35 | /// The search string used to match the elements. 36 | public static IReadOnlyCollection TryFind(this IEnumerable @this, ISearchContext context, string searchText) 37 | { 38 | foreach (var finder in @this) 39 | { 40 | var result = finder.TryFind(context, searchText); 41 | if (result.Count != 0) 42 | return result; 43 | } 44 | 45 | return new IWebElement[0]; 46 | } 47 | 48 | /// 49 | /// Finds all matching elements, searching child iframes if no matches were found. Only returns results from the first finder that returns results. Does not throw exceptions; an empty enumeration is returned if any errors occur. 50 | /// 51 | /// The finders. 52 | /// The web driver, representing the top-level context of this search. 53 | /// The context of the search. All results should be within this context. 54 | /// The search string used to match the elements. 55 | public static IReadOnlyCollection TryFind(this IReadOnlyCollection @this, IWebDriver webDriver, ISearchContext context, string searchText) 56 | { 57 | // First, return all the normal matches. 58 | var result = @this.TryFind(context, searchText); 59 | if (result.Count != 0) 60 | return result; 61 | 62 | // If more results are required, then search child iframes. 63 | foreach (var iframe in context.FindElements(By.TagName("iframe"))) 64 | { 65 | webDriver.SwitchTo().Frame(iframe); 66 | try 67 | { 68 | var html = webDriver.FindElement(By.TagName("html")); 69 | result = TryFind(@this, webDriver, html, searchText); 70 | if (result.Count != 0) 71 | return result; 72 | } 73 | finally 74 | { 75 | webDriver.SwitchTo().ParentFrame(); 76 | } 77 | } 78 | 79 | return result; 80 | } 81 | 82 | /// 83 | /// The default finders used by BrowserBoss. 84 | /// 85 | public static IEnumerable DefaultFinders() 86 | { 87 | yield return new FindByJQueryCss(); 88 | yield return new FindByXPath(); 89 | yield return new FindByValue(); 90 | yield return new FindByLabel(); 91 | yield return new FindByText(); 92 | yield return new FindByNormalizeSpaceText(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/WebDrivers/WebDriverSetupBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using OpenQA.Selenium; 4 | 5 | namespace Nito.BrowserBoss.WebDrivers 6 | { 7 | /// 8 | /// Provides basic functionality for installing/updating web drivers. 9 | /// 10 | public abstract class WebDriverSetupBase : IWebDriverSetup 11 | { 12 | private readonly string _parentPath; 13 | private readonly FileInfo _localVersionFile; 14 | 15 | /// 16 | /// Creates the local directory that contains all installations for this web driver. 17 | /// 18 | /// The name of the web driver. 19 | protected WebDriverSetupBase(string webDriverName) 20 | { 21 | _parentPath = LocalDirectories.WebDriverPath(webDriverName); 22 | Directory.CreateDirectory(_parentPath); 23 | _localVersionFile = new FileInfo(Path.Combine(_parentPath, "version.txt")); 24 | } 25 | 26 | /// 27 | /// The path containing all installations for this web driver. 28 | /// 29 | protected string ParentPath 30 | { 31 | get { return _parentPath; } 32 | } 33 | 34 | /// 35 | /// Installs/updates the web driver. Checks for updates weekly. 36 | /// 37 | protected string Install() 38 | { 39 | // Only check for driver updates every so often. 40 | if (LatestLocalVersionUpdate() > DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(7))) 41 | return Path.Combine(_parentPath, LocalVersion()); 42 | 43 | // Get our installed version and compare it with the available version. 44 | var localVersion = LocalVersion(); 45 | string availableVersion; 46 | try 47 | { 48 | availableVersion = AvailableVersion(); 49 | } 50 | catch 51 | { 52 | if (localVersion == null) 53 | throw; 54 | return Path.Combine(_parentPath, localVersion); 55 | } 56 | if (localVersion == availableVersion) 57 | { 58 | _localVersionFile.LastWriteTimeUtc = DateTime.UtcNow; 59 | return Path.Combine(_parentPath, localVersion); 60 | } 61 | 62 | // Download the newer version and update our installed version to it. 63 | Update(availableVersion); 64 | File.WriteAllText(_localVersionFile.FullName, availableVersion); 65 | return Path.Combine(_parentPath, availableVersion); 66 | } 67 | 68 | /// 69 | /// Returns the latest version available from the Internet. 70 | /// 71 | protected abstract string AvailableVersion(); 72 | 73 | /// 74 | /// Downloads and installs the into the directory Path.Combine(ParentPath, availableVersion). 75 | /// 76 | /// 77 | protected abstract void Update(string availableVersion); 78 | 79 | /// 80 | /// Gets the newest installed version, or null if there is no installed version. 81 | /// 82 | private string LocalVersion() 83 | { 84 | return !_localVersionFile.Exists ? null : System.Text.RegularExpressions.Regex.Replace(File.ReadAllText(_localVersionFile.FullName), @"\s", ""); 85 | } 86 | 87 | /// 88 | /// Gets the timestamp when the last version check was made, or DateTimeOffset.MinValue if no version check has been made yet. 89 | /// 90 | private DateTimeOffset LatestLocalVersionUpdate() 91 | { 92 | return !_localVersionFile.Exists ? DateTimeOffset.MinValue : _localVersionFile.LastWriteTimeUtc; 93 | } 94 | 95 | /// 96 | /// Starts a new instance of the web driver, installing or updating it as necessary. 97 | /// 98 | /// Whether to hide the Selenium command window. 99 | public abstract IWebDriver Start(bool hideCommandWindow = true); 100 | } 101 | } -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Element.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using OpenQA.Selenium; 6 | using OpenQA.Selenium.Interactions; 7 | 8 | namespace Nito.BrowserBoss 9 | { 10 | /// 11 | /// Represents an element on a web page. 12 | /// 13 | public sealed class Element 14 | { 15 | /// 16 | /// The parent session. 17 | /// 18 | private readonly Session _session; 19 | 20 | /// 21 | /// Creates a new element wrapper. 22 | /// 23 | /// The parent session. 24 | /// The underlying web element. 25 | public Element(Session session, IWebElement webElement) 26 | { 27 | WebElement = webElement; 28 | _session = session; 29 | } 30 | 31 | /// 32 | /// The underlying Selenium web element. 33 | /// 34 | public IWebElement WebElement { get; private set; } 35 | 36 | /// 37 | /// Whether the element is selected. 38 | /// 39 | public bool Selected { get { return WebElement.Selected; } } 40 | 41 | /// 42 | /// Returns the parent element. 43 | /// 44 | public Element Parent 45 | { 46 | get { return Find(".."); } 47 | } 48 | 49 | /// 50 | /// Finds child elements of this element. 51 | /// 52 | /// The text to search. 53 | public IEnumerable FindElements(string searchText) 54 | { 55 | return _session.FindElements(WebElement, searchText); 56 | } 57 | 58 | /// 59 | /// Finds a single matching child element of this element. 60 | /// 61 | /// The text to search. 62 | public Element Find(string searchText) 63 | { 64 | return _session.FindElement(WebElement, searchText); 65 | } 66 | 67 | /// 68 | /// Reads the value or text of the element. 69 | /// 70 | public string Read() 71 | { 72 | var result = string.Empty; 73 | if (WebElement.TagName == "input" || WebElement.TagName == "textarea") 74 | result = WebElement.GetAttribute("value") ?? string.Empty; 75 | if (WebElement.TagName == "select") 76 | { 77 | var value = WebElement.GetAttribute("value"); 78 | var option = WebElement.FindElements(By.TagName("option")).FirstOrDefault(x => x.GetAttribute("value") == value); 79 | if (option != null) 80 | result = option.Text ?? string.Empty; 81 | } 82 | if (result == string.Empty) 83 | result = WebElement.Text ?? string.Empty; 84 | return result; 85 | } 86 | 87 | /// 88 | /// Sends text to the element via keystrokes. If this element is a select element, then this selects the appropriate option instead of sending keystrokes. 89 | /// 90 | /// The text to send. 91 | public void Write(string text) 92 | { 93 | _session.Logger.WriteLine("Writing " + text + " to " + this); 94 | if (WebElement.TagName != "select") 95 | { 96 | if (WebElement.GetAttribute("readonly") == "true") 97 | throw new InvalidOperationException("Cannot write to a readonly element."); 98 | WebElement.Clear(); 99 | WebElement.SendKeys(text); 100 | return; 101 | } 102 | 103 | var option = _session.FindElements(WebElement, "option[text()=" + Utility.CssString(text) + "] | option[@value=" + Utility.CssString(text) + "]").FirstOrDefault(); 104 | if (option == null) 105 | throw new InvalidDataException("Element " + this + " does not contain option " + text); 106 | option.WebElement.Click(); 107 | } 108 | 109 | /// 110 | /// Clears the value of an element. 111 | /// 112 | public void Clear() 113 | { 114 | _session.Logger.WriteLine("Clearing " + this); 115 | if (WebElement.GetAttribute("readonly") == "true") 116 | throw new InvalidOperationException("Cannot clear a readonly element."); 117 | WebElement.Clear(); 118 | } 119 | 120 | /// 121 | /// Clicks the element. 122 | /// 123 | public void Click() 124 | { 125 | _session.Logger.WriteLine("Clicking " + this); 126 | WebElement.Click(); 127 | } 128 | 129 | /// 130 | /// Double-clicks the element. 131 | /// 132 | public void DoubleClick() 133 | { 134 | _session.Logger.WriteLine("Doubleclicking " + this); 135 | var js = "var evt = document.createEvent('MouseEvents'); evt.initMouseEvent('dblclick',true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0,null); arguments[0].dispatchEvent(evt);"; 136 | _session.Browser.Script(js, WebElement); 137 | } 138 | 139 | /// 140 | /// Ensures the element is checked. 141 | /// 142 | public void Check() 143 | { 144 | _session.Logger.WriteLine("Checking " + this); 145 | if (!WebElement.Selected) 146 | WebElement.Click(); 147 | } 148 | 149 | /// 150 | /// Ensures the element is unchecked. 151 | /// 152 | public void Uncheck() 153 | { 154 | _session.Logger.WriteLine("Unhecking " + this); 155 | if (!WebElement.Selected) 156 | WebElement.Click(); 157 | } 158 | 159 | /// 160 | /// Drags this element onto another element. 161 | /// 162 | /// The element to which to drag this one. 163 | public void DragDropTo(Element target) 164 | { 165 | _session.Retry(() => 166 | { 167 | new Actions(_session.Browser.WebDriver).DragAndDrop(WebElement, target.WebElement).Perform(); 168 | return true; 169 | }); 170 | } 171 | 172 | /// 173 | /// Returns a human-readable interpretation of the element. 174 | /// 175 | public override string ToString() 176 | { 177 | var id = WebElement.GetAttribute("id"); 178 | if (!string.IsNullOrEmpty(id)) 179 | return "#" + id; 180 | var text = WebElement.Text; 181 | if (!string.IsNullOrEmpty(text)) 182 | return "\"" + text + "\""; 183 | return "<" + WebElement.TagName + ">"; 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BrowserBoss 2 | Easily write scripts for browser automation. 3 | 4 | ## Getting Started - LINQPad 5 | 6 | BrowserBoss was specifically designed with LINQPad in mind. If you have LINQPad installed, you can install [the NuGet package](https://www.nuget.org/packages/Nito.BrowserBoss/) into your query. Then you can run code like this: 7 | 8 | ````cs 9 | // This is a sample site designed to be difficult to script. 10 | Boss.Url = "http://newtours.demoaut.com/"; 11 | 12 | // You can click links by text 13 | Boss.Click("REGISTER"); 14 | 15 | // CSS selectors are commonly used 16 | Boss.Write("#email", "StephenCleary"); 17 | 18 | // XPath also works fine 19 | Boss.Write("//input[@type='password']", "password"); 20 | // Note that the last line matched both password inputs, and filled them both in. 21 | 22 | Boss.Click("input[name='register']"); 23 | 24 | Console.WriteLine("Done"); 25 | ```` 26 | 27 | ## Getting Started - Standalone Applications 28 | 29 | If you want to use BrowserBoss in a "real" application, just install [the Nito.BrowserBoss NuGet package](https://www.nuget.org/packages/Nito.BrowserBoss/) into your project and then you can write code like: 30 | 31 | ````cs 32 | using System; 33 | using Nito.BrowserBoss; 34 | 35 | namespace Example 36 | { 37 | class Program 38 | { 39 | static void Main(string[] args) 40 | { 41 | try 42 | { 43 | // This is a sample site designed to be difficult to script. 44 | Boss.Url = "http://newtours.demoaut.com/"; 45 | 46 | // You can click links by text 47 | Boss.Click("REGISTER"); 48 | 49 | // CSS selectors are commonly used 50 | Boss.Write("#email", "StephenCleary"); 51 | 52 | // XPath also works fine 53 | Boss.Write("//input[@type='password']", "password"); 54 | // Note that the last line matched both password inputs, and filled them both in. 55 | 56 | Boss.Click("input[name='register']"); 57 | 58 | Console.WriteLine("Done"); 59 | } 60 | catch (Exception ex) 61 | { 62 | Console.WriteLine(ex); 63 | } 64 | Console.ReadKey(); 65 | } 66 | } 67 | } 68 | ```` 69 | 70 | Tip: You may also want to install the [NuGet package Costura.Fody](https://www.nuget.org/packages/Costura.Fody/), which auto-magically embeds your referenced dlls as resources. This makes it easy to write small standalone executables to script common tasks. 71 | 72 | ## How It Works: Core Concepts 73 | 74 | BrowserBoss is built on the excellent [Selenium](http://www.seleniumhq.org/) project, but extends it to make scripting easier. BrowserBoss exposes a simplified API, but the underlying Selenium types are also exposed just in case you need them. 75 | 76 | ### Search Texts and Finders 77 | 78 | BrowserBoss uses "search texts" for most of its browser interaction. A search text is a special string that you pass to BrowserBoss that can select one or more elements on a page. In the following code, "#email" is a search text: 79 | 80 | Boss.Write("#email", "StephenCleary@example.com"); 81 | 82 | Search texts are evaluated using *finders*. There are 5 finders built-in to BrowserBoss, and you can add your own by implementing the `Nito.BrowserBoss.Finders.IFind` interface and adding it to the `Boss.Config.Finders` collection. A search text is evaluated by each finder one at a time; as soon as a single finder returns a result, then no more finders are evaluated. 83 | 84 | The built-in finders are: 85 | 86 | 1. CSS selectors. The CSS selector engine used also supports the [JQuery CSS selector extensions](https://api.jquery.com/category/selectors/jquery-selector-extensions/). 87 | 2. XPath. 88 | 3. Value finder. This finder will match any element whose `value` attribute is the search text. 89 | 4. Label finder. This finder will first find any label whose `text` attribute matches the search text, and then match whatever element that label is for (using the `for` attribute if present, or the next HTML element if it is a form element). 90 | 5. Text finder. This finder will match any element whose `text` attribute matches the search text. 91 | 92 | ### Retries 93 | 94 | BrowserBoss will automatically retry as long as it action isn't doable. This helps in situations where the page is slow to load. By default, BrowserBoss will retry any operation for up to 30 seconds, at which point it will give up and throw an exception. This timeout can be changed by setting `Boss.Config.Timeout`. 95 | 96 | ### Easy to Get Started 97 | 98 | BrowserBoss installs all necessary WebDrivers for you automagically, and will periodically keep them up-to-date. So this means if you develop an exe or LINQPad script for your coworker, you can just hand it to them and not have to worry about setup instructions. 99 | 100 | ### Default Browser 101 | 102 | BrowserBoss will auto-detect your default browser and start it automatically. Browser detection currently works for Chrome, Firefox, and IE. If you want a different browser, you can put `Boss.StartChrome();`, `Boss.StartInternetExplorer();`, or `Boss.StartFirefox();` at the top of your script/program. 103 | 104 | ### Focused on Scripting, but Also Object-Oriented 105 | 106 | The primary BrowserBoss API is designed with scripting in mind, particularly with the `using static` keyword coming in C# 6. However, all of the actual implementation logic (i.e., finders, retries, automatic WebDriver installation) is available using properly-designed OOP classes. To get started, check out the `Session` and `Browser` types. 107 | 108 | ## Available APIs on the `Boss` Type 109 | 110 | `Url = "url";` - set this string value to navigate to the specified page. 111 | 112 | `Click("searchText");` - click the matching elements. 113 | 114 | `Write("searchText", "textToWrite");` - writes text to the matching elements. For `select` elements, selects the appropriate option rather than writing text. 115 | 116 | `var element = Boss.Find("searchText");` - finds a single matching element and returns it. You can then perform actions on the returned element, e.g., reading its `value` or `text` by calling `Read()`. 117 | 118 | `var elements = Boss.FindElements("searchText");` - finds multiple matching element and returns them. The returned collection is never empty. 119 | 120 | `Check("searchText");` - checks the matching elements (if they are unchecked). 121 | 122 | `Uncheck("searchText");` - unchecks the matching elements (if they are checked). 123 | 124 | `var result = Boss.Script("javascript", ...);` - execute JavaScript within the browser, optionally passing arguments and retrieving results. 125 | 126 | `DoubleClick("searchText");` - double-clicks the matching elements. 127 | 128 | `Clear("searchText");` - clears the value/text of the matching elements. Note that `Write` will automatically `Clear` its element first. 129 | 130 | `DragDrop("sourceSearchText", "targetSearchText");` - drags a single matching source element onto a single matching target element and drops it there. 131 | 132 | `XPathString("string")` - returns an XPath expression that evaluates to the specified string value. Useful if you're dealing with text that may have single and/or double quotes. 133 | 134 | `CssString("string")` - returns a CSS literal that evaluates to the specified string value. Useful if you're dealing with text that may have single and/or double quotes. 135 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Boss.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Nito.BrowserBoss.Finders; 4 | using Nito.BrowserBoss.Loggers; 5 | 6 | namespace Nito.BrowserBoss 7 | { 8 | /// 9 | /// Provides a single session to control the browser. 10 | /// 11 | public static class Boss 12 | { 13 | private static Session _session; 14 | 15 | /// 16 | /// The current global session. 17 | /// 18 | public static Session Session 19 | { 20 | get 21 | { 22 | if (_session == null) 23 | _session = new Session(Browser.StartDefault()); 24 | return _session; 25 | } 26 | set { _session = value; } 27 | } 28 | 29 | /// 30 | /// The current browser. 31 | /// 32 | public static Browser Browser { get { return Session.Browser; } } 33 | 34 | /// 35 | /// Gets the current URL or navigates to a new URL. 36 | /// 37 | public static string Url 38 | { 39 | get { return Browser.Url; } 40 | set { Browser.Url = value; } 41 | } 42 | 43 | /// 44 | /// Starts the Chrome driver as . Installs/updates the Chrome WebDriver as necessary. 45 | /// 46 | /// Whether to hide the Selenium command window. 47 | public static void StartChrome(bool hideCommandWindow = true) 48 | { 49 | Session = new Session(Browser.StartChrome(hideCommandWindow)); 50 | } 51 | 52 | /// 53 | /// Starts the IE driver as . Installs/updates the Internet Explorer WebDriver as necessary. 54 | /// 55 | /// Whether to hide the Selenium command window. 56 | public static void StartInternetExplorer(bool hideCommandWindow = true) 57 | { 58 | Session = new Session(Browser.StartInternetExplorer(hideCommandWindow)); 59 | } 60 | 61 | /// 62 | /// Starts the Firefox driver as . 63 | /// 64 | /// Whether to hide the Selenium command window. 65 | public static void StartFirefox(bool hideCommandWindow = true) 66 | { 67 | Session = new Session(Browser.StartFirefox(hideCommandWindow)); 68 | } 69 | 70 | /// 71 | /// Executes JavaScript in . 72 | /// 73 | /// The JavaScript to execute. 74 | /// The arguments to pass to . 75 | public static dynamic Script(string script, params object[] args) 76 | { 77 | return Browser.Script(script, args); 78 | } 79 | 80 | /// 81 | /// Finds the elements specified by . Repeatedly searches using the finders defined in until expires. Throws an exception if no matching elements could be found. 82 | /// 83 | /// The search string. 84 | public static IReadOnlyCollection FindElements(string searchText) 85 | { 86 | return Session.FindElements(searchText); 87 | } 88 | 89 | /// 90 | /// Finds a single matching element specified by . Repeatedly searches using the finders defined in until expires. Throws an exception if no matching elements could be found. 91 | /// 92 | /// The search string. 93 | public static Element Find(string searchText) 94 | { 95 | return Session.Find(searchText); 96 | } 97 | 98 | /// 99 | /// Sends text to the matching elements via keystrokes. If an element is a select element, then this selects the appropriate option instead of sending keystrokes. 100 | /// 101 | /// The search string. 102 | /// The text to send. 103 | public static void Write(string searchText, string text) 104 | { 105 | Session.Write(searchText, text); 106 | } 107 | 108 | /// 109 | /// Clears the value of matching elements. Throws an exception if no elements could be cleared. 110 | /// 111 | /// The search string. 112 | public static void Clear(string searchText) 113 | { 114 | Session.Clear(searchText); 115 | } 116 | 117 | /// 118 | /// Clicks the matching elements. Throws an exception if no elements could be clicked. 119 | /// 120 | /// The search string. 121 | public static void Click(string searchText) 122 | { 123 | Session.Click(searchText); 124 | } 125 | 126 | /// 127 | /// Double-clicks the matching elements. Throws an exception if no elements could be double-clicked. 128 | /// 129 | /// The search string. 130 | public static void DoubleClick(string searchText) 131 | { 132 | Session.DoubleClick(searchText); 133 | } 134 | 135 | /// 136 | /// Ensures the matching elements are checked. Throws an exception if no elements could be checked. 137 | /// 138 | /// The search string. 139 | public static void Check(string searchText) 140 | { 141 | Session.Check(searchText); 142 | } 143 | 144 | /// 145 | /// Ensures the matching elements are unchecked. Throws an exception if no elements could be unchecked. 146 | /// 147 | /// The search string. 148 | public static void Uncheck(string searchText) 149 | { 150 | Session.Uncheck(searchText); 151 | } 152 | 153 | /// 154 | /// Drags the matching element onto another matching element. Throws an exception if either search text doesn't match exactly one element each. 155 | /// 156 | /// The search string for the element to drag. 157 | /// The search string for the drag target. 158 | public static void DragDrop(string sourceSearchText, string targetSearchText) 159 | { 160 | Session.DragDrop(sourceSearchText, targetSearchText); 161 | } 162 | 163 | /// 164 | /// Returns an XPath expression that evaluates to the specified string value. 165 | /// 166 | /// The string value to embed in an XPath search text. 167 | public static string XPathString(string value) 168 | { 169 | return Utility.XPathString(value); 170 | } 171 | 172 | /// 173 | /// Returns a CSS literal that evaluates to the specified string value. 174 | /// 175 | /// The string value to embed in a CSS search text. 176 | public static string CssString(string value) 177 | { 178 | return Utility.CssString(value); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Nito.BrowserBoss/Session.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using Nito.BrowserBoss.Finders; 7 | using Nito.BrowserBoss.Loggers; 8 | using OpenQA.Selenium; 9 | using OpenQA.Selenium.Support.UI; 10 | 11 | namespace Nito.BrowserBoss 12 | { 13 | /// 14 | /// A browser session. 15 | /// 16 | public class Session 17 | { 18 | /// 19 | /// The current logger. 20 | /// 21 | private ILogger _logger = new NullLogger(); 22 | 23 | /// 24 | /// Creates a session around the specified browser. 25 | /// 26 | public Session(Browser browser) 27 | { 28 | Browser = browser; 29 | Finders = FindExtensions.DefaultFinders().ToList(); 30 | Timeout = TimeSpan.FromSeconds(30); 31 | } 32 | 33 | /// 34 | /// The current browser. 35 | /// 36 | public Browser Browser { get; private set; } 37 | 38 | /// 39 | /// The amount of time to wait for browser elements to appear. The default is 30 seconds. 40 | /// 41 | public TimeSpan Timeout { get; set; } 42 | 43 | /// 44 | /// The current logger, which can help with debugging. 45 | /// 46 | public ILogger Logger 47 | { 48 | get { return _logger; } 49 | set { _logger = value ?? new NullLogger(); } 50 | } 51 | 52 | /// 53 | /// The collection of finders used to evaluate search strings. 54 | /// 55 | public List Finders { get; private set; } 56 | 57 | /// 58 | /// Repeatedly executes until it returns true or until expires. 59 | /// 60 | /// The function to execute. 61 | public void Retry(Func func) 62 | { 63 | var wait = new WebDriverWait(Browser.WebDriver, Timeout); 64 | wait.Until(_ => 65 | { 66 | try 67 | { 68 | return func(); 69 | } 70 | catch 71 | { 72 | return false; 73 | } 74 | }); 75 | } 76 | 77 | /// 78 | /// Repeatedly executes until it returns a non-empty collection or until expires. 79 | /// 80 | /// The type of elements in the collection returned by . 81 | /// The function to execute. 82 | public T RetrySearch(Func func) where T : ICollection 83 | { 84 | var wait = new WebDriverWait(Browser.WebDriver, Timeout); 85 | var result = default(T); 86 | wait.Until(_ => 87 | { 88 | try 89 | { 90 | result = func(); 91 | return result.Count != 0; 92 | } 93 | catch 94 | { 95 | return false; 96 | } 97 | }); 98 | return result; 99 | } 100 | 101 | /// 102 | /// Repeatedly attempts to find the elements until expires. Throws an exception if no matching elements could be found. 103 | /// 104 | /// The context in which to search. 105 | /// The search string. 106 | public IReadOnlyCollection FindElements(ISearchContext context, string searchText) 107 | { 108 | try 109 | { 110 | return RetrySearch(() => Finders.TryFind(Browser.WebDriver, context, searchText).Select(x => new Element(this, x)).ToList()); 111 | } 112 | catch (WebDriverTimeoutException ex) 113 | { 114 | throw new InvalidDataException("Could not find elements matching " + searchText, ex); 115 | } 116 | } 117 | 118 | /// 119 | /// Repeatedly attempts to find exactly one element until expires. Throws an exception if no matching element could be found. 120 | /// 121 | /// The context in which to search. 122 | /// The search string. 123 | public Element FindElement(ISearchContext context, string searchText) 124 | { 125 | var result = FindElements(context, searchText); 126 | if (result.Count != 1) 127 | throw new InvalidDataException("Multiple elements match " + searchText); 128 | return result.First(); 129 | } 130 | 131 | /// 132 | /// Finds the elements specified by . Repeatedly searches using the finders defined in until expires. Throws an exception if no matching elements could be found. 133 | /// 134 | /// The search string. 135 | public IReadOnlyCollection FindElements(string searchText) 136 | { 137 | return FindElements(Browser.WebDriver, searchText); 138 | } 139 | 140 | /// 141 | /// Finds a single matching element specified by . Repeatedly searches using the finders defined in until expires. Throws an exception if no matching element could be found. 142 | /// 143 | /// The search string. 144 | public Element Find(string searchText) 145 | { 146 | return FindElement(Browser.WebDriver, searchText); 147 | } 148 | 149 | /// 150 | /// Sends text to the matching elements via keystrokes. If an element is a select element, then this selects the appropriate option instead of sending keystrokes. 151 | /// 152 | /// The search string. 153 | /// The text to send. 154 | public void Write(string searchText, string text) 155 | { 156 | FindElements(searchText).Apply(x => x.Write(text)); 157 | } 158 | 159 | /// 160 | /// Clears the value of matching elements. Throws an exception if no elements could be cleared. 161 | /// 162 | /// The search string. 163 | public void Clear(string searchText) 164 | { 165 | FindElements(searchText).Apply(x => x.Clear()); 166 | } 167 | 168 | /// 169 | /// Clicks the matching elements. Throws an exception if no elements could be clicked. 170 | /// 171 | /// The search string. 172 | public void Click(string searchText) 173 | { 174 | FindElements(searchText).Apply(x => x.Click()); 175 | } 176 | 177 | /// 178 | /// Double-clicks the matching elements. Throws an exception if no elements could be double-clicked. 179 | /// 180 | /// The search string. 181 | public void DoubleClick(string searchText) 182 | { 183 | FindElements(searchText).Apply(x => x.DoubleClick()); 184 | } 185 | 186 | /// 187 | /// Ensures the matching elements are checked. Throws an exception if no elements could be checked. 188 | /// 189 | /// The search string. 190 | public void Check(string searchText) 191 | { 192 | FindElements(searchText).Apply(x => x.Check()); 193 | } 194 | 195 | /// 196 | /// Ensures the matching elements are unchecked. Throws an exception if no elements could be unchecked. 197 | /// 198 | /// The search string. 199 | public void Uncheck(string searchText) 200 | { 201 | FindElements(searchText).Apply(x => x.Uncheck()); 202 | } 203 | 204 | /// 205 | /// Drags the matching element onto another matching element. Throws an exception if either search text doesn't match exactly one element each. 206 | /// 207 | /// The search string for the element to drag. 208 | /// The search string for the drag target. 209 | public void DragDrop(string sourceSearchText, string targetSearchText) 210 | { 211 | Find(sourceSearchText).DragDropTo(Find(targetSearchText)); 212 | } 213 | } 214 | } 215 | --------------------------------------------------------------------------------