Вступление
Снова здравствуйте! Представляю вам вторую часть статьи на тему автоматизированного тестирование веб-приложения на Selenium и C#. И если первая часть была из разряда «капитан очевидность», что вызвало негодование у читателей, то в этой части будет много кода. И так, зачем же писать обертку (wrapper) над Selenium API? На мой взгляд, пользователи могут столкнуться со следующими проблемами:
- Интерфейс IWebDriver предоставляет довольно скудную функциональность в плане управления браузером. Поэтому все, что нам понадобится, придется писать самим
- Описание элемента происходит одновременно с его поиском, т.е. на момент определения элемента он должен существовать в браузере. Очень часто решается путем написания getter для каждого элемента. Это накладно и плохо с точки зрения производительности
- ISearchContext.FindElements принимает только один параметр типа OpenQA.Selenium.By, т.е. мы не можем искать сразу по нескольким свойствам. Обычно элемент ищется по первому критерию, а затем начинается отсеивание по остальным
- Отсутствие многих, казалось бы, очевидных методов и свойств. Например: Exist, SetText, Select, SetCheck, InnerHtml и т.д. Вместо этого мы вынуждены довольствоваться Click, SendKeys и Text
- Множество проблем на различных браузерах, например на Firefox и Chrome элемент кликается, а на IE — нет. Приходится писать special cases, «костыли»
- Производительность. Да, драйвера работают не быстро. Впереди планеты всей как обычно IE — поиск может занимать секунды, иногда и десятки секунд
Поехали. Реализация враппера над браузером: класс Browser
Я очень надеюсь, что приведенный код не покажется вам сложным.
В примере инкапсулирован весь специфический для IWebDriver код, трусы наружу не высовываются, т.е. разработчики автотестов не будут иметь прямого доступа к драйверу. Как следствие — наличие методов, в которых вызывается метод драйвера с таким же названием.
В коде нет комментариев — это хороший тон, и как сказал один умный чел: «Комментарии в коде это как волосы в супе. Ты бы стал есть суп с волосами?!»
Я использую Microsoft Code Contracts, не пугайтесь вызовов.
Еще стоит заметить, что в тестируемом продукте подключен jquery, и некоторые действия будут производиться с его использованием.
Класс Browser поддерживает 3 браузера:
- Firefox
- Chrome
- Internet Explorer
Очень жаль, что пока нет C# драйверов для Opera :(
Реализован набор стандартных методов и свойств вроде:
- Start
- Quit
- Navigate
- NavigateBack
- Refresh
- WaitReadyState
- FindElements
- SelectedBrowser
- Url
- Title
- PageSource
- др.
Думаю, названия вполне очевидные, поэтому идем дальше.
Также реализованы специфические функции:
- WaitAjax
- SwitchToFrame
- SwitchToPopupWindow
- AcceptAlert
- GetScreenshot + SaveScreenshot
- ResizeWindow
- ExecuteJavaScript
- DragAndDrop
- др.
namespace Autotests.Utilities
{
[Serializable]
public enum Browsers
{
[Description("Windows Internet Explorer")]
InternetExplorer,
[Description("Mozilla Firefox")]
Firefox,
[Description("Google Chrome")]
Chrome
}
public static class Browser
{
#region Public properties
public static Browsers SelectedBrowser
{
get { return Settings.Default.Browser; }
}
public static Uri Url
{
get { WaitAjax(); return new Uri(WebDriver.Url); }
}
public static string Title
{
get
{
WaitAjax();
return string.Format("{0} - {1}", WebDriver.Title, EnumHelper.GetEnumDescription(SelectedBrowser));
}
}
public static string PageSource
{
get { WaitAjax(); return WebDriver.PageSource; }
}
#endregion
#region Public methods
public static void Start()
{
_webDriver = StartWebDriver();
}
public static void Navigate(Uri url)
{
Contract.Requires(url != null);
WebDriver.Navigate().GoToUrl(url);
}
public static void Quit()
{
if (_webDriver == null) return;
_webDriver.Quit();
_webDriver = null;
}
public static void WaitReadyState()
{
Contract.Assume(WebDriver != null);
var ready = new Func<bool>(() => (bool)ExecuteJavaScript("return document.readyState == 'complete'"));
Contract.Assert(Executor.SpinWait(ready, TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(100)));
}
public static void WaitAjax()
{
Contract.Assume(WebDriver != null);
var ready = new Func<bool>(() => (bool)ExecuteJavaScript("return (typeof($) === 'undefined') ? true : !$.active;"));
Contract.Assert(Executor.SpinWait(ready, TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(100)));
}
public static void SwitchToFrame(IWebElement inlineFrame)
{
WebDriver.SwitchTo().Frame(inlineFrame);
}
public static void SwitchToPopupWindow()
{
foreach (var handle in WebDriver.WindowHandles.Where(handle => handle != _mainWindowHandler))
{
WebDriver.SwitchTo().Window(handle);
}
}
public static void SwitchToMainWindow()
{
WebDriver.SwitchTo().Window(_mainWindowHandler);
}
public static void SwitchToDefaultContent()
{
WebDriver.SwitchTo().DefaultContent();
}
public static void AcceptAlert()
{
var accept = Executor.MakeTry(() => WebDriver.SwitchTo().Alert().Accept());
Executor.SpinWait(accept, TimeSpan.FromSeconds(5));
}
public static IEnumerable<IWebElement> FindElements(By selector)
{
Contract.Assume(WebDriver != null);
return WebDriver.FindElements(selector);
}
public static Screenshot GetScreenshot()
{
WaitReadyState();
return ((ITakesScreenshot)WebDriver).GetScreenshot();
}
public static void SaveScreenshot(string path)
{
Contract.Requires(!string.IsNullOrEmpty(path));
GetScreenshot().SaveAsFile(path, ImageFormat.Jpeg);
}
public static void DragAndDrop(IWebElement source, IWebElement destination)
{
(new Actions(WebDriver)).DragAndDrop(source, destination).Build().Perform();
}
public static void ResizeWindow(int width, int height)
{
ExecuteJavaScript(string.Format("window.resizeTo({0}, {1});", width, height));
}
public static void NavigateBack()
{
WebDriver.Navigate().Back();
}
public static void Refresh()
{
WebDriver.Navigate().Refresh();
}
public static object ExecuteJavaScript(string javaScript, params object[] args)
{
var javaScriptExecutor = (IJavaScriptExecutor)WebDriver;
return javaScriptExecutor.ExecuteScript(javaScript, args);
}
public static void KeyDown(string key)
{
new Actions(WebDriver).KeyDown(Keys.Control);
}
public static void KeyUp(string key)
{
new Actions(WebDriver).KeyUp(Keys.Control);
}
public static void AlertAccept()
{
Thread.Sleep(2000);
WebDriver.SwitchTo().Alert().Accept();
WebDriver.SwitchTo().DefaultContent();
}
#endregion
#region Private
private static IWebDriver _webDriver;
private static string _mainWindowHandler;
private static IWebDriver WebDriver
{
get { return _webDriver ?? StartWebDriver(); }
}
private static IWebDriver StartWebDriver()
{
Contract.Ensures(Contract.Result<IWebDriver>() != null);
if (_webDriver != null) return _webDriver;
switch (SelectedBrowser)
{
case Browsers.InternetExplorer:
_webDriver = StartInternetExplorer();
break;
case Browsers.Firefox:
_webDriver = StartFirefox();
break;
case Browsers.Chrome:
_webDriver = StartChrome();
break;
default:
throw new Exception(string.Format("Unknown browser selected: {0}.", SelectedBrowser));
}
_webDriver.Manage().Window.Maximize();
_mainWindowHandler = _webDriver.CurrentWindowHandle;
return WebDriver;
}
private static InternetExplorerDriver StartInternetExplorer()
{
var internetExplorerOptions = new InternetExplorerOptions
{
IntroduceInstabilityByIgnoringProtectedModeSettings = true,
InitialBrowserUrl = "about:blank",
EnableNativeEvents = true
};
return new InternetExplorerDriver(Directory.GetCurrentDirectory(), internetExplorerOptions);
}
private static FirefoxDriver StartFirefox()
{
var firefoxProfile = new FirefoxProfile
{
AcceptUntrustedCertificates = true,
EnableNativeEvents = true
};
return new FirefoxDriver(firefoxProfile);
}
private static ChromeDriver StartChrome()
{
var chromeOptions = new ChromeOptions();
var defaultDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"..LocalGoogleChromeUser DataDefault";
if (Directory.Exists(defaultDataFolder))
{
Executor.Try(() => DirectoryExtension.ForceDelete(defaultDataFolder));
}
return new ChromeDriver(Directory.GetCurrentDirectory(), chromeOptions);
}
#endregion
}
}
Возможно стоит прокомментировать, что Settings.Default.Browser — это параметр, который задается в свойствах проекта, а Executor.SpinWait — некоторый хелпер, который дожидается выполнения условия с таймаутом и возвращает true / false. Так же встречаются magic strings, извиняюсь)
Автор: natexriver