diff --git a/App.config b/App.config new file mode 100644 index 0000000..6d7c6f1 --- /dev/null +++ b/App.config @@ -0,0 +1,62 @@ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Appium/AppiumTouchCommands.cs b/Appium/AppiumTouchCommands.cs new file mode 100644 index 0000000..53c5cab --- /dev/null +++ b/Appium/AppiumTouchCommands.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using OpenQA.Selenium; +using ProtoTest.Golem.WebDriver; + +namespace ProtoTest.Golem.Appium +{ + public class AppiumTouchCommands + { + private readonly IWebDriver driver; + public Dictionary coords; + + public AppiumTouchCommands(IWebDriver driver) + { + this.driver = driver; + } + + public void ResetCoordinates() + { + coords = new Dictionary(); + } + + public void AddCoordinate(string key, double value) + { + coords.Add(key, value); + } + + public void Execute(string command) + { + driver.ExecuteJavaScript("mobile: " + command, coords); + } + + public void Tap(double X, double Y, int count = 1, double duration = .1) + { + coords = new Dictionary(); + coords.Add("x", X); + coords.Add("y", Y); + coords.Add("tapCount", count); + coords.Add("duration", duration); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + + public void ScrollTo(string id) + { + var elementObject = new Dictionary(); + elementObject.Add("element", id); + driver.ExecuteJavaScript("mobile: scrollTo", elementObject); + } + + public void Swipe(double startX, double startY, double endX, double endY) + { + coords = new Dictionary(); + coords.Add("startX", startX); + coords.Add("startY", startY); + coords.Add("endX", endX); + coords.Add("endY", endY); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + + public void SwipeDown() + { + coords = new Dictionary(); + coords.Add("startX", 0.5); + coords.Add("startY", 0.95); + coords.Add("endX", 0.5); + coords.Add("endY", 0.05); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + + public void SwipeUp() + { + coords = new Dictionary(); + coords.Add("startX", 0.5); + coords.Add("startY", 0.05); + coords.Add("endX", 0.5); + coords.Add("endY", 0.95); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + + public void SwipeRight() + { + coords = new Dictionary(); + coords.Add("startX", 0.05); + coords.Add("startY", 0.5); + coords.Add("endX", 0.95); + coords.Add("endY", 0.5); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + + public void SwipeLeft() + { + coords = new Dictionary(); + coords.Add("startX", 0.95); + coords.Add("startY", 0.5); + coords.Add("endX", 0.05); + coords.Add("endY", 0.5); + driver.ExecuteJavaScript("mobile: swipe", coords); + } + } +} \ No newline at end of file diff --git a/Core/ActionList.cs b/Core/ActionList.cs new file mode 100644 index 0000000..63e7301 --- /dev/null +++ b/Core/ActionList.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace Golem.Core +{ + /// + /// Holds a list of Actions, with some functions to help print + /// + public class ActionList + { + public List actions; + + public ActionList() + { + actions = new List(); + } + + public void addAction(string name, DateTime time) + { + actions.Add(new Action(name, time)); + } + + public void addAction(string name, Action.ActionType type=Action.ActionType.Other) + { + var time = DateTime.Now; + actions.Add(new Action(name, time, type)); + } + + public void PrintActions() + { + foreach (var a in actions) + { + Log.Message(a.name + " : " + a.time.ToString("HH:mm:ss.ffff")); + } + } + + public void PrintActionTimings() + { + DateTime start; + DateTime end; + TimeSpan difference; + for (var i = 1; i < actions.Count; i++) + { + while (actions[i].name == actions[i - 1].name) + { + i++; + } + start = actions[i - 1].time; + end = actions[i].time; + difference = end.Subtract(start); + Log.Message(actions[i].name + " : " + difference); + } + start = actions[0].time; + end = actions[actions.Count - 1].time; + difference = end.Subtract(start); + Log.Message("All Actions : " + difference); + } + + public void RemoveDuplicateEntries() + { + var distinctItems = actions.GroupBy(x => x.name).Select(y => y.Last()); + actions = distinctItems.ToList(); + } + + public class Action + { + public DateTime time; + public string name; + public ActionType type; + + public Action(string name, DateTime time, ActionType type=ActionType.Other) + { + this.name = name; + this.time = time; + this.type = type; + } + + public enum ActionType + { + Message = 0, + Warning = 1, + Error = 2, + Video = 3, + Image = 4, + Link = 5, + Other = 6 + } + } + } + + +} \ No newline at end of file diff --git a/Core/BackgroundVideoRecorder.cs b/Core/BackgroundVideoRecorder.cs new file mode 100644 index 0000000..48c65f9 --- /dev/null +++ b/Core/BackgroundVideoRecorder.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading; +using Gallio.Common.Media; +using Gallio.Framework; +using ProtoTest.Golem.WebDriver; + +namespace ProtoTest.Golem.Core +{ + public class BackgroundVideoRecorder + { + public BackgroundWorker bgWorker; + public Video video; + public Size screensize; + public int fps; + public int frameDelayMs; + public BackgroundVideoRecorder(int fps) + { + this.fps = fps; + this.frameDelayMs = 1000 / fps; + screensize = WebDriverTestBase.testData.driver.GetScreenshot().Size; + Common.Log("The current dimensions are : " + screensize.Width + " x " + screensize.Height); + video = new FlashScreenVideo(new FlashScreenVideoParameters(screensize.Width, screensize.Height, fps)); + bgWorker = new BackgroundWorker(); + bgWorker.DoWork += bgWorker_DoWork; + bgWorker.WorkerSupportsCancellation = true; + bgWorker.RunWorkerAsync(); + } + + void Stop() + { + bgWorker.CancelAsync(); + } + + void bgWorker_DoWork(object sender, DoWorkEventArgs e) + { + while (!bgWorker.CancellationPending) + { + + var bitmap = new Bitmap(WebDriverTestBase.testData.driver.GetScreenshot()); + Common.Log("Bitmap dimensions are : " + bitmap.Width + " x " + bitmap.Height); + if(bitmap!=null) + video.AddFrame(new BitmapVideoFrame(bitmap)); + + Thread.Sleep(frameDelayMs); + } + + } + } +} diff --git a/Core/Common.cs b/Core/Common.cs new file mode 100644 index 0000000..0f7a2c0 --- /dev/null +++ b/Core/Common.cs @@ -0,0 +1,227 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using TestContext = NUnit.Framework.TestContext; + +namespace Golem.Core +{ + /// + /// Random methods that don't belong anywhere else + /// + public class Common + { + /// + /// Return a psuedo-random string based on the current timestamp + /// + /// + public static string GetRandomString() + { + return DateTime.Now.ToString("ddHHmmssZ"); + } + + public static void KillProcess(string name) + { + var runningProcesses = Process.GetProcesses(); + foreach (var process in runningProcesses) + { + try + { + if (process.ProcessName == name) + { + Log("Killing Process : " + name); + process.Kill(); + } + } + catch (Exception) + { + } + } + } + + public static Process ExecuteBatchFile(string filePath) + { + if (!File.Exists(filePath)) + { + Assert.Fail("Could not find batch file : " + filePath); + } + + return Process.Start(filePath); + } + + public static Process ExecuteDosCommand(string command, bool waitToFinish = true) + { + Log("Executing DOS Command: " + command); + var CMDprocess = new Process(); + var StartInfo = new ProcessStartInfo(); + StartInfo.FileName = "cmd"; //starts cmd window + StartInfo.Arguments = "/c \"" + command + "\""; + //StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + // StartInfo.CreateNoWindow = true; + StartInfo.RedirectStandardInput = true; + StartInfo.RedirectStandardOutput = true; + StartInfo.UseShellExecute = false; //required to redirect + CMDprocess.StartInfo = StartInfo; + CMDprocess.Start(); + var SR = CMDprocess.StandardOutput; + var SW = CMDprocess.StandardInput; + CMDprocess.Start(); + var line = ""; + + while ((line != null) && (waitToFinish)) + { + line = SR.ReadLine(); + Log(line); + } + + SW.Close(); + SR.Close(); + return CMDprocess; + } + + public static void Log(string msg) + { + Core.Log.Message(msg); + } + + public static bool IsTruthy(string truth) + { + switch (truth) + { + case "1": + case "true": + case "True": + return true; + case "0": + case "false": + case "False": + default: + return false; + } + } + + public static string GetCurrentTestName() + { + string TestName = "Test"; + try + { + TestName = TestContext.CurrentContext.Test.FullName; + if (string.IsNullOrEmpty(TestName)) + { + TestName = NUnit.Framework.TestContext.CurrentContext.Test.Name; + } + } + catch (Exception e) + { + TestName = "Test"; + } + TestName = Regex.Replace(TestName, "[^a-zA-Z0-9%._]", string.Empty); + return TestName; + } + + public static string GetShortTestName(int length) + { + var name = GetCurrentTestName(); + name = name.Replace("/", "_"); + name = name.Replace(":", "_"); + name = name.Replace("\\", "_"); + name = name.Replace("\"", ""); + name = name.Replace(" ", ""); + if (name.Length > length) + { + name = name.Substring((name.Length - length), length); + } + + return name; + } + + public string GetValueFromXmlFile(string filepath, string xpath) + { + var configFile = new XmlDocument(); + configFile.Load(filepath); + return configFile.SelectSingleNode(xpath).Value ?? ""; + } + + public string GetConfigValue(string fileName, string xpath) + { + var configFile = new XmlDocument(); + configFile.Load(AppDomain.CurrentDomain.BaseDirectory + fileName); + return configFile.SelectSingleNode(xpath).Value ?? ""; + } + + public static TestStatus GetTestOutcome() + { + if (TestBase.testData.VerificationErrors.Count != 0) + { + return TestStatus.Failed; + } + return TestContext.CurrentContext.Result.Outcome.Status; + } + + public static Image ScaleImage(Image image, double scale = .5) + { + var newWidth = (int) (image.Width*scale); + var newHeight = (int) (image.Height*scale); + + var newImage = new Bitmap(newWidth, newHeight); + Graphics.FromImage(newImage).DrawImage(image, 0, 0, newWidth, newHeight); + return newImage; + } + + public static Image ResizeImage(Image image, int newWidth, int newHeight) + { + var newImage = new Bitmap(newWidth, newHeight); + Graphics.FromImage(newImage).DrawImage(image, 0, 0, newWidth, newHeight); + return newImage; + } + + public static void Delay(int delayMs) + { + if (delayMs > 0) + { + Thread.Sleep(delayMs); + } + } + + /// + /// Create a dummy file with some ASCII + /// + /// File path and name to create + public static void CreateDummyFile(string filepath) + { + if (!File.Exists(filepath)) + { + using (var fs = File.Create(filepath)) + { + for (byte i = 0; i < 100; i++) + { + fs.WriteByte(i); + } + + fs.Close(); + } + } + } + + public static string GetCodeDirectory() + { + return AppDomain.CurrentDomain.BaseDirectory.Replace(@"\bin\Debug", "").Replace(@"\bin\Release", ""); + } + + public static void LogImage(Image image) + { + + } + + public static Size GetSizeFromResolution(string resolution) + { + var dimensions = resolution.Split('x'); + return new Size(int.Parse(dimensions[0]), int.Parse(dimensions[1])); + } + } +} \ No newline at end of file diff --git a/Core/Config.cs b/Core/Config.cs new file mode 100644 index 0000000..927dc08 --- /dev/null +++ b/Core/Config.cs @@ -0,0 +1,419 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using Golem.WebDriver; + +namespace Golem.Core +{ + /// + /// The Config class holds instantiates the ConfigSettings class, and any Config-related functions + /// + public class Config + { + public static ConfigSettings settings + { + get { return TestBase.testData.configSettings; } + set { TestBase.testData.configSettings = value; } + } + + /// + /// Returns the App.config value for requested key, or default value if not defined. + /// + /// Application configuration key + /// Default value + /// + public static string GetConfigValue(string key, string defaultValue) + { + var setting = ConfigurationManager.AppSettings[key]; + if (setting == null) + { + return defaultValue; + } + + return setting; + } + + public static ConfigSettings GetDefaultConfig() + { + if (TestBase.testDataCollection.Count == 0) + { + return new ConfigSettings(); + } + else + { + return TestBase.testDataCollection.FirstOrDefault(x=>x.Value.configSettings!=null).Value.configSettings; + } + } + + /// + /// Returns the App.config value for requested key, or default value if not defined, and tries to parse it for an byte. + /// + /// Application configuration key + /// Default value + /// + public static byte GetConfigValueAsByte(string key, string defaultValue) + { + var setting = ""; + try + { + setting = GetConfigValue(key, defaultValue); + return byte.Parse(setting); + } + catch (Exception e) + { + Log.Warning(string.Format( + "Exception Reading App.Config. Using key='{0}', got a result of : '{1}'. Using 1 as default. {2}", + key, setting, e.Message)); + return 1; + } + } + + /// + /// Returns the App.config value for requested key, or default value if not defined, and tries to parse it for an int. + /// + /// Application configuration key + /// Default value + /// + public static int GetConfigValueAsInt(string key, string defaultValue) + { + var setting = ""; + try + { + setting = GetConfigValue(key, defaultValue); + return int.Parse(setting); + } + catch (Exception e) + { + Log.Warning(string.Format( + "Exception Reading App.Config. Using key='{0}', got a result of : '{1}'. Using 1 as default. {2}", + key, setting, e.Message)); + return 1; + } + } + + /// + /// Returns the App.config value for requested key, or default value if not defined and returns a boolean. Looks for + /// True, true, False, false. + /// + /// Application configuration key + /// Default value + /// + public static bool GetConfigValueAsBool(string key, string defaultValue) + { + var setting = ConfigurationManager.AppSettings[key]; + if (setting == null) + { + setting = defaultValue; + } + return Common.IsTruthy(setting); + } + + /// + /// Updates the App.config setting key with value + /// + /// Application configuration key + /// Application configuration key value to set + public static void UpdateConfigFile(string key, string value) + { + var doc = new XmlDocument(); + var path = Assembly.GetCallingAssembly().Location + ".config"; + doc.Load(path); + doc.SelectSingleNode("//add[@key='" + key + "']").Attributes["value"].Value = value; + doc.Save(path); + + path = AppDomain.CurrentDomain.BaseDirectory.Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\App.config"; + doc.Load(path); + doc.SelectSingleNode("//add[@key='" + key + "']").Attributes["value"].Value = value; + doc.Save(path); + } + } + + /// + /// ConfigSettings holds each config section, and reads them in from the App.Config upon instantiation. To override + /// these settings put the commands in a [FixtureInitializer] + /// + public class ConfigSettings + { + public BrowserStackSettings browserStackSettings; + public HttpProxy httpProxy; + public ImageCompareSettings imageCompareSettings; + public PurpleSettings purpleSettings; + public ReportSettings reportSettings; + public RuntimeSettings runTimeSettings; + public SauceLabsSettings sauceLabsSettings; + public BrowserSettings browserSettings; + + public ConfigSettings() + { + browserStackSettings = new BrowserStackSettings(); + httpProxy = new HttpProxy(); + imageCompareSettings = new ImageCompareSettings(); + purpleSettings = new PurpleSettings(); + reportSettings = new ReportSettings(); + runTimeSettings = new RuntimeSettings(); + sauceLabsSettings = new SauceLabsSettings(); + browserSettings = new BrowserSettings(); + } + /// + /// Contains all settings related to the BrowserMobProxy + /// + public class HttpProxy + { + public bool killOldProxy; + public int proxyPort; + public int proxyServerPort; + public string proxyUrl; + public bool startProxy; + public bool useProxy; + public bool validateTraffic; + + public HttpProxy() + { + proxyServerPort = Config.GetConfigValueAsInt("ProxyServerPort", "18880"); + startProxy = Config.GetConfigValueAsBool("StartProxy", "False"); + useProxy = Config.GetConfigValueAsBool("UseProxy", "False"); + proxyPort = Config.GetConfigValueAsInt("ProxyPort", "18881"); + proxyUrl = Config.GetConfigValue("ProxyUrl", "localhost"); + validateTraffic = Config.GetConfigValueAsBool("ValidateTraffic", "False"); + killOldProxy = Config.GetConfigValueAsBool("KillOldProxy", "True"); + } + } + + /// + /// Contains settings for image comparison. + /// + public class ImageCompareSettings + { + public float accuracy; + public byte fuzziness; + public bool updateImages; + + public ImageCompareSettings() + { + fuzziness = Config.GetConfigValueAsByte("Fuzziness", "50"); + accuracy = float.Parse(Config.GetConfigValue("Accuracy", "1"))/100; + updateImages = Config.GetConfigValueAsBool("UpdateImages", "false"); + } + } + + /// + /// Specify what should show up in the report + /// + public class ReportSettings + { + private static string timestamp = DateTime.Now.ToString("MMdd_HHmm"); + public string reportRoot; + public string reportPath; + public string reportTimestamp; + public bool actionLogging; + public bool commandLogging; + public bool diagnosticLog; + public bool htmlOnError; + public bool screenshotOnError; + public bool spellChecking; + public bool testLog; + public bool videoRecordingOnError; + public int numReports; + public bool relativePath; + public bool compressToZip; + + public ReportSettings() + { + reportTimestamp = timestamp; + relativePath = Config.GetConfigValueAsBool("RelativePath", "False"); + htmlOnError = Config.GetConfigValueAsBool("HtmlOnError", "False"); + screenshotOnError = Config.GetConfigValueAsBool("ScreenshotOnError", "True"); + videoRecordingOnError = Config.GetConfigValueAsBool("VideoRecordingOnError", "True"); + commandLogging = Config.GetConfigValueAsBool("CommandLogging", "True"); + actionLogging = Config.GetConfigValueAsBool("ActionLogging", "True"); + spellChecking = Config.GetConfigValueAsBool("SpellChecking", "False"); + diagnosticLog = Config.GetConfigValueAsBool("DiagnosticLog", "True"); + testLog = Config.GetConfigValueAsBool("TestLog", "True"); + numReports = Config.GetConfigValueAsInt("NumReports", "10"); + reportRoot = Config.GetConfigValue("ReportPath", Path.Combine(Common.GetCodeDirectory(), "reports")); + reportPath = Path.Combine(reportRoot, timestamp); + compressToZip = Config.GetConfigValueAsBool("CompressToZip", "False"); + } + } + + /// + /// Specify execution settings + /// + public class RuntimeSettings + { + public bool AutoWaitForElements; + public string BrowserResolution; + public IEnumerable Browsers = new List(); + public int CommandDelayMs; + public int DegreeOfParallelism; + public int ElementTimeoutSec; + public string EnvironmentUrl; + public bool FindHiddenElements; + public bool HighlightFoundElements; + public string HostIp; + public bool LaunchBrowser; + public int OpenWindowTimeoutSec; + public int PageTimeoutSec; + public string RemoteHostPort; + public bool RunOnRemoteHost; + public int TestTimeoutMin; + + public RuntimeSettings() + { + Browsers = GetBrowserInfo(); + LaunchBrowser = Config.GetConfigValueAsBool("LaunchBrowser", "True"); + TestTimeoutMin = Config.GetConfigValueAsInt("TestTimeoutMin", "5"); + ElementTimeoutSec = Config.GetConfigValueAsInt("ElementTimeoutSec", "10"); + OpenWindowTimeoutSec = Config.GetConfigValueAsInt("WindowOpenTimeoutSec", "10"); + PageTimeoutSec = Config.GetConfigValueAsInt("PageTimeoutSec", "30"); + EnvironmentUrl = Config.GetConfigValue("EnvironmentUrl", ""); + DegreeOfParallelism = Config.GetConfigValueAsInt("DegreeOfParallelism", "5"); + CommandDelayMs = Config.GetConfigValueAsInt("CommandDelayMs", "0"); + RunOnRemoteHost = Config.GetConfigValueAsBool("RunOnRemoteHost", "False"); + RemoteHostPort = Config.GetConfigValue("RemoteHostPort", "4444"); + HostIp = Config.GetConfigValue("HostIp", "localhost"); + AutoWaitForElements = Config.GetConfigValueAsBool("AutoWaitForElements", "True"); + HighlightFoundElements = Config.GetConfigValueAsBool("HighlightFoundElements", "True"); + BrowserResolution = Config.GetConfigValue("BrowserResolution", "Default"); + FindHiddenElements = Config.GetConfigValueAsBool("FindHiddenElements", "True"); + } + + public WebDriverBrowser.Browser Browser + { + get { return TestBase.testData.browserInfo.browser; } + set { TestBase.testData.browserInfo.browser = value; } + } + + private List GetBrowserInfo() + { + var hosts = new List(); + var browser = Config.GetConfigValue("Browser", "null"); + var caps = Config.GetConfigValue("BrowserCapabilities", "null"); + if (browser != "null") + { + hosts.Add(new BrowserInfo(WebDriverBrowser.getBrowserFromString(browser), caps)); + } + for (var i = 1; i < 10; i++) + { + browser = Config.GetConfigValue("Browser" + i, "null"); + if (browser != "null") + { + caps = Config.GetConfigValue("Browser" + i + "Capabilities", "null"); + hosts.Add(new BrowserInfo(WebDriverBrowser.getBrowserFromString(browser), caps)); + } + } + if (hosts.Count == 0) + { + hosts.Add(new BrowserInfo(WebDriverBrowser.Browser.Chrome)); + } + return hosts; + } + + + } + + /// + /// settings for TestStack.White module + /// + public class PurpleSettings + { + public string appPath; + public string DataSetPath1; + public string DataSetPath2; + public string DataSetPath3; + public string DataSetPath4; + public bool launchApp; + public string Machine1; + public string Machine2; + public string Machine3; + public string Machine4; + public string ProcessName; + public string ProjectName1; + public string ProjectName2; + public string ProjectName3; + public string ProjectName4; + public string Purple_blankValue; + public string Purple_Delimiter; + public int Purple_ElementTimeoutWaitSeconds; + public string Purple_ValueDelimiterEnd; + public string Purple_ValueDelimiterStart; + public string Purple_windowTitle; + + public PurpleSettings() + { + appPath = Config.GetConfigValue("AppPath", "NOT_SET"); + launchApp = Config.GetConfigValueAsBool("LaunchApp", "True"); + ProcessName = Config.GetConfigValue("ProcessName", "NOT SET"); + Purple_windowTitle = Config.GetConfigValue("Purple_WindowTitle", "EMPTY"); + Purple_blankValue = Config.GetConfigValue("Purple_BlankValue", "!BLANK!"); + Purple_Delimiter = Config.GetConfigValue("Purple_Delimiter", "/"); + Purple_ValueDelimiterStart = Config.GetConfigValue("Purple_ValueDelimiterStart", "["); + Purple_ValueDelimiterEnd = Config.GetConfigValue("Purple_ValueDelimiterEnd", "]"); + Purple_ElementTimeoutWaitSeconds = Config.GetConfigValueAsInt("Purple_ElementWaitTimeOutSeconds", "0"); + Machine1 = Config.GetConfigValue("Machine1", "NOT_SET"); + Machine2 = Config.GetConfigValue("Machine2", "NOT_SET"); + Machine3 = Config.GetConfigValue("Machine3", "NOT_SET"); + Machine4 = Config.GetConfigValue("Machine4", "NOT_SET"); + DataSetPath1 = Config.GetConfigValue("DS1", "NOT_SET"); + DataSetPath2 = Config.GetConfigValue("DS2", "NOT_SET"); + DataSetPath3 = Config.GetConfigValue("DS3", "NOT_SET"); + DataSetPath4 = Config.GetConfigValue("DS4", "NOT_SET"); + ProjectName1 = Config.GetConfigValue("PrjName1", "NOT SET"); + ProjectName2 = Config.GetConfigValue("PrjName2", "NOT SET"); + ProjectName3 = Config.GetConfigValue("PrjName3", "NOT SET"); + ProjectName4 = Config.GetConfigValue("PrjName4", "NOT SET"); + } + } + + public class BrowserStackSettings + { + public string BrowserStack_Key; + public string BrowserStack_OS; + public string BrowserStack_OS_Version; + public string BrowserStack_User; + public string BrowserStackRemoteURL; + + public BrowserStackSettings() + { + BrowserStack_User = Config.GetConfigValue("BrowserStack_User", null); + BrowserStack_Key = Config.GetConfigValue("BrowserStack_Key", null); + BrowserStack_OS = Config.GetConfigValue("BrowserStack_OS", null); + BrowserStack_OS_Version = Config.GetConfigValue("BrowserStack_OS_Version", null); + BrowserStackRemoteURL = Config.GetConfigValue("BrowserStackRemoteURL", "hub.browserstack.com"); + } + } + + public class SauceLabsSettings + { + public string SauceLabsAPIKey; + public string SauceLabsUrl; + public string SauceLabsUsername; + public string ScreenResolution; + public bool UseSauceLabs; + + public SauceLabsSettings() + { + UseSauceLabs = Config.GetConfigValueAsBool("UseSauceLabs", "False"); + SauceLabsUrl = Config.GetConfigValue("SauceLabsUrl", "http://ondemand.saucelabs.com/wd/hub"); + SauceLabsUsername = Config.GetConfigValue("SauceLabsUsername", "NOT_SET"); + SauceLabsAPIKey = Config.GetConfigValue("SauceLabsAPIKey", "NOT_SET"); + ScreenResolution = Config.GetConfigValue("ScreenResolution", "1280x1024"); + } + } + + public class BrowserSettings + { + public string UserAgent; + + public BrowserSettings() + { + UserAgent = Config.GetConfigValue("UserAgent", ""); + } + } + } +} \ No newline at end of file diff --git a/Core/CurrentProcessInfo.cs b/Core/CurrentProcessInfo.cs new file mode 100644 index 0000000..aaa764e --- /dev/null +++ b/Core/CurrentProcessInfo.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace Golem.Core +{ + public class CurrentProcessInfo + { + private readonly Type commandInterface; + private readonly string formatString = "{0}.{1}()_{2}.{3}() : "; + private readonly Type pageObjectType; + public string className = ""; + public string commandName = ""; + public string elementName = ""; + public string methodName = ""; + + public CurrentProcessInfo(Type pageObjectType, Type commandInterface) + { + this.pageObjectType = pageObjectType; + this.commandInterface = commandInterface; + Init(); + } + + public string GetString() + { + return string.Format(formatString, className, methodName, elementName, commandName); + } + + private void Init() + { + var stackTrace = new StackTrace(); + var stackFrames = stackTrace.GetFrames(); + foreach (var stackFrame in stackFrames) + { + var method = stackFrame.GetMethod(); + var type = stackFrame.GetMethod().ReflectedType; + if ((type.BaseType == pageObjectType) && (!stackFrame.GetMethod().IsConstructor)) + { + className = type.Name; + methodName = stackFrame.GetMethod().Name; + } + if (type.GetInterfaces().Contains(commandInterface)) + { + commandName = stackFrame.GetMethod().Name; + elementName = type.Name; + } + } + } + } +} \ No newline at end of file diff --git a/Core/HtmlReportGenerator.cs b/Core/HtmlReportGenerator.cs new file mode 100644 index 0000000..e38fce0 --- /dev/null +++ b/Core/HtmlReportGenerator.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Text; +using System.Web.UI; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using ResultState = NUnit.Framework.Interfaces.ResultState; + +namespace Golem.Core +{ + public class HtmlReportGenerator + { + public StringWriter stringWriter; + public HtmlTextWriter htmlTextWriter; + + public HtmlReportGenerator() + { + this.stringWriter = new StringWriter(); + this.htmlTextWriter = new HtmlTextWriter(stringWriter); + + } + + public string ConvertPathToRelative(string path) + { + path = path.Replace("file:///", ""); + + var reportpath = Config.settings.reportSettings.reportPath; + var relative_path = path.Replace(reportpath, "").Replace(reportpath.Replace("\\", "/"), ""); + + if (relative_path.StartsWith("\\")) + { + relative_path = relative_path.Replace("\\", "/"); + } + + return "." + relative_path; + } + + public void GenerateHtml() + { + GenerateStartTags(); + GenerateSuiteBody(); + GenerateEndTags(); + } + + public void WriteToFile() + { + var path = $"{Config.settings.reportSettings.reportPath}\\{Common.GetCurrentTestName()}.html".Replace("\\", "/"); + var fullPath = Path.GetFullPath(path).Replace("\\", "/"); + var css_path = $"{Common.GetCodeDirectory()}\\dashboard.css"; + var final_path = $"{Config.settings.reportSettings.reportPath}\\dashboard.css"; + + if (!File.Exists(Path.GetFullPath(final_path))) + { + File.Copy(css_path, Path.GetFullPath(final_path)); + } + + var teamcity = Environment.GetEnvironmentVariable("TEAMCITY_VERSION"); + if (teamcity != null) + { + TestContext.WriteLine("url(file:///" + fullPath + ")"); + } + else + { + TestContext.WriteLine(@"file:///" + fullPath); + } + + File.WriteAllText(fullPath, this.stringWriter.ToString()); + TestBase.testData.ReportPath = fullPath; + } + + public void GenerateStartTags() + { + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(" "); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine("Test Dashboard"); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + // this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteBeginTag("body"); + } + + public void GenerateSuiteHeader(string name) + { + this.htmlTextWriter.WriteLine("
"); + } + + public void GenerateSuiteFooter() + { + this.htmlTextWriter.WriteLine("
"); + } + + public void GenerateLogHeader() + { + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table-responsive"); + this.htmlTextWriter.RenderBeginTag("div"); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table table-striped log-table"); + this.htmlTextWriter.RenderBeginTag("table"); + + this.htmlTextWriter.RenderBeginTag("thead"); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-timestamp"); + this.htmlTextWriter.RenderBeginTag("th"); + this.htmlTextWriter.Write("Timestamp"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-text"); + this.htmlTextWriter.RenderBeginTag("th"); + this.htmlTextWriter.Write("Text"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("tbody"); + + } + + public void GenerateIndexHead() + { + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "sub-header"); + this.htmlTextWriter.RenderBeginTag("h2"); + this.htmlTextWriter.Write("Index"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table-responsive"); + this.htmlTextWriter.RenderBeginTag("div"); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table table-striped log-table"); + this.htmlTextWriter.RenderBeginTag("table"); + + this.htmlTextWriter.RenderBeginTag("thead"); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Name"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Status"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("tbody"); + + } + + public void GenerateIndexRow(string name, string url, string status, string message) + { + if (Config.settings.reportSettings.relativePath) + { + url = ConvertPathToRelative(url); + } + + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, url); + this.htmlTextWriter.RenderBeginTag("a"); + + this.htmlTextWriter.Write(name); + this.htmlTextWriter.RenderEndTag(); //a + this.htmlTextWriter.RenderEndTag(); //td + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(status); + this.htmlTextWriter.RenderEndTag(); //td + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(message); + this.htmlTextWriter.RenderEndTag(); //td + + this.htmlTextWriter.RenderEndTag(); //tr + } + + public void GenerateLogStatus(string statusMessage, string exceptionMessage, string stackTrace, string screenshotPath, string videoPath) + { + screenshotPath = ConvertPathToRelative(screenshotPath); + videoPath = ConvertPathToRelative(videoPath); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-status"); + this.htmlTextWriter.RenderBeginTag("div"); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "status"); + this.htmlTextWriter.RenderBeginTag("div"); + this.htmlTextWriter.RenderBeginTag("h2"); + this.htmlTextWriter.Write($"{TestBase.testData.ClassName} {TestBase.testData.MethodName} {TestBase.testData.Status}"); + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "exception-message"); + this.htmlTextWriter.RenderBeginTag("div"); + this.htmlTextWriter.Write(this.escape_text(exceptionMessage)); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "exception-stacktrace"); + this.htmlTextWriter.RenderBeginTag("div"); + this.htmlTextWriter.Write(this.escape_text(stackTrace)); + this.htmlTextWriter.RenderEndTag(); + + if (TestBase.testData.Result.Outcome.Status != TestStatus.Passed) + { + this.htmlTextWriter.RenderBeginTag("div"); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "screenshot"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Src, screenshotPath); + this.htmlTextWriter.RenderBeginTag("img"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + } + + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, videoPath); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-video"); + this.htmlTextWriter.RenderBeginTag("a"); + this.htmlTextWriter.WriteLine("Video.flv"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + + } + + public void GenerateLogEnd() + { + this.htmlTextWriter.RenderEndTag();//tbody + this.htmlTextWriter.RenderEndTag();//table + this.htmlTextWriter.RenderEndTag();//div + } + + public string escape_text(string text) + { + try + { + return text.Replace("<", "<").Replace(">", ">"); + } + catch (Exception) + { + return ""; + } + + } + + public void GenerateLogMessage(string timestamp, string value) + { + GenerateLogRow(timestamp,value); + } + + public void GenerateLogError(string timestamp, string value) + { + GenerateLogRow(timestamp, value,"error"); + } + + public void GenerateLogWarning(string timestamp, string value) + { + GenerateLogRow(timestamp, value, "warning"); + } + + public void GenerateLogRow(string timestamp, string value, string type="message") + { + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-" + type); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.RenderBeginTag("div"); + this.htmlTextWriter.Write(timestamp); + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(this.escape_text(value)); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + } + + public void GenerateLogImage(string timestamp, string path) + { + path = ConvertPathToRelative(path); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(timestamp); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-image"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Width, "75%"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Src, path); + this.htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Img); + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + } + + public void GenerateLogVideo(string timestamp, string path) + { + path = ConvertPathToRelative(path); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(timestamp); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, path); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-video"); + this.htmlTextWriter.RenderBeginTag("a"); + this.htmlTextWriter.WriteLine("Video.flv"); + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + } + + public void GenerateLogLink(string timestamp, string path) + { + path = ConvertPathToRelative(path); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write(timestamp); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "log-link"); + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, path); + this.htmlTextWriter.RenderBeginTag("a"); + this.htmlTextWriter.WriteLine(path); + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + } + + + public void GenerateSuiteBody() + { + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "sub-header"); + this.htmlTextWriter.RenderBeginTag("h2"); + this.htmlTextWriter.Write("Log"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table-responsive"); + this.htmlTextWriter.RenderBeginTag("div"); + + + this.htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Class, "table table-striped"); + this.htmlTextWriter.RenderBeginTag("table"); + + this.htmlTextWriter.RenderBeginTag("thead"); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("#"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Name"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Status"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Pass/Fail"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag(); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("tbody"); + this.htmlTextWriter.RenderBeginTag("tr"); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("1"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("TestClass1"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("Pass"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderBeginTag("td"); + this.htmlTextWriter.Write("10/10"); + this.htmlTextWriter.RenderEndTag(); + + this.htmlTextWriter.RenderEndTag();//tr + this.htmlTextWriter.RenderEndTag();//tbody + + this.htmlTextWriter.RenderEndTag();//table + this.htmlTextWriter.RenderEndTag();//div + } + + public void GenerateEndTags() + { + this.htmlTextWriter.WriteLine(""); + // this.htmlTextWriter.WriteLine("') "); + this.htmlTextWriter.WriteLine(""); + // this.htmlTextWriter.WriteLine(""); + // this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + this.htmlTextWriter.WriteLine(""); + } + + public void WriteToIndexFile() + { + var items = TestBase.testDataCollection; + var path = $"{Config.settings.reportSettings.reportPath}\\index.html"; + var css_path = $"{Common.GetCodeDirectory()}\\dashboard.css"; + var final_path = $"{Config.settings.reportSettings.reportPath}\\dashboard.css"; + if (!File.Exists(final_path)) File.Copy(css_path, final_path); + TestContext.WriteLine(@"file:\\\" + path); + File.WriteAllText(path, this.stringWriter.ToString()); + + } + + public void GenerateIndexSummary() + { + int totalTests = TestBase.testDataCollection.Count(X => X.Value.Result != null); + int totalPassed = TestBase.testDataCollection.Where(x => x.Value.Result != null && x.Value.Result.Outcome == ResultState.Success).Count(); + int totalFailed = TestBase.testDataCollection.Where(x => x.Value.Result != null && x.Value.Result.Outcome == ResultState.Failure).Count(); + int totalSkipped = TestBase.testDataCollection.Where(x => x.Value.Result != null && x.Value.Result.Outcome == ResultState.Skipped).Count(); + this.htmlTextWriter.RenderBeginTag("div"); + if (totalPassed == totalTests) + { + this.htmlTextWriter.AddAttribute("class","success"); + this.htmlTextWriter.RenderBeginTag("h2"); + this.htmlTextWriter.Write($"SUCCESS! All tests passed"); + this.htmlTextWriter.RenderEndTag(); + } + else + { + this.htmlTextWriter.AddAttribute("class", "failure"); + this.htmlTextWriter.RenderBeginTag("h2"); + this.htmlTextWriter.Write($"FAILURE! {(float)totalPassed/(float)totalTests}% {totalPassed}/{totalTests} tests passed ({totalSkipped} Skipped)"); + this.htmlTextWriter.RenderEndTag(); + } + this.htmlTextWriter.RenderEndTag(); + + // number of tests pass / skip / total + // graph and % pass + // + } + } +} diff --git a/Core/Log.cs b/Core/Log.cs new file mode 100644 index 0000000..2c36074 --- /dev/null +++ b/Core/Log.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using Gallio.Common.Media; +using NUnit.Framework; + +namespace Golem.Core +{ + public class Log + { + public static void Message(string text, ActionList.Action.ActionType type=ActionList.Action.ActionType.Other) + { + try + { + TestBase.overlay.Text = text; + Debug.WriteLine(string.Format("DEBUG : ({0}) : {1}", DateTime.Now.ToString("HH:mm:ss::ffff"), text)); +// TestContext.WriteLine(string.Format("({0}) : {1}", DateTime.Now.ToString("HH:mm:ss::ffff"), text)); + TestBase.testData.LogEvent(text, type); + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + } + } + + public static void Failure(string text) + { + try + { + Message("FAILURE: " + text, ActionList.Action.ActionType.Error); + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + } + } + + public static string Image(Image image) + { + try + { + String path = Path.GetFullPath(Path.Combine(Config.settings.reportSettings.reportPath, "screenshot_" + DateTime.Now.ToString("HHms") + ".png")); + image.Save(path, ImageFormat.Png); + Message("file:///" + path, ActionList.Action.ActionType.Image); + return path; + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + return ""; + } + } + + public static string Video(Video video) + { + try + { + String path = Path.GetFullPath(Path.Combine(Config.settings.reportSettings.reportPath, "Video_" + Common.GetShortTestName(90) + DateTime.Now.ToString("HHms") + ".flv")); + FileStream fs = new FileStream(path, FileMode.Create); + video.Save(fs); + Message(path, ActionList.Action.ActionType.Video); + return path; + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + return ""; + } + } + + public static void FilePath(string file) + { + Message("file:///" + file, ActionList.Action.ActionType.Link); + } + + public static void Html(string name, string html) + { + try + { + String path = Path.GetFullPath(Path.Combine(Config.settings.reportSettings.reportPath, "html" + DateTime.Now.ToString("HHms") + ".html")); + if (!File.Exists(path)) + { + File.WriteAllText(path, html); + } + Message("file:///" + path, ActionList.Action.ActionType.Link); + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + } + } + + public static void Warning(string message) + { + try + { + Message("WARNING: " + message, ActionList.Action.ActionType.Warning); + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + } + } + + public static void Error(string message) + { + try + { + Message("ERROR: " + message, ActionList.Action.ActionType.Error); + } + catch (Exception e) + { + TestContext.WriteLine(e.Message); + } + } + + public static void Error(string message, Image image) + { + Error(message); + Image(image); + } + } +} diff --git a/Core/ProcessRunner.cs b/Core/ProcessRunner.cs new file mode 100644 index 0000000..cc066c2 --- /dev/null +++ b/Core/ProcessRunner.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace Golem.Core +{ + public class ProcessRunner + { + private static bool isStarted; + private readonly string command; + private Process process; + + public ProcessRunner(string command) + { + this.command = command; + CreateBatchFile(); + } + + private void CreateBatchFile() + { + using (var sw = File.CreateText(batchPath)) + { + sw.WriteLine(command); + } + } + + public void StopProcess() + { + try + { + process.CloseMainWindow(); + process.Kill(); + isStarted = false; + } + catch (Exception) + { + } + } + + public void StartProcess() + { + if (!isStarted) + { + process = new Process(); + var StartInfo = new ProcessStartInfo(); + StartInfo.FileName = batchPath; + StartInfo.WindowStyle = ProcessWindowStyle.Normal; + StartInfo.CreateNoWindow = false; + process.StartInfo = StartInfo; + process.Start(); + isStarted = true; + } + } + + private static readonly string batchDir = Directory.GetCurrentDirectory(); + private static readonly string batchName = "batch.bat"; + private static readonly string batchPath = batchDir + "\\" + batchName; + } +} \ No newline at end of file diff --git a/Core/TestBase.cs b/Core/TestBase.cs new file mode 100644 index 0000000..65c60bd --- /dev/null +++ b/Core/TestBase.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Web.UI; +using Gallio.Common.Media; +using Gallio.Framework; +using NUnit.Framework; +using Golem.Proxy; +using Golem.WebDriver; +using Ionic.Zip; +using TestContext = NUnit.Framework.TestContext; +using TestStatus = NUnit.Framework.Interfaces.TestStatus; + +namespace Golem.Core +{ + public abstract class TestBase + { + public static CaptionOverlay overlay = new CaptionOverlay + { + FontSize = 20, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Bottom + }; +// public static string reportPath; + protected static Object locker = new object(); + public static BrowserMobProxy proxy; + private static IDictionary _testDataCollection; + + public static IDictionary testDataCollection + { + get { return _testDataCollection ?? (_testDataCollection = new Dictionary()); } + set { _testDataCollection = value; } + } + + public static TestDataContainer testData + { + get + { + var name = Common.GetCurrentTestName(); + var exists = testDataCollection.ContainsKey(name); + if (!exists) + { + lock (locker) + { + var container = new TestDataContainer(name); + testDataCollection.Add(name, container); + return container; + } + } + return testDataCollection[name]; + } + } + + [SetUp] + public virtual void SetUpTestBase() + { + Log.Message(Common.GetCurrentTestName() + " started"); + testData.ClassName = TestContext.CurrentContext.Test.ClassName; + testData.MethodName = TestContext.CurrentContext.Test.MethodName; + Config.settings = new ConfigSettings(); + StartNewProxy(); + StartVideoRecording(); + } + + [TearDown] + public virtual void TearDownTestBase() + { + try + { + LogTestOutcome(); + VerifyHttpTraffic(); + GetHarFile(); + QuitProxy(); + LogVideo(); + AssertNoVerificationErrors(); + } + catch (Exception e) + { + CreateHtmlReport(); + DeleteTestData(); + throw e; + } + finally + { + CreateHtmlReport(); + DeleteTestData(); + } + + } + + private void LogTestOutcome() + { + Log.Message(Common.GetCurrentTestName() + " " + Common.GetTestOutcome()); + + if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed) + { + Log.Error(TestContext.CurrentContext.Result.Message); + Log.Error(TestContext.CurrentContext.Result.StackTrace); + testData.ExceptionMessage = TestContext.CurrentContext.Result.Message; + + } + testData.Status = TestContext.CurrentContext.Result.Outcome.Status.ToString(); + testData.Result = TestContext.CurrentContext.Result; + } + + private void VerifyHttpTraffic() + { + if (Config.settings.httpProxy.useProxy && Config.settings.httpProxy.validateTraffic) + { + proxy.VerifyNoErrorsCodes(); + } + } + + private void DeleteOldReports() + { + try + { + var dirs = Directory.GetDirectories(Config.settings.reportSettings.reportRoot); + int count = dirs.Length; + for (int i = Config.settings.reportSettings.numReports; i < count; i++) + { + string dir = dirs[i - Config.settings.reportSettings.numReports]; + Directory.Delete(dir, true); + } + + } + catch (Exception e) + { + Log.Warning(e.Message); + } + + + } + + [OneTimeSetUp] + public virtual void SuiteSetUp() + { + var context = TestContext.CurrentContext; + CreateReportDirectory(); + SetTestExecutionSettings(); + StartProxyServer(); + } + + + private void CreateReportDirectory() + { + lock (locker) + { + + DeleteOldReports(); + Directory.CreateDirectory(Config.settings.reportSettings.reportPath); + var path = $"{Config.settings.reportSettings.reportPath}\\{Common.GetCurrentTestName()}.html"; + TestContext.WriteLine(path); + Debug.WriteLine(path); + } + } + + + [OneTimeTearDown] + public virtual void SuiteTearDown() + { + StopVideoRecording(); + QuitProxyServer(); + CreateHtmlIndex(); + CompressToZipFile(); + Config.settings = new ConfigSettings(); + } + + private void CreateHtmlIndex() + { + HtmlReportGenerator gen = new HtmlReportGenerator(); + gen.GenerateStartTags(); + gen.GenerateIndexHead(); + gen.GenerateIndexSummary(); + var results = testDataCollection.OrderBy(x => x.Key).ToList(); + string classname = ""; + bool first = true; + foreach (var item in results) + { + if (item.Value.ClassName != classname) + { + if (first == true) + { + first = false; + } + else + { + gen.GenerateSuiteFooter(); + } + gen.GenerateSuiteHeader(item.Value.ClassName); + classname = item.Value.ClassName; + } + + if (item.Value.MethodName != null) + { + gen.GenerateIndexRow(item.Key, item.Value.ReportPath, item.Value.Status, item.Value.ExceptionMessage); + } + } + gen.GenerateLogEnd(); + gen.GenerateEndTags(); + gen.WriteToIndexFile(); + } + + private void CreateHtmlReport() + { + HtmlReportGenerator gen = new HtmlReportGenerator(); + gen.GenerateStartTags(); + gen.GenerateLogHeader(); + gen.GenerateLogStatus(testData.Result.Outcome.Status.ToString(),testData.Result.Message,testData.Result.StackTrace, testData.ScreenshotPath,testData.VideoPath); + foreach (var item in testData.actions.actions) + { + switch (item.type) + { + case ActionList.Action.ActionType.Link: + gen.GenerateLogLink(item.time.ToLongTimeString(), item.name); + break; + case ActionList.Action.ActionType.Video: + gen.GenerateLogVideo(item.time.ToLongTimeString(), item.name); + break; + case ActionList.Action.ActionType.Image: + gen.GenerateLogImage(item.time.ToLongTimeString(), item.name); + break; + case ActionList.Action.ActionType.Error: + gen.GenerateLogError(item.time.ToLongTimeString(), item.name); + break; + case ActionList.Action.ActionType.Warning: + gen.GenerateLogWarning(item.time.ToLongTimeString(), item.name); + break; + case ActionList.Action.ActionType.Message: + gen.GenerateLogMessage(item.time.ToLongTimeString(), item.name); + break; + + default: + gen.GenerateLogRow(item.time.ToLongTimeString(), item.name); + break; + } + + } + gen.GenerateLogEnd(); + gen.GenerateEndTags(); + gen.WriteToFile(); + } + + private void CompressToZipFile() + { + if (!Config.settings.reportSettings.compressToZip) + { + return; + } + + string path = $"{Config.settings.reportSettings.reportPath}"; + + var files = Directory.GetFiles(path); + if (files.Length == 0) + { + return; + } + + using (ZipFile zip = new ZipFile()) + { + foreach (string src in files) + { + zip.AddFile(src, string.Empty); + } + zip.Save($"{path}/report.zip"); + } + } + + private void DeleteTestData() + { + var testName = Common.GetCurrentTestName(); + if (!testDataCollection.ContainsKey(testName)) + { + testDataCollection.Remove(testName); + } + } + public static void LogVerificationPassed(string successText) + { + Log.Message("--> VerificationError Passed: " + successText); +// TestContext.CurrentContext.IncrementAssertCount(); + } + + public static void AddVerificationError(string errorText) + { + Log.Error("--> VerificationError Found: " + errorText); + var error = new VerificationError(errorText, + Config.settings.reportSettings.screenshotOnError); + testData.VerificationErrors.Add(error); + if (error.screenshot != null) + { + Log.Image(error.screenshot); + } + +// TestContext.CurrentContext.IncrementAssertCount(); + } + + public static void AddVerificationError(string errorText, Image image) + { + Log.Error("--> VerificationError Found: " + errorText, image); + testData.VerificationErrors.Add(new VerificationError(errorText, image)); +// TestContext.CurrentContext.IncrementAssertCount(); + } + + private void AssertNoVerificationErrors() + { + if (testData.VerificationErrors.Count >= 1) + { + Assert.Fail("The test failed due to verification errors"); + } + } + + public void SetTestExecutionSettings() + { +// TestAssemblyExecutionParameters.DegreeOfParallelism = Config.settings.runTimeSettings.DegreeOfParallelism; +// TestAssemblyExecutionParameters.DefaultTestCaseTimeout = +// TimeSpan.FromMinutes(Config.settings.runTimeSettings.TestTimeoutMin); + } + + private void GetHarFile() + { + try + { + if (Config.settings.httpProxy.startProxy) + { + var name = Common.GetShortTestName(80); + proxy.SaveHarToFile(); + Log.FilePath(proxy.GetHarFilePath()); + proxy.CreateHar(); + } + } + catch (Exception e) + { + throw new Exception("Error caught getting BMP Har File : " + e.Message); + } + } + + internal void StartNewProxy() + { + try + { + if (Config.settings.httpProxy.startProxy) + { + proxy.CreateProxy(); + proxy.CreateHar(); + } + } + catch (Exception e) + { + Log.Failure("Failed to setup proxy: " + e.Message + ": Trying again..."); + proxy.CreateProxy(); + proxy.CreateHar(); + } + } + + internal void StartProxyServer() + { + try + { + proxy = new BrowserMobProxy(); + if (Config.settings.httpProxy.startProxy) + { + proxy.KillOldProxy(); + proxy.StartServer(); + } + } + catch (Exception e) + { + throw new Exception("Error caught starting BMP Proxy Server : " + e.Message); + } + } + + private void QuitProxyServer() + { + try + { + if (Config.settings.httpProxy.startProxy) + { + proxy.QuitServer(); + } + } + catch (Exception) + { + } + } + + private void QuitProxy() + { + try + { + if (Config.settings.httpProxy.startProxy) + { + proxy.QuitProxy(); + } + } + catch (Exception) + { + } + } + + public static string GetCurrentClassName() + { + var stackTrace = new StackTrace(); // get call stack + var stackFrames = stackTrace.GetFrames(); // get method calls (frames) + foreach (var stackFrame in stackFrames) + { + if ((stackFrame.GetMethod().ReflectedType.BaseType == typeof (BasePageObject) || stackFrame.GetMethod().ReflectedType.BaseType == typeof(BaseComponent)) && + (!stackFrame.GetMethod().IsConstructor)) + { + return stackFrame.GetMethod().ReflectedType.Name; + } + } + foreach (var stackFrame in stackFrames) + { + if ((stackFrame.GetMethod().ReflectedType.BaseType == typeof (TestBase)) && + (!stackFrame.GetMethod().IsConstructor)) + { + return stackFrame.GetMethod().ReflectedType.Name + "." + stackFrame.GetMethod().Name; + } + } + + return ""; + } + + public static string GetCurrentMethodName() + { + var stackTrace = new StackTrace(); // get call stack + var stackFrames = stackTrace.GetFrames(); // get method calls (frames) + + // write call stack method names + foreach (var stackFrame in stackFrames) + { + if ((stackFrame.GetMethod().ReflectedType.IsSubclassOf(typeof (BasePageObject)) || stackFrame.GetMethod().ReflectedType.IsSubclassOf(typeof(BaseComponent))) && + (!stackFrame.GetMethod().IsConstructor)) + { + return stackFrame.GetMethod().Name; + } + } + foreach (var stackFrame in stackFrames) + { + if ((stackFrame.GetMethod().ReflectedType.IsSubclassOf(typeof (TestBase)) && + (!stackFrame.GetMethod().IsConstructor))) + { + return stackFrame.GetMethod().ReflectedType.Name + "." + stackFrame.GetMethod().Name; + } + } + + return ""; + } + + public static string GetCurrentClassAndMethodName() + { + var stackTrace = new StackTrace(); // get call stack + var stackFrames = stackTrace.GetFrames(); // get method calls (frames) + + foreach (var stackFrame in stackFrames) + { + var type = stackFrame.GetMethod().ReflectedType; + if (((type.IsSubclassOf(typeof (BasePageObject)) || type.IsSubclassOf(typeof(BaseComponent))) && + (!stackFrame.GetMethod().IsConstructor))) + { + return stackFrame.GetMethod().ReflectedType.Name + "." + stackFrame.GetMethod().Name; + } + } + + foreach (var stackFrame in stackFrames) + { + var type = stackFrame.GetMethod().ReflectedType; + if (type.IsSubclassOf(typeof (TestBase)) && (!stackFrame.GetMethod().IsConstructor)) + { + return stackFrame.GetMethod().ReflectedType.Name + "." + stackFrame.GetMethod().Name; + } + } + + return ""; + } + + public void LogVideo() + { + if ((Config.settings.reportSettings.videoRecordingOnError) && + testData.recorder != null && testData.recorder.Video != null) + { + var path = Log.Video(testData.recorder.Video); + TestBase.testData.VideoPath = path; + } + } + + public void StartVideoRecording() + { + try + { + if (Config.settings.reportSettings.videoRecordingOnError) + { + testData.recorder = Capture.StartRecording(new CaptureParameters { Zoom = .25 }, 5); + testData.recorder.OverlayManager.AddOverlay(overlay); + } + } + catch (Exception e) + { + Log.Failure("Exception caught while trying to start video recording : " + e.Message); + } + + } + + public void StopVideoRecording() + { + try + { + if (Config.settings.reportSettings.videoRecordingOnError && Config.settings.runTimeSettings.RunOnRemoteHost == false) + { + testData.recorder.Stop(); + } + } + catch (Exception e) + { + Log.Failure("Exception caught while trying to stop video recording : " + e.Message); + } + } + } +} \ No newline at end of file diff --git a/Core/TestDataContainer.cs b/Core/TestDataContainer.cs new file mode 100644 index 0000000..3c14cee --- /dev/null +++ b/Core/TestDataContainer.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gallio.Common.Media; +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Core +{ + public class TestDataContainer + { + private readonly object eventLocker = new object(); + public ActionList actions; + public BrowserInfo browserInfo; + public ConfigSettings configSettings; + private bool eventsRegistered; + public ScreenRecorder recorder; + public string testName; + public List VerificationErrors; + public string ExceptionMessage = ""; + public string StackTrace = ""; + public TestContext.ResultAdapter Result; + public string Status = ""; + public string ReportPath = ""; + public string ScreenshotPath = ""; + public string VideoPath = ""; + public string ClassName = ""; + public string MethodName = null; + + public TestDataContainer(string name) + { + testName = name; + actions = new ActionList(); + VerificationErrors = new List(); + configSettings = Config.GetDefaultConfig(); + + var count = configSettings.runTimeSettings.Browsers.Count(); + if (count > 0) + { + browserInfo = configSettings.runTimeSettings.Browsers.First(); + } + else + { + browserInfo = new BrowserInfo(WebDriverBrowser.Browser.Chrome); + } + + SetupEvents(); + + } + + public void GetCurrentResult() + { + Result = TestContext.CurrentContext.Result; + } + + public IWebDriver driver { get; set; } + + public void LogEvent(string name, ActionList.Action.ActionType type=ActionList.Action.ActionType.Other) + { + TestBase.testData.actions.addAction(name, type); + } + + private void WriteActionToLog(string name, EventArgs e) + { + TestBase.overlay.Text = name; +// if (Config.settings.Report.diagnosticLog) +// Log.Message(string.Format("({0}) : {1}", DateTime.Now.ToString("HH:mm:ss::ffff"), name)); + AddAction(name, e); + } + + private void AddAction(string name, EventArgs e) + { + TestBase.testData.actions.addAction(name); + } + + private void SetupEvents() + { + lock (eventLocker) + { + PageObjectActionEvent += AddAction; + BeforeTestEvent += WriteActionToLog; + AfterTestEvent += WriteActionToLog; + GenericEvent += WriteActionToLog; + eventsRegistered = true; + } + } + + private void RemoveEvents() + { + lock (eventLocker) + { + if (eventsRegistered) + { + PageObjectActionEvent -= AddAction; + BeforeTestEvent -= WriteActionToLog; + AfterTestEvent -= WriteActionToLog; + GenericEvent -= WriteActionToLog; + eventsRegistered = false; + } + } + } + + private delegate void ActionEvent(string name, EventArgs e); +#pragma warning disable 67 + private static event ActionEvent BeforeTestEvent; + private static event ActionEvent AfterTestEvent; + private static event ActionEvent PageObjectActionEvent; + private static event ActionEvent BeforeCommandEvent; + private static event ActionEvent AfterCommandEvent; + private static event ActionEvent BeforeSuiteEvent; + private static event ActionEvent AfterSuiteEvent; + private static event ActionEvent GenericEvent; +#pragma warning restore 67 + } +} \ No newline at end of file diff --git a/ElementImages/WebDriverTestBase.OpenPage_GoogleLogo.bmp b/ElementImages/WebDriverTestBase.OpenPage_GoogleLogo.bmp new file mode 100644 index 0000000..0ea819a Binary files /dev/null and b/ElementImages/WebDriverTestBase.OpenPage_GoogleLogo.bmp differ diff --git a/ElementImages/WebDriverTestBase.OpenPage_ImFeelingLuckyButton.bmp b/ElementImages/WebDriverTestBase.OpenPage_ImFeelingLuckyButton.bmp new file mode 100644 index 0000000..ba542d3 Binary files /dev/null and b/ElementImages/WebDriverTestBase.OpenPage_ImFeelingLuckyButton.bmp differ diff --git a/ElementImages/WebDriverTestBase.OpenPage_SearchButton.bmp b/ElementImages/WebDriverTestBase.OpenPage_SearchButton.bmp new file mode 100644 index 0000000..d8d2b88 Binary files /dev/null and b/ElementImages/WebDriverTestBase.OpenPage_SearchButton.bmp differ diff --git a/ElementImages/WebDriverTestBase.OpenPage_SearchField.bmp b/ElementImages/WebDriverTestBase.OpenPage_SearchField.bmp new file mode 100644 index 0000000..1c72a3c Binary files /dev/null and b/ElementImages/WebDriverTestBase.OpenPage_SearchField.bmp differ diff --git a/ElementImages/WebDriverTestBase.OpenPage_SignInButon.bmp b/ElementImages/WebDriverTestBase.OpenPage_SignInButon.bmp new file mode 100644 index 0000000..e068818 Binary files /dev/null and b/ElementImages/WebDriverTestBase.OpenPage_SignInButon.bmp differ diff --git a/Golem.2.0.0.1.nupkg b/Golem.2.0.0.1.nupkg new file mode 100644 index 0000000..7e33f05 Binary files /dev/null and b/Golem.2.0.0.1.nupkg differ diff --git a/Golem.2.0.0.2.nupkg b/Golem.2.0.0.2.nupkg new file mode 100644 index 0000000..07bf55f Binary files /dev/null and b/Golem.2.0.0.2.nupkg differ diff --git a/Golem.2.0.0.3.nupkg b/Golem.2.0.0.3.nupkg new file mode 100644 index 0000000..b35eedd Binary files /dev/null and b/Golem.2.0.0.3.nupkg differ diff --git a/Golem.2.0.1.0.nupkg b/Golem.2.0.1.0.nupkg new file mode 100644 index 0000000..7594c59 Binary files /dev/null and b/Golem.2.0.1.0.nupkg differ diff --git a/Golem.2.0.1.1.nupkg b/Golem.2.0.1.1.nupkg new file mode 100644 index 0000000..568818b Binary files /dev/null and b/Golem.2.0.1.1.nupkg differ diff --git a/Golem.2.0.1.2.nupkg b/Golem.2.0.1.2.nupkg new file mode 100644 index 0000000..cbe495f Binary files /dev/null and b/Golem.2.0.1.2.nupkg differ diff --git a/Golem.2.0.2.0.nupkg b/Golem.2.0.2.0.nupkg new file mode 100644 index 0000000..19ba753 Binary files /dev/null and b/Golem.2.0.2.0.nupkg differ diff --git a/Golem.2.1.0.0.nupkg b/Golem.2.1.0.0.nupkg new file mode 100644 index 0000000..73fb3de Binary files /dev/null and b/Golem.2.1.0.0.nupkg differ diff --git a/Golem.2.1.1.0.nupkg b/Golem.2.1.1.0.nupkg new file mode 100644 index 0000000..e4e4fda Binary files /dev/null and b/Golem.2.1.1.0.nupkg differ diff --git a/Golem.2.1.10.nupkg b/Golem.2.1.10.nupkg new file mode 100644 index 0000000..8490dcc Binary files /dev/null and b/Golem.2.1.10.nupkg differ diff --git a/Golem.2.1.2.0.nupkg b/Golem.2.1.2.0.nupkg new file mode 100644 index 0000000..646ae5e Binary files /dev/null and b/Golem.2.1.2.0.nupkg differ diff --git a/Golem.2.1.2.1.nupkg b/Golem.2.1.2.1.nupkg new file mode 100644 index 0000000..e046f84 Binary files /dev/null and b/Golem.2.1.2.1.nupkg differ diff --git a/Golem.2.1.3.nupkg b/Golem.2.1.3.nupkg new file mode 100644 index 0000000..a0b0176 Binary files /dev/null and b/Golem.2.1.3.nupkg differ diff --git a/Golem.2.1.4.nupkg b/Golem.2.1.4.nupkg new file mode 100644 index 0000000..297c6b9 Binary files /dev/null and b/Golem.2.1.4.nupkg differ diff --git a/Golem.2.1.5.nupkg b/Golem.2.1.5.nupkg new file mode 100644 index 0000000..d347bf2 Binary files /dev/null and b/Golem.2.1.5.nupkg differ diff --git a/Golem.2.1.6.nupkg b/Golem.2.1.6.nupkg new file mode 100644 index 0000000..bb84695 Binary files /dev/null and b/Golem.2.1.6.nupkg differ diff --git a/Golem.2.1.7.nupkg b/Golem.2.1.7.nupkg new file mode 100644 index 0000000..c30be69 Binary files /dev/null and b/Golem.2.1.7.nupkg differ diff --git a/Golem.2.1.8.nupkg b/Golem.2.1.8.nupkg new file mode 100644 index 0000000..b6fc359 Binary files /dev/null and b/Golem.2.1.8.nupkg differ diff --git a/Golem.2.1.9.nupkg b/Golem.2.1.9.nupkg new file mode 100644 index 0000000..7c8bd02 Binary files /dev/null and b/Golem.2.1.9.nupkg differ diff --git a/Golem.2.3.0.nupkg b/Golem.2.3.0.nupkg new file mode 100644 index 0000000..7887427 Binary files /dev/null and b/Golem.2.3.0.nupkg differ diff --git a/Golem.csproj b/Golem.csproj new file mode 100644 index 0000000..07c9530 --- /dev/null +++ b/Golem.csproj @@ -0,0 +1,297 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {DABFC1CB-7C18-4AC7-AFA2-91C453E9E266} + Library + Properties + Golem + Golem + v4.0 + 512 + + .\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + packages\DotNetZip.1.10.1\lib\net20\DotNetZip.dll + True + + + False + packages\Gallio_MbUnit.3.4.14.0\lib\net40\Gallio.dll + + + False + packages\Gallio_MbUnit.3.4.14.0\lib\net40\Gallio40.dll + + + packages\HtmlAgilityPack.1.4.9.5\lib\Net40\HtmlAgilityPack.dll + True + + + Resources\InputSimulator.dll + + + + packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll + + + packages\NUnit.3.7.1\lib\net40\nunit.framework.dll + + + False + Resources\PurpleLib.dll + + + packages\RestSharp.105.2.3\lib\net4\RestSharp.dll + True + + + + + + + + + + + + + + + packages\Selenium.WebDriver.3.4.0\lib\net40\WebDriver.dll + True + + + packages\Selenium.Support.3.4.0\lib\net40\WebDriver.Support.dll + True + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + + + Designer + + + Designer + + + Always + + + PreserveNewest + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Golem.nuspec b/Golem.nuspec new file mode 100644 index 0000000..f2a1fc6 --- /dev/null +++ b/Golem.nuspec @@ -0,0 +1,29 @@ + + + + Golem + 2.3.0 + Golem + Brian Kitchener - BlueModus + Brian Kitchener + http://www.github.com/prototest/prototest.golem + Selenium+ Test Automation Framework + + new versions of NUnit and Selenium.WebDriver + + Copyright 2017 + + Selenium WebDriver Framework Golem ProtoTest Gallio MbUnit Rest White TestStack Test Testing + + + + + + + + + + + + + \ No newline at end of file diff --git a/Golem.sln b/Golem.sln index 810eeef..a5efba8 100644 --- a/Golem.sln +++ b/Golem.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B48787D4-70A4-4226-9FE8-7AD27E1BBC1B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtoTest.Golem", "ProtoTest.Golem\ProtoTest.Golem.csproj", "{DABFC1CB-7C18-4AC7-AFA2-91C453E9E266}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Golem", "Golem.csproj", "{DABFC1CB-7C18-4AC7-AFA2-91C453E9E266}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{BAAF8DA6-E48E-4F1C-BE1D-3B3D0D49CF77}" ProjectSection(SolutionItems) = preProject diff --git a/IEDriverServer.exe b/IEDriverServer.exe new file mode 100644 index 0000000..610c348 Binary files /dev/null and b/IEDriverServer.exe differ diff --git a/MicrosoftWebDriver.exe b/MicrosoftWebDriver.exe new file mode 100644 index 0000000..e850f62 Binary files /dev/null and b/MicrosoftWebDriver.exe differ diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f0879db --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Golem.Core")] +[assembly: AssemblyDescription("EventDriven WebDriver Automation Framework")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Golem")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("e7b9ba96-eddc-4a21-9f4d-180215dfc400")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] \ No newline at end of file diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..f965cf5 --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Golem.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Golem.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..454e922 --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + \ No newline at end of file diff --git a/Proxy/BrowserMobProxy.cs b/Proxy/BrowserMobProxy.cs new file mode 100644 index 0000000..045c326 --- /dev/null +++ b/Proxy/BrowserMobProxy.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using NUnit.Framework; +using Ionic.Zip; +using Newtonsoft.Json; +using Golem.Core; +using Golem.Proxy.HAR; +using RestSharp; +using Log = Golem.Core.Log; + +namespace Golem.Proxy +{ + /// + /// This class acts as a wrapper around a BrowserMobProxy jar. + /// It contains methods to launch and stop the server, as well as to issue REST commands to start/stop/configure an + /// individual proxy. + /// + public class BrowserMobProxy + { + private static readonly string zipPath = AppDomain.CurrentDomain.BaseDirectory + + @"\Proxy\browsermob-proxy-2.1.0-beta-6-bin.zip"; + + private static readonly string batchPath = @"C:\BMP\browsermob-proxy-2.1.0-beta-6\bin\browsermob-proxy"; + private static readonly string extractPath = @"C:\BMP"; + public static string Indent = " "; + private readonly IRestClient client; + private readonly IDictionary proxyPortsByTest; + private readonly IRestRequest request; + private readonly int serverPort; + private static bool server_started; + private IRestResponse response; + private Process serverProcess; + + public BrowserMobProxy() + { + serverPort = Config.settings.httpProxy.proxyServerPort; + proxyPortsByTest = new Dictionary(); + client = new RestClient(new Uri("http://localhost:" + serverPort)); + request = new RestRequest(); + response = new RestResponse(); + + } + + public HarResult har + { + get + { + if (!Config.settings.httpProxy.useProxy) + throw new Exception( + "Could not get Proxy Har as proxy has been turned off. Please enabled it by updating your App.Config with the following keys : StartProxy=true, UseProxy=true"); + return GetHar(); + } + } + + public int proxyPort + { + get + { + if (!Config.settings.httpProxy.startProxy) + return Config.settings.httpProxy.proxyPort; + if (!proxyPortsByTest.ContainsKey(TestContext.CurrentContext.Test.FullName)) + { + proxyPortsByTest.Add(TestContext.CurrentContext.Test.FullName, + Config.settings.httpProxy.proxyPort); + Config.settings.httpProxy.proxyPort++; + } + + return proxyPortsByTest[TestContext.CurrentContext.Test.FullName]; + } + set + { + if (!Config.settings.httpProxy.startProxy) + Config.settings.httpProxy.proxyPort = value; + if (!proxyPortsByTest.ContainsKey(TestContext.CurrentContext.Test.FullName)) + { + proxyPortsByTest.Add(TestContext.CurrentContext.Test.FullName, + Config.settings.httpProxy.proxyPort); + Config.settings.httpProxy.proxyPort++; + } + proxyPortsByTest[TestContext.CurrentContext.Test.FullName] = value; + } + } + + public void StartServer() + { + UnzipProxy(); + Common.Log("Starting BrowserMob server on port " + serverPort); + serverProcess = new Process(); + var StartInfo = new ProcessStartInfo(); + StartInfo.FileName = batchPath; + StartInfo.Arguments = "-port " + serverPort; + StartInfo.WindowStyle = ProcessWindowStyle.Normal; + StartInfo.CreateNoWindow = false; + serverProcess.StartInfo = StartInfo; + serverProcess.Start(); + client.BaseUrl = new Uri("http://localhost:" + serverPort); + WaitForServerToStart(); + server_started = true; + } + + public void KillOldProxy() + { + if (Config.settings.httpProxy.killOldProxy) + { + var runningProcesses = Process.GetProcesses(); + foreach (var process in runningProcesses) + { + try + { + if ((process.ProcessName == "java") && (process.StartInfo.CreateNoWindow == false)) + { + Log.Message("Killing old BMP Proxy"); + process.Kill(); + } + } + catch (Exception) + { + } + } + } + } + + public void QuitServer() + { + try + { + Log.Message("Stopping BrowserMobProxy Server"); + serverProcess.CloseMainWindow(); + serverProcess.Kill(); + server_started = false; + } + catch (Exception e) + { + Log.Error(e.Message); + } + } + + public bool WaitForServerToStart(int timeout = 30) + { + for (var i = 0; i < timeout; i++) + { + request.Method = Method.GET; + request.Resource = "/"; + response = client.Execute(request); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return true; + } + Thread.Sleep(1000); + } + throw new Exception("Could not start the BrowserMobProxy " + response.StatusCode); + } + + public void UnzipProxy() + { + if (File.Exists(batchPath) == false) + { + Log.Message("BrowserMobProxy not found, unzipping"); + using (var zf = ZipFile.Read(zipPath)) + { + zf.ExtractAll(extractPath); + } + } + } + + public void QuitProxy() + { + Log.Message("Quitting Proxy on Port " + proxyPort); + request.Method = Method.DELETE; + request.Resource = "/proxy/" + proxyPort; + response = client.Execute(request); + if (response.StatusCode != HttpStatusCode.OK) + { + Log.Warning("Could not quit proxy on port : " + proxyPort); + } + } + + public void CreateProxy() + { + if (server_started==false) + { + StartServer(); + } + Log.Message("Creating Proxy on Port " + proxyPort); + + request.Method = Method.POST; + request.Resource = "/proxy?port=" + proxyPort; + response = client.Execute(request); + if (response.StatusCode != HttpStatusCode.OK) + { + response = client.Execute(request); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception("Could not start Proxy at port : " + proxyPort + " : " + response.ErrorMessage); + } + } + } + + public void CreateHar() + { + Log.Message("Creating a new Har"); + request.Method = Method.PUT; + request.Resource = string.Format("/proxy/{0}/har", proxyPort); + response = client.Execute(request); + if (response.ResponseStatus != ResponseStatus.Completed) + { + throw new Exception("Could not create Har on port " + proxyPort + ": " + response.StatusCode); + } + } + + public string GetPrettyHar() + { + return JsonConvert.SerializeObject(GetHarString(), Formatting.Indented); + } + + public void CreatePage(string name) + { + Log.Message("Creating a new Proxy Page with name " + name); + request.Method = Method.PUT; + request.Resource = string.Format("/proxy/{0}/har/pageRef", proxyPort); + response = client.Execute(request); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception("Could not create Page : " + response.StatusCode); + } + } + + public void DeleteProxy() + { + Log.Message("Deleting proxy on port " + proxyPort); + request.Method = Method.DELETE; + request.Resource = string.Format("/proxy/{0}", proxyPort); + response = client.Execute(request); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception("Could not delete proxy at port : " + proxyPort + " : " + response.StatusCode); + } + } + + public string GetHarString() + { + request.Method = Method.GET; + request.Resource = string.Format("/proxy/{0}/har ", proxyPort); + response = client.Execute(request); + return response.Content; + } + + public HarResult GetHar() + { + request.RequestFormat = DataFormat.Json; + request.Method = Method.GET; + request.Resource = string.Format("/proxy/{0}/har ", proxyPort); + var responseHar = client.Execute(request); + if (responseHar.StatusCode != HttpStatusCode.OK) + { + throw new Exception("Could not get har for proxy at port " + proxyPort + " : " + + responseHar.StatusCode); + } + return JsonConvert.DeserializeObject(responseHar.Content); + } + + public List FilterEntries(string url) + { + try + { + return har.Log.Entries.Where(entry => entry.Request.Url.Contains(url)).ToList(); + } + catch (Exception) + { + return new List(); + } + } + + public bool IsQueryStringInEntry(QueryStringItem querysString, Entry entry) + { + return + entry.Request.QueryString.Any( + qs => (qs.Name.Contains(querysString.Name)) && (qs.Value.Contains(querysString.Value))); + } + + public string GetValueForQueryStringWithName(string name, Entry entry) + { + try + { + return entry.Request.QueryString.First(qs => qs.Name.Contains(name)).Value; + } + catch (Exception) + { + return null; + } + } + + public Entry GetLastEntryForUrl(string url) + { + return har.Log.Entries.Last(entry => entry.Request.Url.Contains(url)); + } + + public void VerifyQueryStringInEntry(QueryStringItem queryString, Entry entry) + { + if (IsQueryStringInEntry(queryString, entry)) + { + Log.Message(string.Format("!--Verification Passed. Request contains {0}={1}", queryString.Name, + queryString.Value)); + return; + } + + string message; + var value = GetValueForQueryStringWithName(queryString.Name, entry); + if (value != null) + { + message = string.Format("Expected {0}={1}. Actual {2}={3}", queryString.Name, queryString.Value, + queryString.Name, value); + } + else + { + message = string.Format("No QueryString found with Description={0}", queryString.Name); + } + TestBase.AddVerificationError(string.Format("Request From {0} to {1} not correct. {2}", + TestBase.testData.driver.Url, entry.Request.Url, message)); + } + + public void VerifyRequestMade(string url) + { + var entries = FilterEntries(url); + if (entries.Count == 0) + { + TestBase.AddVerificationError("Did not find request with url " + url); + } + } + + public void VerifyRequestQueryString(string url, QueryStringItem queryString) + { + if (IsQueryStringInEntry(queryString, GetLastEntryForUrl(url))) + { + Log.Message(string.Format("!--Verification Passed. Request contains {0}={1}", queryString.Name, + queryString.Value)); + return; + } + + string message; + var value = GetValueForQueryStringWithName(queryString.Name, GetLastEntryForUrl(url)); + if (value != null) + { + message = string.Format("Expected {0}={1}. Actual {2}={3}", queryString.Name, queryString.Value, + queryString.Name, value); + } + else + { + message = string.Format("No QueryString found with Description={0}", queryString.Name); + } + TestBase.AddVerificationError(string.Format("Request From {0} to {1} not correct. {2}", + TestBase.testData.driver.Url, url, message)); + } + + public void Whitelist(string values, int statusCode = 408) + { + request.Method = Method.PUT; + request.Resource = string.Format("/proxy/{0}/whitelist", proxyPort); + request.AddParameter("regex", values); + request.AddParameter("status", statusCode); + response = client.Execute(request); + } + + public void Blacklist(string values, int statusCode = 407) + { + request.Method = Method.PUT; + request.Resource = string.Format("/proxy/{0}/blacklist", proxyPort); + request.AddParameter("regex", values); + request.AddParameter("status", statusCode); + response = client.Execute(request); + } + + public void ClearWhitelist() + { + request.Method = Method.DELETE; + request.Resource = string.Format("/proxy/{0}/whitelist", proxyPort); + response = client.Execute(request); + } + + public void ClearBlacklist() + { + request.Method = Method.DELETE; + request.Resource = string.Format("/proxy/{0}/blacklist", proxyPort); + response = client.Execute(request); + } + + public void SetBandwidthLimit(string kbps) + { + request.Method = Method.PUT; + request.Resource = string.Format("/proxy/{0}/limit", proxyPort); + request.AddParameter("downstreamKbps", kbps); + request.AddParameter("upstreamKbps", kbps); + request.AddParameter("enable", "true"); + response = client.Execute(request); + } + + public void SaveHarToFile() + { + using (var outfile = new StreamWriter(GetHarFilePath())) + { + outfile.Write(GetPrettyHar()); + } + } + + public string GetHarFilePath() + { + return Directory.GetCurrentDirectory() + + Path.DirectorySeparatorChar + + "HTTP_Traffic_" + Common.GetShortTestName(80) + ".har"; + } + + public void VerifyNoErrorsCodes() + { + var har = this.har; + var errors = + har.Log.Entries.Where(entry => entry.Response.Status > 400 && entry.Response.Status < 599).ToList(); + foreach (var error in errors) + { + TestBase.AddVerificationError(string.Format("Request to {0} returned an error code of {1} ({2}) : {3}", + error.Request.Url, error.Response.Status, error.Response.StatusText, error.Response.Content.Text)); + } + } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Browser.cs b/Proxy/HAR/Browser.cs new file mode 100644 index 0000000..5530eca --- /dev/null +++ b/Proxy/HAR/Browser.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class Browser + { + public string Name { get; set; } + public string Version { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Cache.cs b/Proxy/HAR/Cache.cs new file mode 100644 index 0000000..348c6d9 --- /dev/null +++ b/Proxy/HAR/Cache.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class Cache + { + public CacheEntry BeforeRequest { get; set; } + public CacheEntry AfterRequest { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/CacheEntry.cs b/Proxy/HAR/CacheEntry.cs new file mode 100644 index 0000000..36d211b --- /dev/null +++ b/Proxy/HAR/CacheEntry.cs @@ -0,0 +1,13 @@ +using System; + +namespace Golem.Proxy.HAR +{ + public class CacheEntry + { + public DateTime? Expires { get; set; } + public DateTime LastAccess { get; set; } + public string Etag { get; set; } + public int HitCount { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Content.cs b/Proxy/HAR/Content.cs new file mode 100644 index 0000000..e5ea769 --- /dev/null +++ b/Proxy/HAR/Content.cs @@ -0,0 +1,12 @@ +namespace Golem.Proxy.HAR +{ + public class Content + { + public int Size { get; set; } + public int? Compression { get; set; } + public string MimeType { get; set; } + public string Text { get; set; } + public string Encoding { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Cookie.cs b/Proxy/HAR/Cookie.cs new file mode 100644 index 0000000..0e976f7 --- /dev/null +++ b/Proxy/HAR/Cookie.cs @@ -0,0 +1,16 @@ +using System; + +namespace Golem.Proxy.HAR +{ + public class Cookie + { + public string Name { get; set; } + public string Value { get; set; } + public string Path { get; set; } + public string Domain { get; set; } + public DateTime? Expires { get; set; } + public bool? HttpOnly { get; set; } + public bool? Secure { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Creator.cs b/Proxy/HAR/Creator.cs new file mode 100644 index 0000000..38d4cdb --- /dev/null +++ b/Proxy/HAR/Creator.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class Creator + { + public string Name { get; set; } + public string Version { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Entry.cs b/Proxy/HAR/Entry.cs new file mode 100644 index 0000000..7e2f3a0 --- /dev/null +++ b/Proxy/HAR/Entry.cs @@ -0,0 +1,18 @@ +using System; + +namespace Golem.Proxy.HAR +{ + public class Entry + { + public int Time { get; set; } + public Request Request { get; set; } + public Response Response { get; set; } + public DateTime StartedDateTime { get; set; } + public Timings Timings { get; set; } + public string PageRef { get; set; } + public Cache Cache { get; set; } + public string ServerIpAddress { get; set; } + public string Connection { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/HarResult.cs b/Proxy/HAR/HarResult.cs new file mode 100644 index 0000000..2ff1175 --- /dev/null +++ b/Proxy/HAR/HarResult.cs @@ -0,0 +1,7 @@ +namespace Golem.Proxy.HAR +{ + public class HarResult + { + public Log Log { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Header.cs b/Proxy/HAR/Header.cs new file mode 100644 index 0000000..3e01c9e --- /dev/null +++ b/Proxy/HAR/Header.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class Header + { + public string Name { get; set; } + public string Value { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Log.cs b/Proxy/HAR/Log.cs new file mode 100644 index 0000000..bf3c048 --- /dev/null +++ b/Proxy/HAR/Log.cs @@ -0,0 +1,12 @@ +namespace Golem.Proxy.HAR +{ + public class Log + { + public Entry[] Entries { get; set; } + public string Version { get; set; } + public Browser Browser { get; set; } + public Creator Creator { get; set; } + public Page[] Pages { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Page.cs b/Proxy/HAR/Page.cs new file mode 100644 index 0000000..50a2db2 --- /dev/null +++ b/Proxy/HAR/Page.cs @@ -0,0 +1,13 @@ +using System; + +namespace Golem.Proxy.HAR +{ + public class Page + { + public string Id { get; set; } + public PageTimings PageTimings { get; set; } + public DateTime StartedDateTime { get; set; } + public string Title { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/PageTimings.cs b/Proxy/HAR/PageTimings.cs new file mode 100644 index 0000000..10adc01 --- /dev/null +++ b/Proxy/HAR/PageTimings.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class PageTimings + { + public int? OnContentLoad { get; set; } + public int? OnLoad { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Param.cs b/Proxy/HAR/Param.cs new file mode 100644 index 0000000..bf1c3c5 --- /dev/null +++ b/Proxy/HAR/Param.cs @@ -0,0 +1,11 @@ +namespace Golem.Proxy.HAR +{ + public class Param + { + public string Name { get; set; } + public string Value { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/PostData.cs b/Proxy/HAR/PostData.cs new file mode 100644 index 0000000..6f23dde --- /dev/null +++ b/Proxy/HAR/PostData.cs @@ -0,0 +1,10 @@ +namespace Golem.Proxy.HAR +{ + public class PostData + { + public string MimeType { get; set; } + public Param[] Params { get; set; } + public string Text { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/QueryStringItem.cs b/Proxy/HAR/QueryStringItem.cs new file mode 100644 index 0000000..f651cb2 --- /dev/null +++ b/Proxy/HAR/QueryStringItem.cs @@ -0,0 +1,9 @@ +namespace Golem.Proxy.HAR +{ + public class QueryStringItem + { + public string Name { get; set; } + public string Value { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Request.cs b/Proxy/HAR/Request.cs new file mode 100644 index 0000000..fe1f43b --- /dev/null +++ b/Proxy/HAR/Request.cs @@ -0,0 +1,16 @@ +namespace Golem.Proxy.HAR +{ + public class Request + { + public string Method { get; set; } + public QueryStringItem[] QueryString { get; set; } + public PostData PostData { get; set; } + public Cookie[] Cookies { get; set; } + public Header[] Headers { get; set; } + public int BodySize { get; set; } + public string Url { get; set; } + public string HttpVersion { get; set; } + public int HeadersSize { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Response.cs b/Proxy/HAR/Response.cs new file mode 100644 index 0000000..5201e44 --- /dev/null +++ b/Proxy/HAR/Response.cs @@ -0,0 +1,16 @@ +namespace Golem.Proxy.HAR +{ + public class Response + { + public Content Content { get; set; } + public Header[] Headers { get; set; } + public int Status { get; set; } + public Cookie[] Cookies { get; set; } + public int BodySize { get; set; } + public string HttpVersion { get; set; } + public int HeadersSize { get; set; } + public string StatusText { get; set; } + public string RedirectUrl { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/HAR/Timings.cs b/Proxy/HAR/Timings.cs new file mode 100644 index 0000000..966abc0 --- /dev/null +++ b/Proxy/HAR/Timings.cs @@ -0,0 +1,14 @@ +namespace Golem.Proxy.HAR +{ + public class Timings + { + public int? Blocked { get; set; } + public int? Dns { get; set; } + public int? Connect { get; set; } + public int Send { get; set; } + public int Wait { get; set; } + public int Receive { get; set; } + public int? Ssl { get; set; } + public string Comment { get; set; } + } +} \ No newline at end of file diff --git a/Proxy/ProxyTests.cs b/Proxy/ProxyTests.cs new file mode 100644 index 0000000..1187ca2 --- /dev/null +++ b/Proxy/ProxyTests.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using OpenQA.Selenium; +using ProtoTest.Golem.Core; +using ProtoTest.Golem.Proxy.HAR; +using ProtoTest.Golem.WebDriver; + +namespace ProtoTest.Golem.Proxy +{ + internal class ProxyTests : WebDriverTestBase + { + private readonly string filePath = Config.GetConfigValue("DataCsvPath", + Common.GetCodeDirectory() + @"\Proxy\Data.csv"); + + public IEnumerable GetRowNumber() + { + if (!File.Exists(filePath)) + { + Assert.Fail("Could not run tests, " + filePath + " could not be found"); + } + var file = new StreamReader(filePath); + var rowNumber = 0; + var line = file.ReadLine(); + while (line != null) + { + rowNumber++; + yield return rowNumber; + } + } + + // if (!File.Exists(filePath)) + // { + // Assert.Fail("Could not run tests, " + filePath + " could not be found"); + // } + // StreamReader file = new StreamReader(filePath); + // string line = file.ReadLine(); + // int numLines = 0; + // while (line != null) + // { + // numLines++; + // var fields = LineSplitter(line).ToArray(); + // string url = fields[0]; + // string linkText = fields[1]; + // List queryStrings = new List(); + // for (var i = 2; i < fields.Length; i++) + // { + // var qs = fields[i].Split('='); + // if (qs[0] != "") + // { + // queryStrings.Add(new QueryStringItem() { Name = qs[0], Value = qs[1] }); + // } + + // } + + // yield return new TestObject() {url = fields[0], queryStrings = queryStrings,ElementText = linkText}; + // line = file.ReadLine(); + // } + //} + + + [Test, Factory("GetRowNumber")] + public void DDTTest(int RowNumber) + { + var testObject = new TestObject(RowNumber); + driver.Navigate().GoToUrl(testObject.url); + if (testObject.ElementText != "") + { + driver.FindVisibleElement(By.XPath("//*[text()='" + testObject.ElementText + "']")).Click(); + } + var lastRequest = proxy.GetLastEntryForUrl("om.healthgrades.com"); + + foreach (var querystring in testObject.queryStrings) + { + proxy.VerifyQueryStringInEntry(querystring, lastRequest); + } + } + + [FixtureInitializer] + public void init() + { + Config.Settings.httpProxy.startProxy = true; + Config.Settings.httpProxy.useProxy = true; + } + + [Test] + public void Test() + { + driver.Navigate().GoToUrl("http://www.healthgrades.com/physician/dr-john-schultz-2324x"); + driver.Sleep(3000); + proxy.VerifyRequestQueryString("om.healthgrades.com", new QueryStringItem {Name = "c6", Value = "physician"}); + } + + public class TestObject + { + private readonly string filePath = Config.GetConfigValue("DataCsvPath", + Common.GetCodeDirectory() + @"\Proxy\Data.csv"); + + public string ElementText; + public string line; + public List queryStrings; + public int rowNumber; + public string url; + + public TestObject(int rowNum) + { + line = GetLineByRow(rowNum); + var fields = LineSplitter(line).ToArray(); + url = fields[0]; + ElementText = fields[1]; + for (var i = 2; i < fields.Length; i++) + { + var qs = fields[i].Split('='); + if (qs[0] != "") + { + queryStrings.Add(new QueryStringItem {Name = qs[0], Value = qs[1]}); + } + } + } + + public string GetLineByRow(int rowNum) + { + if (!File.Exists(filePath)) + { + Assert.Fail("Could not run tests, " + filePath + " could not be found"); + } + var file = new StreamReader(filePath); + var line = ""; + for (var i = 1; i < rowNum; i++) + { + line = file.ReadLine(); + } + return line; + } + + private IEnumerable LineSplitter(string line) + { + var fieldStart = 0; + for (var i = 0; i < line.Length; i++) + { + if (line[i] == ',') + { + var field = line.Substring(fieldStart, i - fieldStart).Replace("\"", ""); + yield return field; + fieldStart = i + 1; + } + if (line[i] == '"') + for (i++; line[i] != '"'; i++) + { + } + } + } + } + } +} \ No newline at end of file diff --git a/Proxy/browsermob-proxy-2.1.0-beta-6-bin.zip b/Proxy/browsermob-proxy-2.1.0-beta-6-bin.zip new file mode 100644 index 0000000..66fa5c5 Binary files /dev/null and b/Proxy/browsermob-proxy-2.1.0-beta-6-bin.zip differ diff --git a/Purple/BaseScreenObject.cs b/Purple/BaseScreenObject.cs new file mode 100644 index 0000000..7c506c8 --- /dev/null +++ b/Purple/BaseScreenObject.cs @@ -0,0 +1,7 @@ +namespace Golem.Purple +{ + public class BaseScreenObject + { + + } +} \ No newline at end of file diff --git a/Purple/ElementImageComparer.cs b/Purple/ElementImageComparer.cs new file mode 100644 index 0000000..dd07c7f --- /dev/null +++ b/Purple/ElementImageComparer.cs @@ -0,0 +1,160 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using Gallio.Framework; +using Golem.Core; +using Golem.Purple.Elements; +using Golem.WebDriver.Elements.Images; + +namespace Golem.Purple +{ + //TODO: this class is going to need to be re-factored! + public class ElementImageComparer + { + public static bool UpdateImages = Config.settings.imageCompareSettings.updateImages; + private readonly Image liveImage; + private readonly Image storedImage; + public float difference; + public string differenceString; + public IPurpleElement element; + + public ElementImageComparer(IPurpleElement element) + { + this.element = element; + CreateDirectory(); + liveImage = GetLiveImage(); + storedImage = GetStoredImage(); + } + + private string ImageLocation + { + get + { + return Directory.GetCurrentDirectory().Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\ElementImages\\" + element.ElementName.Replace(" ", "") + ".bmp"; + } + } + + public Image GetDifferenceImage() + { + var bmp = new Bitmap(liveImage.Width, liveImage.Height); + return liveImage.GetDifferenceOverlayImage(storedImage) + .Resize(storedImage.Width, storedImage.Height); + } + + public bool ImagesMatch() + { + if ((!File.Exists(ImageLocation)) || (UpdateImages)) + { + UpdateImage(); + } + difference = ImageComparer.ImageComparePercentage(storedImage, liveImage, + Config.settings.imageCompareSettings.fuzziness); + differenceString = (difference*100).ToString("0.##\\%"); + return difference < Config.settings.imageCompareSettings.accuracy; + } + + public Image GetMergedImage() + { + var overlayImage = OverlayImages(liveImage, GetDifferenceImage()); + var mergedImage = CombineImages(storedImage, liveImage, overlayImage); + return mergedImage; + } + + private Image CombineImages(Image image1, Image image2, Image image3) + { + var newWidth = image1.Width + image2.Width + image3.Width; + var newHeight = image1.Height; + var bmp = new Bitmap(newWidth, newHeight); + using (var gr = Graphics.FromImage(bmp)) + { + gr.DrawImage(image1, new Point(0, 0)); + gr.DrawImage(image2, new Point(image1.Width, 0)); + gr.DrawImage(image3, new Point(image2.Width + image1.Width, 0)); + } + return bmp; + } + + public Image GetLiveImage() + { + return element.UIAElement.GetImage(); + } + + public Image GetStoredImage() + { + if (File.Exists(ImageLocation)) + return Image.FromFile(ImageLocation); + return GetLiveImage(); + } + + public Image OverlayImages(Image imageBackground, Image imageOverlay) + { + imageOverlay = imageOverlay.Resize(imageBackground.Width, imageBackground.Height); + Image img = new Bitmap(imageBackground.Width, imageBackground.Height); + using (var gr = Graphics.FromImage(img)) + { + gr.DrawImage(imageBackground, new Point(0, 0)); + gr.DrawImage(imageOverlay, new Point(0, 0)); + } + return img; + } + + public void CreateDirectory() + { + if (!Directory.Exists(Common.GetCodeDirectory() + "\\ElementImages")) + Directory.CreateDirectory(Common.GetCodeDirectory() + "\\ElementImages"); + } + + public void DeleteOldImage() + { + if (File.Exists(ImageLocation)) + File.Delete(ImageLocation); + } + + public void UpdateImage() + { + using (var image = GetLiveImage()) + { + SaveImage(image); + } + } + + private void SaveImage(Image image) + { + try + { + DeleteOldImage(); + using (var tempImage = new Bitmap(image)) + { + tempImage.Save(ImageLocation, ImageFormat.Bmp); + } + } + catch (Exception e) + { + Log.Message("Exception saving image : " + e.Message); + } + } + + private Image cropImage(Image img, Rectangle cropArea) + { + var bmpImage = new Bitmap(img); + var bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat); + return bmpCrop; + } + + public void VerifyImage() + { + if (ImagesMatch()) + { + TestContext.CurrentContext.IncrementAssertCount(); + Log.Message("Images match!"); + } + + else + { + TestBase.AddVerificationError("Images don't match", GetMergedImage()); + } + } + } +} \ No newline at end of file diff --git a/Purple/ElementVerification.cs b/Purple/ElementVerification.cs new file mode 100644 index 0000000..e240957 --- /dev/null +++ b/Purple/ElementVerification.cs @@ -0,0 +1,174 @@ +using System.Drawing; +using NUnit.Framework; +using Golem.Core; +using Golem.Purple.Elements; + +namespace Golem.Purple +{ + /// + /// Methods for performing non-terminating validations, and Wait commands. + /// TODO I refactored the IPurpleElement class - only used for image comparison at the moment + /// + public class ElementVerification + { + private const string errorMessage = "{0}: {1}({2}): {3} after {4} seconds"; + private readonly IPurpleElement element; + private readonly bool failTest; + private readonly bool isTrue = true; + private readonly int timeoutSec; + private bool condition; + private string message; + private string notMessage; + + public ElementVerification(IPurpleElement element, int timeoutSec = 0, bool failTest = false, bool isTrue = true) + { + this.element = element; + this.timeoutSec = timeoutSec; + this.failTest = failTest; + this.isTrue = isTrue; + } + + public ElementVerification Not() + { + return new ElementVerification(element, timeoutSec, failTest, false); + } + + private void VerificationFailed(string message = "", Image image = null) + { + if (message == "") message = GetErrorMessage(); + if (failTest) + { + Assert.Fail(message); + } + else + { + if (image == null) + TestBase.AddVerificationError(message); + else + TestBase.AddVerificationError(message, image); + } + } + + private string GetErrorMessage() + { + string newMessage; + newMessage = isTrue ? notMessage : message; + + return string.Format(errorMessage, TestBase.GetCurrentClassAndMethodName(), element.ElementName, + element.PurplePath, + newMessage, timeoutSec); + } + + private string GetSuccessMessage() + { + string newMessage; + var correctMessage = "{0}: {1}({2}): {3}"; + newMessage = isTrue ? message : notMessage; + + return string.Format(correctMessage, TestBase.GetCurrentClassAndMethodName(), element.ElementName, + element.PurplePath, + newMessage); + } + + //public IPurpleElement HasChildElement(string PurplePath) + //{ + // //message = "has child with PurplePath " + PurplePath; + // //notMessage = "no child with PurplePath " + PurplePath; + + // //for (int i = 0; i <= timeoutSec; i++) + // //{ + // // condition = element.UIAElement.Current.IsEnabled && element.getItem().GetMultiple(PurplePath).Length > 0; + // // if (condition == isTrue) + // // { + // // Log.Message("!--Verification Passed " + GetSuccessMessage()); + // // return element; + // // } + // // Common.Delay(1000); + // //} + // //VerificationFailed(); + // //return element; + //} + + + //public IPurpleElement Present() + //{ + // //message = "is present"; + // //notMessage = "not present"; + + // //for (int i = 0; i <= timeoutSec; i++) + // //{ + // // condition = element.getItem().Present(); + // // if (condition == isTrue) + // // { + // // Log.Message("!--Verification Passed " + GetSuccessMessage()); + // // return element; + // // } + // // Common.Delay(1000); + // //} + // //VerificationFailed(); + // //return element; + //} + + //public IPurpleElement Visible() + //{ + // //message = "is visible"; + // //notMessage = "not visible"; + // //if (timeoutSec == 0) timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + // //for (int i = 0; i <= timeoutSec; i++) + // //{ + // // condition = element.getItem().Present() && element.getItem().Visible; + // // if (condition == isTrue) + // // { + // // Log.Message("!--Verification Passed " + GetSuccessMessage()); + // // return element; + // // } + // // Common.Delay(1000); + // //} + // //VerificationFailed(); + // //return element; + //} + + //public IPurpleElement Name(string value) + //{ + // //message = "contains text '" + value + "'"; + // //notMessage = "doesn't contain text '" + value + "'"; + // //for (int i = 0; i <= timeoutSec; i++) + // //{ + + // // condition = element.getItem().Present() && (element.getItem().Name.Contains(value)); + // // if (condition == isTrue) + // // { + // // Log.Message("!--Verification Passed " + GetSuccessMessage()); + // // return element; + // // } + // // Common.Delay(1000); + // //} + // //VerificationFailed(); + // //return element; + //} + + public IPurpleElement Image() + { + message = "image matches"; + notMessage = "image is {0} different"; + var comparer = new ElementImageComparer(element); + condition = element.UIAElement.Current.IsEnabled && comparer.ImagesMatch(); + if (condition == isTrue) + { +// TestContext.CurrentContext.IncrementAssertCount(); + Log.Message(GetSuccessMessage()); + } + + else + { + notMessage = string.Format(notMessage, comparer.differenceString); + VerificationFailed( + string.Format("{0}: {1}({2}): {3}", TestBase.GetCurrentClassAndMethodName(), element.ElementName, + element.PurplePath, + notMessage), comparer.GetMergedImage()); + } + + return element; + } + } +} \ No newline at end of file diff --git a/Purple/Extensions.cs b/Purple/Extensions.cs new file mode 100644 index 0000000..da59ec3 --- /dev/null +++ b/Purple/Extensions.cs @@ -0,0 +1,74 @@ +using System; +using System.Drawing; +using System.Windows; +using System.Windows.Automation; +using Golem.Purple.PurpleCore; +using Golem.Purple.PurpleElements; + +namespace Golem.Purple +{ + public static class Extensions + { + //TODO this has to be refactored + public static Image GetImage(this AutomationElement window) + { + return new ScreenCapture().CaptureScreenShot(); + } + + public static void SetCheckbox(this PurpleCheckBox box, bool isChecked) + { + if (box.Checked != isChecked) + box.Click(); + } + + public static Image GetImage(this PurpleElementBase item) + { + Image screenImage = new ScreenCapture().CaptureScreenShot(); + var cropArea = item.Bounds.ToRectangle(); + var bmpImage = new Bitmap(screenImage); + Bitmap bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat); + return bmpCrop; + } + + public static Rectangle + ToRectangle(this Rect value) + { + var result = + new Rectangle(); + result.X = (int) value.X; + result.Y = (int) value.Y; + result.Width = (int) value.Width; + result.Height = (int) value.Height; + return result; + } + + public static bool IsStale(this PurpleElementBase item) + { + try + { + var enabled = item.PurpleElement.Current.IsEnabled; + return false; + } + catch (Exception) + { + return true; + } + } + + public static bool Present(this PurpleElementBase item) + { + return !item.IsStale() && item.PurpleElement.Current.IsEnabled; + } + + public static void WaitForVisible(this PurpleElementBase item) + { + try + { + var enabled = item.PurpleElement.Current.IsEnabled; + } + catch (Exception) + { + } + } + } +} \ No newline at end of file diff --git a/Purple/IPurpleElement.cs b/Purple/IPurpleElement.cs new file mode 100644 index 0000000..a126b06 --- /dev/null +++ b/Purple/IPurpleElement.cs @@ -0,0 +1,12 @@ +using System; +using System.Windows.Automation; + +namespace Golem.Purple.Elements +{ + public interface IPurpleElement + { + String ElementName { get; } + String PurplePath { get; } + AutomationElement UIAElement { get; } + } +} \ No newline at end of file diff --git a/Purple/ImageManipulation.cs b/Purple/ImageManipulation.cs new file mode 100644 index 0000000..bb11c1a --- /dev/null +++ b/Purple/ImageManipulation.cs @@ -0,0 +1,357 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using Golem.Core; +using Golem.Purple.Elements; +using Golem.Purple.PurpleElements; +using Golem.WebDriver.Elements.Images; + +namespace Golem.Purple +{ + public class ImageManipulation + { + public static bool UpdateImages = Config.settings.imageCompareSettings.updateImages; + private readonly Image liveImage; + private readonly string snapshotIndexFormat = "00"; + //private readonly Image storedImage; + public float difference; + public string differenceString; + public string DirLocation; + public IPurpleElement element; + public string FileName; + public int snapshotIndex; + + /// + /// Creates a new object with a reference to a PurpleElement object. + /// It erase all the previous snapshots that might have been taken. + /// + /// purple element + public ImageManipulation(IPurpleElement element) + { + this.element = element; + CreateDirectory(); + liveImage = GetLiveImage(); + //storedImage = GetStoredImage(); + DirLocation = Directory.GetCurrentDirectory().Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\ElementImages\\"; + FileName = element.ElementName.Replace(" ", ""); + DeleteAllSnapshots(); + } + + /// + /// Creates a new object with a reference to a PurpleElement object. + /// It keeps all the previous snapshots that might have been taken. + /// + /// purple element + /// indicates that we like to keep previous snapshots + public ImageManipulation(IPurpleElement element, bool keepOld) + { + this.element = element; + CreateDirectory(); + liveImage = GetLiveImage(); + //storedImage = GetStoredImage(); + DirLocation = Directory.GetCurrentDirectory().Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\ElementImages\\"; + FileName = element.ElementName.Replace(" ", ""); + if (keepOld) + { + snapshotIndex = GetLastStoredSnapshotIndex(); + } + else + { + DeleteAllSnapshots(); + } + } + + // Functions copied and modified from ElementImageComparer + + private string ImageLocation + { + get + { + return Directory.GetCurrentDirectory().Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\ElementImages\\" + element.ElementName.Replace(" ", "") + ".bmp"; + } + } + + /// + /// Takes a snapshot and save it to a file + /// + /// + public int TakeSnapshot() + { + snapshotIndex++; + var currentImage = GetLiveImage(); + Save(currentImage, FileName + "-Snapshot" + snapshotIndex.ToString(snapshotIndexFormat)); + return snapshotIndex; + } + + /// + /// Saves an image to disk + /// + /// Image content + /// Image filename + public void Save(Image image, string imageName) + { + using (var tempImage = new Bitmap(image)) + { + tempImage.Save(DirLocation + imageName + ".bmp", ImageFormat.Bmp); + } + } + + public int GetCurrentSnapshotIndex() + { + return snapshotIndex; + } + + /// + /// Returns the latest snapshot index from disk + /// + public int GetLastStoredSnapshotIndex() + { + var files = Directory.GetFiles(DirLocation, FileName + "-Snapshot*"); + var index = 0; + if (files.Length > 0) + { + var sortedFiles = from s in files orderby s descending select s; + var latestFile = sortedFiles.ElementAt(0); + index = int.Parse(latestFile.Substring(latestFile.IndexOf("-Snapshot") + 9, 2)); + } + return index; + } + + /// + /// delete all the snapshots from disk + /// + public void DeleteAllSnapshots() + { + var files2 = Directory.GetFiles(DirLocation, FileName + "-Snapshot*"); + foreach (var currentFile in files2) + { + File.Delete(currentFile); + } + snapshotIndex = 0; + } + + /// + /// Get he image content based on the index of the snapshot taken + /// + /// snapshot order + /// Image + public Image GetStoredSnapshot(int snapshot) + { + var fName = FileName + "-Snapshot" + snapshot.ToString(snapshotIndexFormat); + if (File.Exists(DirLocation + fName + ".bmp")) + return Image.FromFile(DirLocation + fName + ".bmp"); + return null; + } + + /// + /// Returns the image of the latest stored snapshot + /// + /// Image + public Image GetLatestStoredSnapshot() + { + var fName = FileName + "-Snapshot" + snapshotIndex.ToString(snapshotIndexFormat); + if (File.Exists(DirLocation + fName)) + return Image.FromFile(DirLocation + fName + ".bmp"); + return null; + } + + /// + /// Gets an image stored on disk. Do not include file extension + /// + /// Name of the file without extension + /// Image + public Image GetImageFromDisk(string imageName) + { + var fName = imageName + ".bmp"; + if (File.Exists(DirLocation + fName)) + return Image.FromFile(DirLocation + fName); + return null; + } + + /// + /// Retruns the image which is the result of the difference between 2 given images. + /// The resultimg image will have the size of image 2 + /// + /// Image 1 + /// Image 2 + /// Image resulting from comparisson + public Image GetDifferenceImage(Image image1, Image image2) + { + var bmp = new Bitmap(image2.Width, image2.Height); + return liveImage.GetDifferenceOverlayImage(image1).Resize(image2.Width, image2.Height); + } + + /// + /// Retruns the image which is the result of the difference between an image previoulsy captured and the current image + /// on screen. + /// + /// the image we want to compare with + /// Image + public Image GetDifferenceImage(Image oldImage) + { + var bmp = new Bitmap(liveImage.Width, liveImage.Height); + return liveImage.GetDifferenceOverlayImage(oldImage).Resize(liveImage.Width, liveImage.Height); + } + + /// + /// Retruns the image which is the result of the difference between a snapshot previoulsy captured and the current + /// image on screen. + /// + /// + /// + public Image GetDifferenceImageAgainstSnapshot(int snapshot) + { + var oldImage = GetStoredSnapshot(snapshot); + var bmp = new Bitmap(liveImage.Width, liveImage.Height); + return liveImage.GetDifferenceOverlayImage(oldImage).Resize(liveImage.Width, liveImage.Height); + } + + /// + /// Compares 2 images and returns a boolean value indicating if the change is greater than an accuracy value. + /// + /// Image 1 + /// Image 2 + /// boolean + public bool ImagesMatch(Image source1, Image source2) + { + difference = ImageComparer.ImageComparePercentage(source1, source2, + Config.settings.imageCompareSettings.fuzziness); + differenceString = (difference*100).ToString("0.##\\%"); + return difference < Config.settings.imageCompareSettings.accuracy; + } + + /// + /// Compares 2 images, and returns a boolean value indicating if the change is greater than an accuracy value. + /// + /// Image 1 + /// Image 2 + /// Accuracy of the comparisson + /// boolean + public bool ImagesMatch(Image source1, Image source2, float accuracy) + { + difference = ImageComparer.ImageComparePercentage(source1, source2, + Config.settings.imageCompareSettings.fuzziness); + differenceString = (difference*100).ToString("0.##\\%"); + return difference < accuracy; + } + + /// + /// Compares 2 images and return the difference expressed as a percentage + /// + /// Image 1 + /// Image 2 + /// Image 2 + /// float value that indicates the difference expressed as percentage + public string ImagesMatchReturnValue(Image source1, Image source2, int fuzzines = -1) + { + byte fuz; + if (fuzzines == -1) + { + fuz = Config.settings.imageCompareSettings.fuzziness; + } + else + { + fuz = Byte.Parse(fuzzines.ToString()); + } + difference = ImageComparer.ImageComparePercentage(source1, source2, fuz); + differenceString = (difference*100).ToString("0.##"); + return differenceString; + } + + /// + /// Compares an image captured on a previous snapshot and the current image, and returns a boolean value indicating if + /// the change is greater than an accuracy value. + /// + /// number of snapshot + /// boolean + public bool ImagesMatchCurrentAndSnapshot(int snapshot) + { + var current = GetLiveImage(); + var oldImage = GetStoredSnapshot(snapshot); + return ImagesMatch(current, oldImage); + } + + /// + /// Compares an image captured on a previous snapshot and the current image, and returns a boolean value indicating if + /// the change is greater than an accuracy value. + /// + /// number of snapshot + /// accuracy to campare against + /// boolean + public bool ImagesMatchCurrentAndSnapshot(int snapshot, float accuracy) + { + var current = GetLiveImage(); + var oldImage = GetStoredSnapshot(snapshot); + return ImagesMatch(current, oldImage, accuracy); + } + + /// + /// Creates a combination of 2 images side by side + /// + /// image 1 + /// image 2 + /// Image result + public Image CombineImages(Image image1, Image image2) + { + var newWidth = image1.Width + image2.Width; + var newHeight = image1.Height; + var bmp = new Bitmap(newWidth, newHeight); + using (var gr = Graphics.FromImage(bmp)) + { + gr.DrawImage(image1, new Point(0, 0)); + gr.DrawImage(image2, new Point(image1.Width, 0)); + } + return bmp; + } + + public Image OverlayImages(Image imageBackground, Image imageOverlay) + { + imageOverlay = imageOverlay.Resize(imageBackground.Width, imageBackground.Height); + Image img = new Bitmap(imageBackground.Width, imageBackground.Height); + using (var gr = Graphics.FromImage(img)) + { + gr.DrawImage(imageBackground, new Point(0, 0)); + gr.DrawImage(imageOverlay, new Point(0, 0)); + } + return img; + } + + public void CreateDirectory() + { + if (!Directory.Exists(Common.GetCodeDirectory() + "\\ElementImages")) + Directory.CreateDirectory(Common.GetCodeDirectory() + "\\ElementImages"); + } + + /// + /// Returns an image that represents the screen at this moment + /// + /// Image + public Image GetLiveImage() + { + var img = element.UIAElement.GetImage(); + var cropArea = ((PurpleElementBase) element).Bounds.ToRectangle(); + return cropImage(img, cropArea); + } + + /* + public Image GetStoredImage() + { + if (File.Exists(ImageLocation)) + return Image.FromFile(ImageLocation); + return GetLiveImage(); + } + */ + + public Image cropImage(Image img, Rectangle cropArea) + { + var bmpImage = new Bitmap(img); + var bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat); + return bmpCrop; + } + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/Locator.cs b/Purple/PurpleCore/Locator.cs new file mode 100644 index 0000000..6e9ff1b --- /dev/null +++ b/Purple/PurpleCore/Locator.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Timers; +using System.Windows.Automation; +using NUnit.Framework; +using Golem.Core; +using PurpleLib; +using Timer = System.Timers.Timer; + +namespace Golem.Purple.PurpleCore +{ + public class Locator + { + private static readonly PurplePath _purplePath = new PurplePath(); + + private readonly Timer elementTimeoutTimer = + new Timer(Config.settings.purpleSettings.Purple_ElementTimeoutWaitSeconds*1000); + + private bool notfound; + + public Locator() + { + _purplePath.Delimiter = Config.settings.purpleSettings.Purple_Delimiter; + _purplePath.BlankValue = Config.settings.purpleSettings.Purple_blankValue; + _purplePath.DefaultWindowName = Config.settings.purpleSettings.Purple_windowTitle; + _purplePath.ValueDelimiterStart = Config.settings.purpleSettings.Purple_ValueDelimiterStart; + _purplePath.ValueDelimiterEnd = Config.settings.purpleSettings.Purple_ValueDelimiterEnd; + + elementTimeoutTimer.Elapsed += elementTimeout; + } + + public PurplePath ByPurplePath + { + get { return _purplePath; } + } + + //TODO need to figure out how and why this kills subsequent tests - prossible due to unhandled exception + //handling it though causes the test to hang instead of moving onto the next test in the suite + public AutomationElement WaitForElementAvailable(string purplePath, string name) + { + elementTimeoutTimer.Start(); + AutomationElement elementAvailable = null; + var stopWatch = new Stopwatch(); + stopWatch.Start(); + Log.Message(string.Format("Locating Element: {0} ", name)); + while (elementAvailable == null) + { + try + { + elementAvailable = ByPurplePath.FindElement(purplePath); + } + catch (Exception e) + { + Thread.Sleep(50); + } + if (notfound) + { + if (PurpleTestBase.PerfLogging) + { + PurplePerformanceLogger.AddEntry(name, purplePath, + Config.settings.purpleSettings.Purple_ElementTimeoutWaitSeconds, 0); + } + break; + } + } + elementTimeoutTimer.Stop(); + stopWatch.Stop(); + if (!notfound) + { + var time = stopWatch.Elapsed; + Log.Message(string.Format("Element: {2} found in {0}.{1} seconds.", time.Seconds, time.Milliseconds, + name)); + if (PurpleTestBase.PerfLogging) + { + PurplePerformanceLogger.AddEntry(name, purplePath, time.Seconds, time.Milliseconds); + } + } + if (elementAvailable == null) + { + Assert.Fail("Element: {0} with Path: {1} Failed to respond in alloted time.", name, purplePath); + } + return elementAvailable; + } + + private void elementTimeout(object source, ElapsedEventArgs args) + { + Log.Message("Element took longer than " + Config.settings.purpleSettings.Purple_ElementTimeoutWaitSeconds + + " Seconds to respond."); + notfound = true; + } + + public bool HasChildren(string purplePath, string name) + { + var presumedParent = WaitForElementAvailable(purplePath, name); + return ByPurplePath.HasChildren(presumedParent); + } + + public List GetChildren(AutomationElement presumedParent) + { + return ByPurplePath.GetChildren(presumedParent); + } + + public string FindPurplePath(AutomationElement element) + { + return ByPurplePath.getPurplePath(element); + } + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/PurplePerformanceLogger.cs b/Purple/PurpleCore/PurplePerformanceLogger.cs new file mode 100644 index 0000000..6f12be5 --- /dev/null +++ b/Purple/PurpleCore/PurplePerformanceLogger.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Golem.Purple.PurpleCore +{ + public static class PurplePerformanceLogger + { + private static readonly List LogEvents = new List(); + + public static void AddEntry(string name, string locator, int seconds, int miliseconds) + { + var newEntry = new LogEntries(); + newEntry.ElementName = name; + newEntry.ElementLocator = locator; + newEntry.SecondsToLocate = seconds; + newEntry.MilisecondsToLocate = miliseconds; + LogEvents.Add(newEntry); + } + + public struct LogEntries + { + public string ElementLocator; + public string ElementName; + public int MilisecondsToLocate; + public int SecondsToLocate; + } + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/RectX.cs b/Purple/PurpleCore/RectX.cs new file mode 100644 index 0000000..c05ca5d --- /dev/null +++ b/Purple/PurpleCore/RectX.cs @@ -0,0 +1,53 @@ +using System.Windows; + +namespace Golem.Purple.PurpleCore +{ + //This was copied from TestStack.White source code + //Teststack.White available at http://docs.teststack.net/White/GettingStarted.html + public static class RectX + { + public static readonly Point UnlikelyWindowPosition = new Point(-10000, -10000); + + public static bool IsZeroSize(this Rect rect) + { + return rect.Height == 0 && rect.Width == 0; + } + + public static Point Center(this Rect rect) + { + var topLeftX = rect.Left; + var topRightX = rect.Right; + return new Point((int) (topLeftX + (topRightX - topLeftX)/2), (int) (rect.Top + (rect.Bottom - rect.Top)/2)); + } + + public static Point East(this Rect rectangle, int by) + { + return new Point((int) (rectangle.Right + by), rectangle.Center().Y); + } + + public static Point ImmediateExteriorEast(this Rect rectangle) + { + return new Point((int) (rectangle.Right + 1), rectangle.Center().Y); + } + + public static Point ImmediateInteriorEast(this Rect rectangle) + { + return new Point((int) (rectangle.Right - 1), rectangle.Center().Y); + } + + public static Point ImmediateExteriorWest(this Rect rectangle) + { + return new Point((int) (rectangle.Left - 1), rectangle.Center().Y); + } + + public static Point ImmediateInteriorNorth(this Rect rectangle) + { + return new Point(rectangle.Center().X, rectangle.Top + 1); + } + + public static Point ImmediateInteriorSouth(this Rect rectangle) + { + return new Point(rectangle.Center().X, rectangle.Bottom - 1); + } + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/ScreenCapture.cs b/Purple/PurpleCore/ScreenCapture.cs new file mode 100644 index 0000000..a6b9f9a --- /dev/null +++ b/Purple/PurpleCore/ScreenCapture.cs @@ -0,0 +1,63 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Golem.Purple.PurpleCore +{ + /// + /// Provides functions to capture the entire screen, or a particular window, and save it to a file. + /// + public class ScreenCapture + { + // This code is a modified version of many similar classes along the same lines. Source from TestStack.White + // TestStack.white available: http://docs.teststack.net/White/GettingStarted.html + + [DllImport("gdi32.dll")] + private static extern bool BitBlt(IntPtr hdcDest, int xDest, int yDest, int wDest, int hDest, IntPtr hdcSource, + int xSrc, int ySrc, CopyPixelOperation rop); + + [DllImport("user32.dll")] + private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc); + + [DllImport("gdi32.dll")] + private static extern IntPtr DeleteDC(IntPtr hDc); + + [DllImport("gdi32.dll")] + private static extern IntPtr DeleteObject(IntPtr hDc); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + private static extern IntPtr SelectObject(IntPtr hdc, IntPtr bmp); + + [DllImport("user32.dll")] + public static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + public static extern IntPtr GetWindowDC(IntPtr ptr); + + public virtual Bitmap CaptureScreenShot() + { + var sz = Screen.PrimaryScreen.Bounds.Size; + var hDesk = GetDesktopWindow(); + var hSrce = GetWindowDC(hDesk); + var hDest = CreateCompatibleDC(hSrce); + var hBmp = CreateCompatibleBitmap(hSrce, sz.Width, sz.Height); + var hOldBmp = SelectObject(hDest, hBmp); + BitBlt(hDest, 0, 0, sz.Width, sz.Height, hSrce, 0, 0, + CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt); + var bmp = Image.FromHbitmap(hBmp); + SelectObject(hDest, hOldBmp); + DeleteObject(hBmp); + DeleteDC(hDest); + ReleaseDC(hDesk, hSrce); + + return bmp; + } + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/Window_VisualStyle.cs b/Purple/PurpleCore/Window_VisualStyle.cs new file mode 100644 index 0000000..3316e19 --- /dev/null +++ b/Purple/PurpleCore/Window_VisualStyle.cs @@ -0,0 +1,9 @@ +namespace Golem.Purple.PurpleCore +{ + public enum Window_VisualStyle + { + Maximized, + Minimized, + Normal + } +} \ No newline at end of file diff --git a/Purple/PurpleCore/WindowsConstants.cs b/Purple/PurpleCore/WindowsConstants.cs new file mode 100644 index 0000000..fdf1db8 --- /dev/null +++ b/Purple/PurpleCore/WindowsConstants.cs @@ -0,0 +1,50 @@ +namespace Purple.Core +{ + /// + /// Constants defined in winuser.h in C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\Include\WinUser.h + /// + public class WindowsConstants + { + public const uint SW_HIDE = 0; + public const uint SW_SHOWNORMAL = 1; + public const uint SW_NORMAL = 1; + public const uint SW_SHOWMINIMIZED = 2; + public const uint SW_SHOWMAXIMIZED = 3; + public const uint SW_MAXIMIZE = 3; + public const uint SW_SHOWNOACTIVATE = 4; + public const uint SW_SHOW = 5; + public const uint SW_MINIMIZE = 6; + public const uint SW_SHOWMINNOACTIVE = 7; + public const uint SW_SHOWNA = 8; + public const uint SW_RESTORE = 9; + public const uint SW_SHOWDEFAULT = 10; + public const uint SW_FORCEMINIMIZE = 11; + public const uint SW_MAX = 11; + public const long WS_CAPTION = 0x00C00000L; + public const long WS_DISABLED = 0x08000000L; + public const long WS_VSCROLL = 0x00200000L; + public const long WS_HSCROLL = 0x00100000L; + public const long WS_MINIMIZEBOX = 0x00020000L; + public const long WS_MAXIMIZEBOX = 0x00010000L; + public const long WS_POPUP = 0x80000000L; + public const long WS_SYSMENU = 0x00080000L; + public const long WS_TABSTOP = 0x00010000L; + public const long WS_VISIBLE = 0x10000000L; + public const int INPUT_MOUSE = 0; + public const int INPUT_KEYBOARD = 1; + public const int MOUSEEVENTF_MOVE = 0x0001; + public const int MOUSEEVENTF_LEFTDOWN = 0x0002; + public const int MOUSEEVENTF_LEFTUP = 0x0004; + public const int MOUSEEVENTF_RIGHTDOWN = 0x0008; + public const int MOUSEEVENTF_RIGHTUP = 0x0010; + public const int MOUSEEVENTF_MIDDLEDOWN = 0x0020; + public const int MOUSEEVENTF_MIDDLEUP = 0x0040; + public const int MOUSEEVENTF_XDOWN = 0x0080; + public const int MOUSEEVENTF_XUP = 0x0100; + public const int MOUSEEVENTF_WHEEL = 0x0800; + public const int MOUSEEVENTF_VIRTUALDESK = 0x4000; + public const int MOUSEEVENTF_ABSOLUTE = 0x8000; + public const int HourGlassValue = 65557; + public static uint WPF_RESTORETOMAXIMIZED = 2; + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleButton.cs b/Purple/PurpleElements/PurpleButton.cs new file mode 100644 index 0000000..5fbe642 --- /dev/null +++ b/Purple/PurpleElements/PurpleButton.cs @@ -0,0 +1,16 @@ +using System.Windows.Automation; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleButton : PurpleElementBase + { + //TODO Finish implimenting PurpleButton + public PurpleButton(string name, string pPath) : base(name, pPath) + { + } + + public PurpleButton(string name, AutomationElement element) : base(name, element) + { + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleCheckBox.cs b/Purple/PurpleElements/PurpleCheckBox.cs new file mode 100644 index 0000000..961cc51 --- /dev/null +++ b/Purple/PurpleElements/PurpleCheckBox.cs @@ -0,0 +1,49 @@ +using System; +using System.Windows.Automation; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleCheckBox : PurpleElementBase + { + public PurpleCheckBox(string name, string locatorPath) : base(name, locatorPath) + { + } + + //TODO: Finish Implimenting PurpleCheckBox. + public bool Checked + { + get { return IsElementToggledOn(); } + set { Check(value); } + } + + public bool IsElementToggledOn() + { + Object objPattern; + TogglePattern togPattern; + if (PurpleElement.TryGetCurrentPattern(TogglePattern.Pattern, out objPattern)) + { + togPattern = objPattern as TogglePattern; + return togPattern.Current.ToggleState == ToggleState.On; + } + return false; + } + + public void Check(bool value) + { + if (IsElementToggledOn()) + { + if (!value) + { + Click(); + } + } + else + { + if (value) + { + Click(); + } + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleComboBox.cs b/Purple/PurpleElements/PurpleComboBox.cs new file mode 100644 index 0000000..1c19ce8 --- /dev/null +++ b/Purple/PurpleElements/PurpleComboBox.cs @@ -0,0 +1,14 @@ +namespace Golem.Purple.PurpleElements +{ + public class PurpleComboBox : PurpleElementBase + { + public PurpleComboBox(string name, string locatorPath) : base(name, locatorPath) + { + } + + //TODO: Impliment PurpleComboBox + public void Select(string option) + { + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleDropDown.cs b/Purple/PurpleElements/PurpleDropDown.cs new file mode 100644 index 0000000..4e3e1c5 --- /dev/null +++ b/Purple/PurpleElements/PurpleDropDown.cs @@ -0,0 +1,146 @@ +using System; +using System.Windows.Automation; +using Golem.Core; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleDropDown : PurpleElementBase + { + private AutomationElementCollection availableOptions; + + public PurpleDropDown(string name, string locatorPath) : base(name, locatorPath) + { + } + + //change to get selected item + public string GetSelected() + { + var nameofSelected = ""; + if (_UIAElement != null) + { + object basePattern; + if (_UIAElement.TryGetCurrentPattern(SelectionPattern.Pattern, out basePattern)) + { + var selection = (BasePattern) basePattern as SelectionPattern; + if (selection != null) + { + var automationElements = selection.Current.GetSelection(); + foreach (var automationElement in automationElements) + { + nameofSelected = automationElement.Current.Name; + } + } + } + } + else + { + if (PurpleElement.Current.IsEnabled) + { + GetSelected(); + } + } + return nameofSelected; + } + + public void SelectItem(string item) + { + if (_UIAElement != null) + { + //Now we need to find the right one + var matchingIndex = -1; + AutomationElement itemToSelect = null; + + object basePattern; + if (_UIAElement.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out basePattern)) + { + var expand = (BasePattern) basePattern as ExpandCollapsePattern; + if (expand != null) + { + if (expand.Current.ExpandCollapseState == ExpandCollapseState.Collapsed) + { + expand.Expand(); + + availableOptions = _UIAElement.FindAll(TreeScope.Subtree, + new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem)); + for (var x = 0; x < availableOptions.Count; x++) + { + if (item == availableOptions[x].Current.Name) + { + itemToSelect = availableOptions[x]; + } + } + if (itemToSelect != null) + { + var selectPattern = + (SelectionItemPattern) itemToSelect.GetCurrentPattern(SelectionItemPattern.Pattern); + try + { + selectPattern.Select(); + } + catch (Exception e) + { + Log.Message("An exception was handled by PurpleDropDown Class: " + e.Message); + } + } + } + } + } + } + else + { + if (PurpleElement.Current.IsEnabled) + { + SelectItem(item); + } + } + } + + public void SelectItemByPosition(int item) + { + if (_UIAElement != null) + { + //Now we need to find the right one + //int matchingIndex = -1; + //AutomationElement itemToSelect = null; + object basePattern; + if (_UIAElement.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out basePattern)) + { + var expand = (BasePattern) basePattern as ExpandCollapsePattern; + if (expand != null) + { + if (expand.Current.ExpandCollapseState == ExpandCollapseState.Collapsed) + { + expand.Expand(); + + availableOptions = _UIAElement.FindAll(TreeScope.Subtree, + new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem)); + + if (item < availableOptions.Count && availableOptions[item - 1] != null) + { + var selectPattern = + (SelectionItemPattern) + availableOptions[item - 1].GetCurrentPattern(SelectionItemPattern.Pattern); + try + { + selectPattern.Select(); + } + catch (Exception e) + { + //There is a timeout exception on the filter data panel + Log.Message("An exception was handled by PurpleDropDown Class: " + e.Message); + } + } + } + } + } + } + else + { + if (PurpleElement.Current.IsEnabled) + { + SelectItemByPosition(item); + } + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleElementBase.cs b/Purple/PurpleElements/PurpleElementBase.cs new file mode 100644 index 0000000..eea7946 --- /dev/null +++ b/Purple/PurpleElements/PurpleElementBase.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows; +using System.Windows.Automation; +using Golem.Purple.Elements; +using Golem.Purple.PurpleCore; +using Purple.Core; +using PurpleLib; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleElementBase : IPurpleElement + { + private Locator _locator; + protected AutomationElement _UIAElement; + //This is not built yet -- should be interesting we might want to do this on the screenobject base + private UIA_ElementCacher elementCache; + + public PurpleElementBase(string name, string locatorPath) + { + ElementName = name; + PurplePath = locatorPath; + } + + public PurpleElementBase(string name, AutomationElement element) + { + ElementName = name; + _UIAElement = element; + PurplePath = aLocator.FindPurplePath(element); + } + + /// + /// This can be used to find a PurplePath for an automation element + /// + public Locator aLocator + { + get { return new Locator(); } + } + + //These functions are used to set the cursor position and handle click events + [DllImport("user32.dll")] + private static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll")] + private static extern void mouse_event(uint dwFlags, int dx, int dy, uint cButtons, uint dwExtraInfo); + + public void Invoke() + { + //This function may need to go in a different class -- although a lot of elements use the Invoke Pattern + ((InvokePattern) PurpleElement.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); + } + + public void SetFocus() + { + if (_UIAElement != null) + { + _UIAElement.SetFocus(); + } + else + { + if (!PurpleElement.Current.IsOffscreen) + { + SetFocus(); + } + } + } + + public bool IsEnabled() + { + var enabled = false; + if (_UIAElement == null) + { + enabled = PurpleElement.Current.IsEnabled; + } + else + { + enabled = _UIAElement.Current.IsEnabled; + } + return enabled; + } + + public bool IsOnScreen() + { + bool isVisible; + if (_UIAElement != null) + { + if (_UIAElement.Current.IsOffscreen) + { + isVisible = false; + } + else + { + isVisible = true; + } + } + else + { + if (PurpleElement.Current.IsOffscreen) + { + isVisible = false; + } + else + { + isVisible = true; + } + } + return isVisible; + } + + #region Accessor Methods + + public AutomationElement PurpleElement + { + get + { + //This will wait for specified ElementTimeout config before trying to interact with the element + _UIAElement = aLocator.WaitForElementAvailable(PurplePath, ElementName); + return _UIAElement; + } + } + + public Rect Bounds + { + get { return PurpleElement.Current.BoundingRectangle; } + } + + //used with Interface + public String ElementName { get; private set; } + public String PurplePath { get; private set; } + + public AutomationElement UIAElement + { + get { return _UIAElement; } + } + + public bool HasChildren + { + get { return aLocator.HasChildren(PurplePath, ElementName); } + } + + public List GetChildren() + { + return aLocator.GetChildren(PurpleElement); + } + + #endregion + + #region MouseFunctions Functions for dealing with and simulating mouse input + + public void MoveCursor(Point position) + { + SetCursorPos((int) position.X, (int) position.Y); + } + + public void LMB_Down() + { + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + } + + public void LMB_Up() + { + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + } + + public void RMB_Down() + { + mouse_event(WindowsConstants.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0); + } + + public void RMB_Up() + { + mouse_event(WindowsConstants.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0); + } + + public void Click() + { + if (_UIAElement != null) + { + var point = _UIAElement.GetClickablePoint(); + SetCursorPos((int) point.X, (int) point.Y); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + } + else + { + if (!PurpleElement.Current.IsOffscreen) + { + if (_UIAElement.Current.IsEnabled) + { + Click(); + } + } + } + } + + public void DoubleLeftClick() + { + if (_UIAElement != null) + { + var point = PurpleElement.GetClickablePoint(); + SetCursorPos((int) point.X, (int) point.Y); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + } + else + { + if (!PurpleElement.Current.IsOffscreen) + { + if (_UIAElement.Current.IsEnabled) + { + DoubleLeftClick(); + } + } + } + } + + public void RightClick() + { + if (_UIAElement != null) + { + var point = _UIAElement.GetClickablePoint(); + SetCursorPos((int) point.X, (int) point.Y); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0); + Thread.Sleep(50); + mouse_event(WindowsConstants.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0); + } + else + { + if (!PurpleElement.Current.IsOffscreen) + { + if (_UIAElement.Current.IsEnabled) + { + RightClick(); + } + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleMenu.cs b/Purple/PurpleElements/PurpleMenu.cs new file mode 100644 index 0000000..0e624ba --- /dev/null +++ b/Purple/PurpleElements/PurpleMenu.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Threading; +using System.Windows.Automation; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleMenu : PurpleElementBase + { + private string _pathAfterMenu; + private string[] _pathtoMenuSelection; + + public PurpleMenu(string name, string locatorPath, string targetPath) : base(name, locatorPath) + { + _pathAfterMenu = targetPath; + } + + public new void Click() + { + //This is not used since we can invoke menu directly + var menuNums = _pathtoMenuSelection.Count(); + AutomationElement menu = null; + for (var x = 0; x < menuNums - 1; x++) + { + menu = PurpleElement.FindFirst(TreeScope.Descendants, + new PropertyCondition(AutomationElement.NameProperty, _pathtoMenuSelection[x])); + ((ExpandCollapsePattern) menu.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Expand(); + Thread.Sleep(50); + } + if (menu != null) + { + var MenuItem = menu.FindFirst(TreeScope.Descendants, + new PropertyCondition(AutomationElement.NameProperty, _pathtoMenuSelection[menuNums - 1])); + ((InvokePattern) MenuItem.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurplePanel.cs b/Purple/PurpleElements/PurplePanel.cs new file mode 100644 index 0000000..36a3093 --- /dev/null +++ b/Purple/PurpleElements/PurplePanel.cs @@ -0,0 +1,29 @@ +using System.Windows; + +namespace Golem.Purple.PurpleElements +{ + public class PurplePanel : PurpleElementBase + { + public PurplePanel(string name, string locatorPath) : base(name, locatorPath) + { + } + + public void DragAndDrop(Point startPoint, Point endPoint, bool RMB = false) + { + if (!RMB) //Use the left mouse button as default + { + MoveCursor(startPoint); + LMB_Down(); + MoveCursor(endPoint); + LMB_Up(); + } + else + { + MoveCursor(startPoint); + RMB_Down(); + MoveCursor(endPoint); + RMB_Up(); + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleRadioButton.cs b/Purple/PurpleElements/PurpleRadioButton.cs new file mode 100644 index 0000000..e6cf657 --- /dev/null +++ b/Purple/PurpleElements/PurpleRadioButton.cs @@ -0,0 +1,9 @@ +namespace Golem.Purple.PurpleElements +{ + public class PurpleRadioButton : PurpleElementBase + { + public PurpleRadioButton(string name, string locatorPath) : base(name, locatorPath) + { + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleSpinner.cs b/Purple/PurpleElements/PurpleSpinner.cs new file mode 100644 index 0000000..dff50e2 --- /dev/null +++ b/Purple/PurpleElements/PurpleSpinner.cs @@ -0,0 +1,17 @@ +namespace Golem.Purple.PurpleElements +{ + public class PurpleSpinner : PurpleElementBase + { + public PurpleSpinner(string name, string locatorPath) : base(name, locatorPath) + { + } + + public void Increment() + { + } + + public void Decrement() + { + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleTable.cs b/Purple/PurpleElements/PurpleTable.cs new file mode 100644 index 0000000..ded29c2 --- /dev/null +++ b/Purple/PurpleElements/PurpleTable.cs @@ -0,0 +1,101 @@ +using System.Windows.Automation; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleTable : PurpleElementBase + { + private int _colCount = -1; + private int _rowCount = -1; + private object basePattern2; + // Changes for the sake of speeding up the code - Start + + private bool PElementPattern; + + public PurpleTable(string name, string locatorPath) : base(name, locatorPath) + { + } + + public int RowCount + { + get + { + if (_rowCount.Equals(-1)) + { + SetCounts(); + } + return _rowCount; + } + } + + public int ColumnCount + { + get + { + if (_colCount.Equals(-1)) + { + SetCounts(); + } + return _colCount; + } + } + + public string GetValue(int row, int column) + { + var somenonsense = ""; + object basePattern; + if (PurpleElement.TryGetCurrentPattern(TablePattern.Pattern, out basePattern)) + { + var gridItem = (BasePattern) basePattern as TablePattern; + if (gridItem != null) + { + var tableitem = gridItem.GetItem(row, column); + somenonsense = tableitem.Current.Name; + } + } + return somenonsense; + } + + public void EvaluatePattern() + { + if (PurpleElement.TryGetCurrentPattern(TablePattern.Pattern, out basePattern2)) + { + PElementPattern = true; + } + else + { + PElementPattern = false; + } + } + + public string GetValueNew(int row, int column) + { + var somenonsense = ""; + if (PElementPattern) + { + var gridItem = (BasePattern) basePattern2 as TablePattern; + if (gridItem != null) + { + var tableitem = gridItem.GetItem(row, column); + somenonsense = tableitem.Current.Name; + } + } + return somenonsense; + } + + // Changes for the sake of speeding up the code - End + + private void SetCounts() + { + object basePattern; + if (PurpleElement.TryGetCurrentPattern(TablePattern.Pattern, out basePattern)) + { + var gridItem = (BasePattern) basePattern as TablePattern; + if (gridItem != null) + { + _rowCount = gridItem.Current.RowCount; + _colCount = gridItem.Current.ColumnCount; + } + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleTextBox.cs b/Purple/PurpleElements/PurpleTextBox.cs new file mode 100644 index 0000000..350ed20 --- /dev/null +++ b/Purple/PurpleElements/PurpleTextBox.cs @@ -0,0 +1,73 @@ +using System.Windows.Automation; +using System.Windows.Forms; +using Golem.Core; + +namespace Golem.Purple.PurpleElements +{ + public class PurpleTextBox : PurpleElementBase + { + private string _textToEnter; + + public PurpleTextBox(string name, string locatorPath) : base(name, locatorPath) + { + } + + public PurpleTextBox(string name, AutomationElement element) : base(name, element) + { + } + + public string Text + { + get + { + var enteredText = "ELEMENT NOT FOUND"; + if (PurpleElement != null) + { + enteredText = GetText(); + } + return enteredText; + } + set + { + if (PurpleElement.Current.IsEnabled) + { + EnterText(value); + } + } + } + + private string GetText() + { + var textValue = "THERE IS NO TEXT"; + if (_UIAElement.Current.IsPassword) + { + Log.Message(string.Format("Field is {0} is a Password field, cannot get value", ElementName)); + textValue = "PASSWORD FIELD CANNOT BE READ"; + } + object basePattern; + if (_UIAElement.TryGetCurrentPattern(ValuePattern.Pattern, out basePattern)) + { + var valuePattern = (BasePattern) basePattern as ValuePattern; + if (valuePattern != null) + { + textValue = valuePattern.Current.Value; + } + var textPattern = (BasePattern) basePattern as TextPattern; + if (textPattern != null) + { + textValue = textPattern.DocumentRange.GetText(int.MaxValue); + } + } + return textValue; + } + + private void EnterText(string val) + { + if (!_UIAElement.Current.IsOffscreen) + { + _UIAElement.SetFocus(); + SendKeys.SendWait(val); + } + } + } +} \ No newline at end of file diff --git a/Purple/PurpleElements/PurpleWindow.cs b/Purple/PurpleElements/PurpleWindow.cs new file mode 100644 index 0000000..2abefd5 --- /dev/null +++ b/Purple/PurpleElements/PurpleWindow.cs @@ -0,0 +1,154 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Automation; +using WindowsInput; +using Golem.Core; +using Golem.Purple.PurpleCore; + +namespace Golem.Purple.PurpleElements +{ + public static class PurpleWindow + { + private static IntPtr handle; + private static Locator _locator; + public static AutomationElement purpleWindow { get; private set; } + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, + uint uFlags); + + public static bool FindRunningProcess() + { + var processRunning = false; + var processes = Process.GetProcessesByName(Config.settings.purpleSettings.ProcessName); + if (processes.Length == 0) + { + Log.Message(string.Format("Could not find process {0}. Attempting to start process...", + Config.settings.purpleSettings.ProcessName)); + var startProcess = new ProcessStartInfo(Config.settings.purpleSettings.appPath); + var app = Process.Start(startProcess); + waitForWindow(); + + //SetWindowPos(app.MainWindowHandle, new IntPtr(-1), 0, 0, 0, 0, 3); //This should bring the application to the front once it starts + //Thread.Sleep(2000); + //SetForegroundWindow(handle); //this didn't bring the app to the front + } + else + { + Log.Message( + string.Format("Process Length is: {0}. Attempting to kill existing process and start it up again.", + processes.Length)); + EndProcess(); + FindRunningProcess(); + } + //the PurpleLib will always try to find an element the first window the with name = Purple_windowTitle; + return processRunning; + } + + private static void waitForWindow() + { + _locator = new Locator(); + purpleWindow = + _locator.WaitForElementAvailable( + Config.settings.purpleSettings.Purple_Delimiter + Config.settings.purpleSettings.Purple_windowTitle, + Config.settings.purpleSettings.Purple_windowTitle); + } + + public static void EndProcess(String DontsaveProjectPath = "notused") + { + purpleWindow = null; + var processes = Process.GetProcessesByName(Config.settings.purpleSettings.ProcessName); + foreach (var process in processes) + { + process.Kill(); + Thread.Sleep(2000); + //process.CloseMainWindow(); + //PurpleButton dontsave = new PurpleButton("Save Dialog: No", "/LifeQuest™ Pipeline/Save Project?/Save Project?/No"); + //dontsave.Invoke(); + } + } + + private static WindowPattern GetWindowPattern(AutomationElement targetControl) + { + WindowPattern windowPattern = null; + + try + { + windowPattern = targetControl.GetCurrentPattern(WindowPattern.Pattern) + as WindowPattern; + } + catch (InvalidOperationException) + { + // object doesn't support the WindowPattern control pattern + return null; + } + // Make sure the element is usable. + if (false == windowPattern.WaitForInputIdle(10000)) + { + // Object not responding in a timely manner + return null; + } + return windowPattern; + } + + public static void SetVisualState(Window_VisualStyle visualState) + { + var windowPattern = GetWindowPattern(purpleWindow); + + try + { + if (windowPattern.Current.WindowInteractionState == WindowInteractionState.ReadyForUserInteraction) + { + switch (visualState) + { + case Window_VisualStyle.Maximized: + // Confirm that the element can be maximized + if ((windowPattern.Current.CanMaximize) && !(windowPattern.Current.IsModal)) + { + windowPattern.SetWindowVisualState(WindowVisualState.Maximized); + } + break; + case Window_VisualStyle.Minimized: + // Confirm that the element can be minimized + if ((windowPattern.Current.CanMinimize) && !(windowPattern.Current.IsModal)) + { + windowPattern.SetWindowVisualState(WindowVisualState.Minimized); + } + break; + case Window_VisualStyle.Normal: + windowPattern.SetWindowVisualState(WindowVisualState.Normal); + break; + default: + windowPattern.SetWindowVisualState(WindowVisualState.Normal); + break; + } + } + } + catch (InvalidOperationException) + { + // object is not able to perform the requested action + } + } + + //These are duplicated on the PurpleElement we should pick a place + public static void HoldKey(VirtualKeyCode key) + { + InputSimulator.SimulateKeyDown(key); + } + + public static void ReleaseKey(VirtualKeyCode key) + { + InputSimulator.SimulateKeyUp(key); + } + + public static void PressKey(VirtualKeyCode key) + { + InputSimulator.SimulateKeyPress(key); + } + } +} \ No newline at end of file diff --git a/Purple/PurpleTestBase.cs b/Purple/PurpleTestBase.cs new file mode 100644 index 0000000..3b83e62 --- /dev/null +++ b/Purple/PurpleTestBase.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Golem.Core; +using Golem.Purple.PurpleElements; + +namespace Golem.Purple +{ + public class PurpleTestBase : TestBase + { + public static List TestOutcomes = new List(); + private string ProjectFile; + private string TestFileLoc; + //Used for logging how long it takes elements to appear. + //Has to be set in the constructor for the testclass + public static bool PerfLogging { get; set; } + + public string TestFileLocation + { + get + { + if (TestFileLoc == null) + { + SetFileInfo(); + } + return TestFileLoc; + } + } + + public string ProjectFileName + { + get + { + if (ProjectFile == null) + { + SetFileInfo(); + } + return ProjectFile; + } + } + + private void SetFileInfo() + { + //TODO This is fugly - need a better solution + TestFileLoc = @"C:\"; + ProjectFile = "NOT CONFIGURED"; + var machineName = Environment.MachineName; + if (machineName == Config.settings.purpleSettings.Machine1) + { + TestFileLoc = Config.settings.purpleSettings.DataSetPath1; + ProjectFile = Config.settings.purpleSettings.ProjectName1; + } + if (machineName == Config.settings.purpleSettings.Machine2) + { + TestFileLoc = Config.settings.purpleSettings.DataSetPath2; + ProjectFile = Config.settings.purpleSettings.ProjectName2; + } + if (machineName == Config.settings.purpleSettings.Machine3) + { + TestFileLoc = Config.settings.purpleSettings.DataSetPath3; + ProjectFile = Config.settings.purpleSettings.ProjectName3; + } + if (machineName == Config.settings.purpleSettings.Machine4) + { + TestFileLoc = Config.settings.purpleSettings.DataSetPath4; + ProjectFile = Config.settings.purpleSettings.ProjectName4; + } + } + + public void TestSettings() + { + } + + [SetUp] + public void SetUp() + { + PurpleWindow.FindRunningProcess(); + } + + [TearDown] + public override void TearDownTestBase() + { + Log.Message(Common.GetCurrentTestName() + " " + TestContext.CurrentContext.Result.Outcome); + PurpleWindow.EndProcess(); + } + + [TestFixtureTearDown] + public void FixtureTearDown() + { + } + + public void LogScreenshotIfTestFailed() + { + //Will need to rework logscreenshot if failed for NUnit + //if(TestContext.CurrentContext.Outcome!=TestOutcome.Passed) + // TestLog.EmbedImage(null, PurpleWindow.purpleWindow.GetImage()); + } + } +} \ No newline at end of file diff --git a/Resources/InputSimulator.XML b/Resources/InputSimulator.XML new file mode 100644 index 0000000..002b2e9 --- /dev/null +++ b/Resources/InputSimulator.XML @@ -0,0 +1,1362 @@ + + + + InputSimulator + + + + + XButton definitions for use in the MouseData property of the structure. (See: http://msdn.microsoft.com/en-us/library/ms646273(VS.85).aspx) + + + + + Set if the first X button is pressed or released. + + + + + Set if the second X button is pressed or released. + + + + + The combined/overlayed structure that includes Mouse, Keyboard and Hardware Input message data (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) + + + + + The INPUT structure is used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks. (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) + Declared in Winuser.h, include Windows.h + + + This structure contains information identical to that used in the parameter list of the keybd_event or mouse_event function. + Windows 2000/XP: INPUT_KEYBOARD supports nonkeyboard input methods, such as handwriting recognition or voice recognition, as if it were text input by using the KEYEVENTF_UNICODE flag. For more information, see the remarks section of KEYBDINPUT. + + + + + Specifies the type of the input event. This member can be one of the following values. + InputType.MOUSE - The event is a mouse event. Use the mi structure of the union. + InputType.KEYBOARD - The event is a keyboard event. Use the ki structure of the union. + InputType.HARDWARE - Windows 95/98/Me: The event is from input hardware other than a keyboard or mouse. Use the hi structure of the union. + + + + + The data structure that contains information about the simulated Mouse, Keyboard or Hardware event. + + + + + The list of VirtualKeyCodes (see: http://msdn.microsoft.com/en-us/library/ms645540(VS.85).aspx) + + + + + Left mouse button + + + + + Right mouse button + + + + + Control-break processing + + + + + Middle mouse button (three-button mouse) - NOT contiguous with LBUTTON and RBUTTON + + + + + Windows 2000/XP: X1 mouse button - NOT contiguous with LBUTTON and RBUTTON + + + + + Windows 2000/XP: X2 mouse button - NOT contiguous with LBUTTON and RBUTTON + + + + + BACKSPACE key + + + + + TAB key + + + + + CLEAR key + + + + + ENTER key + + + + + SHIFT key + + + + + CTRL key + + + + + ALT key + + + + + PAUSE key + + + + + CAPS LOCK key + + + + + Input Method Editor (IME) Kana mode + + + + + IME Hanguel mode (maintained for compatibility; use HANGUL) + + + + + IME Hangul mode + + + + + IME Junja mode + + + + + IME final mode + + + + + IME Hanja mode + + + + + IME Kanji mode + + + + + ESC key + + + + + IME convert + + + + + IME nonconvert + + + + + IME accept + + + + + IME mode change request + + + + + SPACEBAR + + + + + PAGE UP key + + + + + PAGE DOWN key + + + + + END key + + + + + HOME key + + + + + LEFT ARROW key + + + + + UP ARROW key + + + + + RIGHT ARROW key + + + + + DOWN ARROW key + + + + + SELECT key + + + + + PRINT key + + + + + EXECUTE key + + + + + PRINT SCREEN key + + + + + INS key + + + + + DEL key + + + + + HELP key + + + + + 0 key + + + + + 1 key + + + + + 2 key + + + + + 3 key + + + + + 4 key + + + + + 5 key + + + + + 6 key + + + + + 7 key + + + + + 8 key + + + + + 9 key + + + + + A key + + + + + B key + + + + + C key + + + + + D key + + + + + E key + + + + + F key + + + + + G key + + + + + H key + + + + + I key + + + + + J key + + + + + K key + + + + + L key + + + + + M key + + + + + N key + + + + + O key + + + + + P key + + + + + Q key + + + + + R key + + + + + S key + + + + + T key + + + + + U key + + + + + V key + + + + + W key + + + + + X key + + + + + Y key + + + + + Z key + + + + + Left Windows key (Microsoft Natural keyboard) + + + + + Right Windows key (Natural keyboard) + + + + + Applications key (Natural keyboard) + + + + + Computer Sleep key + + + + + Numeric keypad 0 key + + + + + Numeric keypad 1 key + + + + + Numeric keypad 2 key + + + + + Numeric keypad 3 key + + + + + Numeric keypad 4 key + + + + + Numeric keypad 5 key + + + + + Numeric keypad 6 key + + + + + Numeric keypad 7 key + + + + + Numeric keypad 8 key + + + + + Numeric keypad 9 key + + + + + Multiply key + + + + + Add key + + + + + Separator key + + + + + Subtract key + + + + + Decimal key + + + + + Divide key + + + + + F1 key + + + + + F2 key + + + + + F3 key + + + + + F4 key + + + + + F5 key + + + + + F6 key + + + + + F7 key + + + + + F8 key + + + + + F9 key + + + + + F10 key + + + + + F11 key + + + + + F12 key + + + + + F13 key + + + + + F14 key + + + + + F15 key + + + + + F16 key + + + + + F17 key + + + + + F18 key + + + + + F19 key + + + + + F20 key + + + + + F21 key + + + + + F22 key + + + + + F23 key + + + + + F24 key + + + + + NUM LOCK key + + + + + SCROLL LOCK key + + + + + Left SHIFT key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Right SHIFT key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Left CONTROL key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Right CONTROL key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Left MENU key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Right MENU key - Used only as parameters to GetAsyncKeyState() and GetKeyState() + + + + + Windows 2000/XP: Browser Back key + + + + + Windows 2000/XP: Browser Forward key + + + + + Windows 2000/XP: Browser Refresh key + + + + + Windows 2000/XP: Browser Stop key + + + + + Windows 2000/XP: Browser Search key + + + + + Windows 2000/XP: Browser Favorites key + + + + + Windows 2000/XP: Browser Start and Home key + + + + + Windows 2000/XP: Volume Mute key + + + + + Windows 2000/XP: Volume Down key + + + + + Windows 2000/XP: Volume Up key + + + + + Windows 2000/XP: Next Track key + + + + + Windows 2000/XP: Previous Track key + + + + + Windows 2000/XP: Stop Media key + + + + + Windows 2000/XP: Play/Pause Media key + + + + + Windows 2000/XP: Start Mail key + + + + + Windows 2000/XP: Select Media key + + + + + Windows 2000/XP: Start Application 1 key + + + + + Windows 2000/XP: Start Application 2 key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the ';:' key + + + + + Windows 2000/XP: For any country/region, the '+' key + + + + + Windows 2000/XP: For any country/region, the ',' key + + + + + Windows 2000/XP: For any country/region, the '-' key + + + + + Windows 2000/XP: For any country/region, the '.' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the '/?' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the '`~' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the '[{' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the '\|' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the ']}' key + + + + + Used for miscellaneous characters; it can vary by keyboard. Windows 2000/XP: For the US standard keyboard, the 'single-quote/double-quote' key + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard + + + + + Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key + + + + + Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. The PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP + + + + + Attn key + + + + + CrSel key + + + + + ExSel key + + + + + Erase EOF key + + + + + Play key + + + + + Zoom key + + + + + Reserved + + + + + PA1 key + + + + + Clear key + + + + + Specifies various aspects of a keystroke. This member can be certain combinations of the following values. + + + + + KEYEVENTF_EXTENDEDKEY = 0x0001 (If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224).) + + + + + KEYEVENTF_KEYUP = 0x0002 (If specified, the key is being released. If not specified, the key is being pressed.) + + + + + KEYEVENTF_UNICODE = 0x0004 (If specified, wScan identifies the key and wVk is ignored.) + + + + + KEYEVENTF_SCANCODE = 0x0008 (Windows 2000/XP: If specified, the system synthesizes a VK_PACKET keystroke. The wVk parameter must be zero. This flag can only be combined with the KEYEVENTF_KEYUP flag. For more information, see the Remarks section.) + + + + + The KEYBDINPUT structure contains information about a simulated keyboard event. (see: http://msdn.microsoft.com/en-us/library/ms646271(VS.85).aspx) + Declared in Winuser.h, include Windows.h + + + Windows 2000/XP: INPUT_KEYBOARD supports nonkeyboard-input methods—such as handwriting recognition or voice recognition—as if it were text input by using the KEYEVENTF_UNICODE flag. If KEYEVENTF_UNICODE is specified, SendInput sends a WM_KEYDOWN or WM_KEYUP message to the foreground thread's message queue with wParam equal to VK_PACKET. Once GetMessage or PeekMessage obtains this message, passing the message to TranslateMessage posts a WM_CHAR message with the Unicode character originally specified by wScan. This Unicode character will automatically be converted to the appropriate ANSI value if it is posted to an ANSI window. + Windows 2000/XP: Set the KEYEVENTF_SCANCODE flag to define keyboard input in terms of the scan code. This is useful to simulate a physical keystroke regardless of which keyboard is currently being used. The virtual key value of a key may alter depending on the current keyboard layout or what other keys were pressed, but the scan code will always be the same. + + + + + Specifies a virtual-key code. The code must be a value in the range 1 to 254. The Winuser.h header file provides macro definitions (VK_*) for each value. If the dwFlags member specifies KEYEVENTF_UNICODE, wVk must be 0. + + + + + Specifies a hardware scan code for the key. If dwFlags specifies KEYEVENTF_UNICODE, wScan specifies a Unicode character which is to be sent to the foreground application. + + + + + Specifies various aspects of a keystroke. This member can be certain combinations of the following values. + KEYEVENTF_EXTENDEDKEY - If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224). + KEYEVENTF_KEYUP - If specified, the key is being released. If not specified, the key is being pressed. + KEYEVENTF_SCANCODE - If specified, wScan identifies the key and wVk is ignored. + KEYEVENTF_UNICODE - Windows 2000/XP: If specified, the system synthesizes a VK_PACKET keystroke. The wVk parameter must be zero. This flag can only be combined with the KEYEVENTF_KEYUP flag. For more information, see the Remarks section. + + + + + Time stamp for the event, in milliseconds. If this parameter is zero, the system will provide its own time stamp. + + + + + Specifies an additional value associated with the keystroke. Use the GetMessageExtraInfo function to obtain this information. + + + + + Specifies the type of the input event. This member can be one of the following values. + + + + + INPUT_MOUSE = 0x00 (The event is a mouse event. Use the mi structure of the union.) + + + + + INPUT_KEYBOARD = 0x01 (The event is a keyboard event. Use the ki structure of the union.) + + + + + INPUT_HARDWARE = 0x02 (Windows 95/98/Me: The event is from input hardware other than a keyboard or mouse. Use the hi structure of the union.) + + + + + Provides a useful wrapper around the User32 SendInput and related native Windows functions. + + + + + The SendInput function synthesizes keystrokes, mouse motions, and button clicks. + + Number of structures in the Inputs array. + Pointer to an array of INPUT structures. Each structure represents an event to be inserted into the keyboard or mouse input stream. + Specifies the size, in bytes, of an INPUT structure. If cbSize is not the size of an INPUT structure, the function fails. + The function returns the number of events that it successfully inserted into the keyboard or mouse input stream. If the function returns zero, the input was already blocked by another thread. To get extended error information, call GetLastError.Microsoft Windows Vista. This function fails when it is blocked by User Interface Privilege Isolation (UIPI). Note that neither GetLastError nor the return value will indicate the failure was caused by UIPI blocking. + + Microsoft Windows Vista. This function is subject to UIPI. Applications are permitted to inject input only into applications that are at an equal or lesser integrity level. + The SendInput function inserts the events in the INPUT structures serially into the keyboard or mouse input stream. These events are not interspersed with other keyboard or mouse input events inserted either by the user (with the keyboard or mouse) or by calls to keybd_event, mouse_event, or other calls to SendInput. + This function does not reset the keyboard's current state. Any keys that are already pressed when the function is called might interfere with the events that this function generates. To avoid this problem, check the keyboard's state with the GetAsyncKeyState function and correct as necessary. + + + + + The GetAsyncKeyState function determines whether a key is up or down at the time the function is called, and whether the key was pressed after a previous call to GetAsyncKeyState. (See: http://msdn.microsoft.com/en-us/library/ms646293(VS.85).aspx) + + Specifies one of 256 possible virtual-key codes. For more information, see Virtual Key Codes. Windows NT/2000/XP: You can use left- and right-distinguishing constants to specify certain keys. See the Remarks section for further information. + + If the function succeeds, the return value specifies whether the key was pressed since the last call to GetAsyncKeyState, and whether the key is currently up or down. If the most significant bit is set, the key is down, and if the least significant bit is set, the key was pressed after the previous call to GetAsyncKeyState. However, you should not rely on this last behavior; for more information, see the Remarks. + + Windows NT/2000/XP: The return value is zero for the following cases: + - The current desktop is not the active desktop + - The foreground thread belongs to another process and the desktop does not allow the hook or the journal record. + + Windows 95/98/Me: The return value is the global asynchronous key state for each virtual key. The system does not check which thread has the keyboard focus. + + Windows 95/98/Me: Windows 95 does not support the left- and right-distinguishing constants. If you call GetAsyncKeyState with these constants, the return value is zero. + + + The GetAsyncKeyState function works with mouse buttons. However, it checks on the state of the physical mouse buttons, not on the logical mouse buttons that the physical buttons are mapped to. For example, the call GetAsyncKeyState(VK_LBUTTON) always returns the state of the left physical mouse button, regardless of whether it is mapped to the left or right logical mouse button. You can determine the system's current mapping of physical mouse buttons to logical mouse buttons by calling + Copy CodeGetSystemMetrics(SM_SWAPBUTTON) which returns TRUE if the mouse buttons have been swapped. + + Although the least significant bit of the return value indicates whether the key has been pressed since the last query, due to the pre-emptive multitasking nature of Windows, another application can call GetAsyncKeyState and receive the "recently pressed" bit instead of your application. The behavior of the least significant bit of the return value is retained strictly for compatibility with 16-bit Windows applications (which are non-preemptive) and should not be relied upon. + + You can use the virtual-key code constants VK_SHIFT, VK_CONTROL, and VK_MENU as values for the vKey parameter. This gives the state of the SHIFT, CTRL, or ALT keys without distinguishing between left and right. + + Windows NT/2000/XP: You can use the following virtual-key code constants as values for vKey to distinguish between the left and right instances of those keys. + + Code Meaning + VK_LSHIFT Left-shift key. + VK_RSHIFT Right-shift key. + VK_LCONTROL Left-control key. + VK_RCONTROL Right-control key. + VK_LMENU Left-menu key. + VK_RMENU Right-menu key. + + These left- and right-distinguishing constants are only available when you call the GetKeyboardState, SetKeyboardState, GetAsyncKeyState, GetKeyState, and MapVirtualKey functions. + + + + + The GetKeyState function retrieves the status of the specified virtual key. The status specifies whether the key is up, down, or toggled (on, off alternating each time the key is pressed). (See: http://msdn.microsoft.com/en-us/library/ms646301(VS.85).aspx) + + + Specifies a virtual key. If the desired virtual key is a letter or digit (A through Z, a through z, or 0 through 9), nVirtKey must be set to the ASCII value of that character. For other keys, it must be a virtual-key code. + If a non-English keyboard layout is used, virtual keys with values in the range ASCII A through Z and 0 through 9 are used to specify most of the character keys. For example, for the German keyboard layout, the virtual key of value ASCII O (0x4F) refers to the "o" key, whereas VK_OEM_1 refers to the "o with umlaut" key. + + + The return value specifies the status of the specified virtual key, as follows: + If the high-order bit is 1, the key is down; otherwise, it is up. + If the low-order bit is 1, the key is toggled. A key, such as the CAPS LOCK key, is toggled if it is turned on. The key is off and untoggled if the low-order bit is 0. A toggle key's indicator light (if any) on the keyboard will be on when the key is toggled, and off when the key is untoggled. + + + The key status returned from this function changes as a thread reads key messages from its message queue. The status does not reflect the interrupt-level state associated with the hardware. Use the GetAsyncKeyState function to retrieve that information. + An application calls GetKeyState in response to a keyboard-input message. This function retrieves the state of the key when the input message was generated. + To retrieve state information for all the virtual keys, use the GetKeyboardState function. + An application can use the virtual-key code constants VK_SHIFT, VK_CONTROL, and VK_MENU as values for the nVirtKey parameter. This gives the status of the SHIFT, CTRL, or ALT keys without distinguishing between left and right. An application can also use the following virtual-key code constants as values for nVirtKey to distinguish between the left and right instances of those keys. + VK_LSHIFT + VK_RSHIFT + VK_LCONTROL + VK_RCONTROL + VK_LMENU + VK_RMENU + + These left- and right-distinguishing constants are available to an application only through the GetKeyboardState, SetKeyboardState, GetAsyncKeyState, GetKeyState, and MapVirtualKey functions. + + + + + The GetMessageExtraInfo function retrieves the extra message information for the current thread. Extra message information is an application- or driver-defined value associated with the current thread's message queue. + + + To set a thread's extra message information, use the SetMessageExtraInfo function. + + + + Determines whether a key is up or down at the time the function is called by calling the GetAsyncKeyState function. (See: http://msdn.microsoft.com/en-us/library/ms646293(VS.85).aspx) + + The key code. + + true if the key is down; otherwise, false. + + + The GetAsyncKeyState function works with mouse buttons. However, it checks on the state of the physical mouse buttons, not on the logical mouse buttons that the physical buttons are mapped to. For example, the call GetAsyncKeyState(VK_LBUTTON) always returns the state of the left physical mouse button, regardless of whether it is mapped to the left or right logical mouse button. You can determine the system's current mapping of physical mouse buttons to logical mouse buttons by calling + Copy CodeGetSystemMetrics(SM_SWAPBUTTON) which returns TRUE if the mouse buttons have been swapped. + + Although the least significant bit of the return value indicates whether the key has been pressed since the last query, due to the pre-emptive multitasking nature of Windows, another application can call GetAsyncKeyState and receive the "recently pressed" bit instead of your application. The behavior of the least significant bit of the return value is retained strictly for compatibility with 16-bit Windows applications (which are non-preemptive) and should not be relied upon. + + You can use the virtual-key code constants VK_SHIFT, VK_CONTROL, and VK_MENU as values for the vKey parameter. This gives the state of the SHIFT, CTRL, or ALT keys without distinguishing between left and right. + + Windows NT/2000/XP: You can use the following virtual-key code constants as values for vKey to distinguish between the left and right instances of those keys. + + Code Meaning + VK_LSHIFT Left-shift key. + VK_RSHIFT Right-shift key. + VK_LCONTROL Left-control key. + VK_RCONTROL Right-control key. + VK_LMENU Left-menu key. + VK_RMENU Right-menu key. + + These left- and right-distinguishing constants are only available when you call the GetKeyboardState, SetKeyboardState, GetAsyncKeyState, GetKeyState, and MapVirtualKey functions. + + + + + Determines whether the specified key is up or down by calling the GetKeyState function. (See: http://msdn.microsoft.com/en-us/library/ms646301(VS.85).aspx) + + The for the key. + + true if the key is down; otherwise, false. + + + The key status returned from this function changes as a thread reads key messages from its message queue. The status does not reflect the interrupt-level state associated with the hardware. Use the GetAsyncKeyState function to retrieve that information. + An application calls GetKeyState in response to a keyboard-input message. This function retrieves the state of the key when the input message was generated. + To retrieve state information for all the virtual keys, use the GetKeyboardState function. + An application can use the virtual-key code constants VK_SHIFT, VK_CONTROL, and VK_MENU as values for the nVirtKey parameter. This gives the status of the SHIFT, CTRL, or ALT keys without distinguishing between left and right. An application can also use the following virtual-key code constants as values for nVirtKey to distinguish between the left and right instances of those keys. + VK_LSHIFT + VK_RSHIFT + VK_LCONTROL + VK_RCONTROL + VK_LMENU + VK_RMENU + + These left- and right-distinguishing constants are available to an application only through the GetKeyboardState, SetKeyboardState, GetAsyncKeyState, GetKeyState, and MapVirtualKey functions. + + + + + Determines whether the toggling key is toggled on (in-effect) or not by calling the GetKeyState function. (See: http://msdn.microsoft.com/en-us/library/ms646301(VS.85).aspx) + + The for the key. + + true if the toggling key is toggled on (in-effect); otherwise, false. + + + The key status returned from this function changes as a thread reads key messages from its message queue. The status does not reflect the interrupt-level state associated with the hardware. Use the GetAsyncKeyState function to retrieve that information. + An application calls GetKeyState in response to a keyboard-input message. This function retrieves the state of the key when the input message was generated. + To retrieve state information for all the virtual keys, use the GetKeyboardState function. + An application can use the virtual-key code constants VK_SHIFT, VK_CONTROL, and VK_MENU as values for the nVirtKey parameter. This gives the status of the SHIFT, CTRL, or ALT keys without distinguishing between left and right. An application can also use the following virtual-key code constants as values for nVirtKey to distinguish between the left and right instances of those keys. + VK_LSHIFT + VK_RSHIFT + VK_LCONTROL + VK_RCONTROL + VK_LMENU + VK_RMENU + + These left- and right-distinguishing constants are available to an application only through the GetKeyboardState, SetKeyboardState, GetAsyncKeyState, GetKeyState, and MapVirtualKey functions. + + + + + Calls the Win32 SendInput method to simulate a Key DOWN. + + The VirtualKeyCode to press + + + + Calls the Win32 SendInput method to simulate a Key UP. + + The VirtualKeyCode to lift up + + + + Calls the Win32 SendInput method with a KeyDown and KeyUp message in the same input sequence in order to simulate a Key PRESS. + + The VirtualKeyCode to press + + + + Calls the Win32 SendInput method with a stream of KeyDown and KeyUp messages in order to simulate uninterrupted text entry via the keyboard. + + The text to be simulated. + + + + Performs a simple modified keystroke like CTRL-C where CTRL is the modifierKey and C is the key. + The flow is Modifier KEYDOWN, Key PRESS, Modifier KEYUP. + + The modifier key + The key to simulate + + + + Performs a modified keystroke where there are multiple modifiers and one key like CTRL-ALT-C where CTRL and ALT are the modifierKeys and C is the key. + The flow is Modifiers KEYDOWN in order, Key PRESS, Modifiers KEYUP in reverse order. + + The list of modifier keys + The key to simulate + + + + Performs a modified keystroke where there is one modifier and multiple keys like CTRL-K-C where CTRL is the modifierKey and K and C are the keys. + The flow is Modifier KEYDOWN, Keys PRESS in order, Modifier KEYUP. + + The modifier key + The list of keys to simulate + + + + Performs a modified keystroke where there are multiple modifiers and multiple keys like CTRL-ALT-K-C where CTRL and ALT are the modifierKeys and K and C are the keys. + The flow is Modifiers KEYDOWN in order, Keys PRESS in order, Modifiers KEYUP in reverse order. + + The list of modifier keys + The list of keys to simulate + + + + The MOUSEINPUT structure contains information about a simulated mouse event. (see: http://msdn.microsoft.com/en-us/library/ms646273(VS.85).aspx) + Declared in Winuser.h, include Windows.h + + + If the mouse has moved, indicated by MOUSEEVENTF_MOVE, dxand dy specify information about that movement. The information is specified as absolute or relative integer values. + If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535. The event procedure maps these coordinates onto the display surface. Coordinate (0,0) maps onto the upper-left corner of the display surface; coordinate (65535,65535) maps onto the lower-right corner. In a multimonitor system, the coordinates map to the primary monitor. + Windows 2000/XP: If MOUSEEVENTF_VIRTUALDESK is specified, the coordinates map to the entire virtual desktop. + If the MOUSEEVENTF_ABSOLUTE value is not specified, dxand dy specify movement relative to the previous mouse event (the last reported position). Positive values mean the mouse moved right (or down); negative values mean the mouse moved left (or up). + Relative mouse motion is subject to the effects of the mouse speed and the two-mouse threshold values. A user sets these three values with the Pointer Speed slider of the Control Panel's Mouse Properties sheet. You can obtain and set these values using the SystemParametersInfo function. + The system applies two tests to the specified relative mouse movement. If the specified distance along either the x or y axis is greater than the first mouse threshold value, and the mouse speed is not zero, the system doubles the distance. If the specified distance along either the x or y axis is greater than the second mouse threshold value, and the mouse speed is equal to two, the system doubles the distance that resulted from applying the first threshold test. It is thus possible for the system to multiply specified relative mouse movement along the x or y axis by up to four times. + + + + + Specifies the absolute position of the mouse, or the amount of motion since the last mouse event was generated, depending on the value of the dwFlags member. Absolute data is specified as the x coordinate of the mouse; relative data is specified as the number of pixels moved. + + + + + Specifies the absolute position of the mouse, or the amount of motion since the last mouse event was generated, depending on the value of the dwFlags member. Absolute data is specified as the y coordinate of the mouse; relative data is specified as the number of pixels moved. + + + + + If dwFlags contains MOUSEEVENTF_WHEEL, then mouseData specifies the amount of wheel movement. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120. + Windows Vista: If dwFlags contains MOUSEEVENTF_HWHEEL, then dwData specifies the amount of wheel movement. A positive value indicates that the wheel was rotated to the right; a negative value indicates that the wheel was rotated to the left. One wheel click is defined as WHEEL_DELTA, which is 120. + Windows 2000/XP: IfdwFlags does not contain MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN, or MOUSEEVENTF_XUP, then mouseData should be zero. + If dwFlags contains MOUSEEVENTF_XDOWN or MOUSEEVENTF_XUP, then mouseData specifies which X buttons were pressed or released. This value may be any combination of the following flags. + + + + + A set of bit flags that specify various aspects of mouse motion and button clicks. The bits in this member can be any reasonable combination of the following values. + The bit flags that specify mouse button status are set to indicate changes in status, not ongoing conditions. For example, if the left mouse button is pressed and held down, MOUSEEVENTF_LEFTDOWN is set when the left button is first pressed, but not for subsequent motions. Similarly, MOUSEEVENTF_LEFTUP is set only when the button is first released. + You cannot specify both the MOUSEEVENTF_WHEEL flag and either MOUSEEVENTF_XDOWN or MOUSEEVENTF_XUP flags simultaneously in the dwFlags parameter, because they both require use of the mouseData field. + + + + + Time stamp for the event, in milliseconds. If this parameter is 0, the system will provide its own time stamp. + + + + + Specifies an additional value associated with the mouse event. An application calls GetMessageExtraInfo to obtain this extra information. + + + + + The set of MouseFlags for use in the Flags property of the structure. (See: http://msdn.microsoft.com/en-us/library/ms646273(VS.85).aspx) + + + + + Specifies that movement occurred. + + + + + Specifies that the left button was pressed. + + + + + Specifies that the left button was released. + + + + + Specifies that the right button was pressed. + + + + + Specifies that the right button was released. + + + + + Specifies that the middle button was pressed. + + + + + Specifies that the middle button was released. + + + + + Windows 2000/XP: Specifies that an X button was pressed. + + + + + Windows 2000/XP: Specifies that an X button was released. + + + + + Windows NT/2000/XP: Specifies that the wheel was moved, if the mouse has a wheel. The amount of movement is specified in mouseData. + + + + + Windows 2000/XP: Maps coordinates to the entire desktop. Must be used with MOUSEEVENTF_ABSOLUTE. + + + + + Specifies that the dx and dy members contain normalized absolute coordinates. If the flag is not set, dxand dy contain relative data (the change in position since the last reported position). This flag can be set, or not set, regardless of what kind of mouse or other pointing device, if any, is connected to the system. For further information about relative mouse motion, see the following Remarks section. + + + + + The HARDWAREINPUT structure contains information about a simulated message generated by an input device other than a keyboard or mouse. (see: http://msdn.microsoft.com/en-us/library/ms646269(VS.85).aspx) + Declared in Winuser.h, include Windows.h + + + + + Value specifying the message generated by the input hardware. + + + + + Specifies the low-order word of the lParam parameter for uMsg. + + + + + Specifies the high-order word of the lParam parameter for uMsg. + + + + diff --git a/Resources/InputSimulator.dll b/Resources/InputSimulator.dll new file mode 100644 index 0000000..604b366 Binary files /dev/null and b/Resources/InputSimulator.dll differ diff --git a/Resources/PurpleLib.dll b/Resources/PurpleLib.dll new file mode 100644 index 0000000..fe46837 Binary files /dev/null and b/Resources/PurpleLib.dll differ diff --git a/Rest/DynamicXml.cs b/Rest/DynamicXml.cs new file mode 100644 index 0000000..98bd69a --- /dev/null +++ b/Rest/DynamicXml.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Xml.Linq; + +namespace Golem.Rest +{ + public class DynamicXml + { + /// + /// Provides a mechanism to get a dynamic object from an xml document + /// + /// + /// + public static void Parse(dynamic parent, XElement node) + { + if (node.HasElements) + { + if (node.Elements(node.Elements().First().Name.LocalName).Count() > 1) + { + var item = new ExpandoObject(); + var list = new List(); + foreach (var element in node.Elements()) + { + Parse(list, element); + } + + AddProperty(item, node.Elements().First().Name.LocalName, list); + AddProperty(parent, node.Name.ToString(), item); + } + else + { + var item = new ExpandoObject(); + + foreach (var attribute in node.Attributes()) + { + AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim()); + } + + //element + foreach (var element in node.Elements()) + { + Parse(item, element); + } + + AddProperty(parent, node.Name.ToString(), item); + } + } + else + { + AddProperty(parent, node.Name.ToString(), node.Value.Trim()); + } + } + + private static void AddProperty(dynamic parent, string name, object value) + { + if (parent is List) + { + (parent as List).Add(value); + } + else + { + (parent as IDictionary)[name] = value; + } + } + } +} \ No newline at end of file diff --git a/Rest/Given.cs b/Rest/Given.cs new file mode 100644 index 0000000..b67de43 --- /dev/null +++ b/Rest/Given.cs @@ -0,0 +1,86 @@ +using System; +using System.Net; +using RestSharp; + +namespace Golem.Rest +{ + //given.Domain("http://www.google.com").Header("key","value").Authentication(username,password).when.post("/resource/id").then.responseBody.Verify().Text("text in body").header.Verify().Text("Header text"); + /// + /// Given class holds methods for any setup, such as url setting, and request configuring. + /// + public class Given + { + private readonly IRestClient client; + private readonly IRestRequest request; + private readonly IRestResponse response; + + public Given(WebProxy proxy = null) + { + client = new RestClient(); + response = new RestResponse(); + request = new RestRequest(); + if (proxy != null) + { + client.Proxy = proxy; + } + } + + public When When + { + get { return new When(client, request, response); } + } + + public Given And + { + get { return this; } + } + + public Given Token(string token, string value) + { + request.AddUrlSegment(token, value); + return this; + } + + public Given Resource(string resource) + { + request.Resource = resource; + return this; + } + + public Given Header(string name, string value) + { + request.AddHeader(name, value); + return this; + } + + public Given Domain(string domain) + { + client.BaseUrl = new Uri(domain); + return this; + } + + public Given Paramater(string name, string value) + { + request.AddParameter(name, value); + return this; + } + + public Given File(string filePath) + { + request.AddFile("", filePath); + return this; + } + + public Given Body(string bodyString) + { + request.AddParameter("text/xml", bodyString, ParameterType.RequestBody); + return this; + } + + public Given Cookie(string name, string value) + { + request.AddCookie(name, value); + return this; + } + } +} \ No newline at end of file diff --git a/Rest/OAuth/OAuth_Token_Keeper.cs b/Rest/OAuth/OAuth_Token_Keeper.cs new file mode 100644 index 0000000..65d635f --- /dev/null +++ b/Rest/OAuth/OAuth_Token_Keeper.cs @@ -0,0 +1,151 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Golem.Rest.OAuth +{ + public static class REST_VERB + { + public const string GET = "GET"; + public const string POST = "POST"; + } + + public static class Oauth_Signature_Methods + { + //enum Signautre_Methods {HMAC_SHA1, RSA_SHA1} + public const string HMAC_SHA1 = "HMAC-SHA1"; + public const string RSA_SHA1 = "RSA-SHA1"; + public const string VERSION = "1.0"; + } + + public static class OAuth_Token_Keeper + { + //This class is used to store the things needed for oauthentication + public static string oauth_token { get; set; } + public static string oauth_token_secret { get; set; } + public static string oauth_consumer_key { get; set; } + public static string oauth_consumer_secret { get; set; } + public static string oauth_signature_method { get; set; } + public static string resource_url { get; set; } + public static string oauth_version { get; set; } + + public static bool checkvalues() + { + var populated = false; + if (oauth_token != null && oauth_token_secret != null && oauth_consumer_key != null && + oauth_consumer_secret != null && oauth_signature_method != null && resource_url != null && + oauth_version != null) + { + populated = true; + } + return populated; + } + } + + public class OAuth_Request_Builder + { + private readonly string baseFormat = + "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}"; + + private readonly string http_verb; + private readonly string resource_URL; + private string baseString; + private string oauth_nonce; + private string oauth_signature; //This is the hashed string + private string oauth_timestamp; + + public OAuth_Request_Builder(string URL, string verb) + { + resource_URL = URL; + http_verb = verb; + } + + private void setNonce() + { + //building this at the time th request is made + oauth_nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); + } + + private void setTimestamp() + { + //building this at the time the request is made + var timeSpan = DateTime.UtcNow + - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString(); + } + + private void BuildBaseString(string parameters) + { + //we need to get a basestring out of this eventually + //There maybe some special considerations in here (i.e. Twitter API expects parameters in alphabetical order) + //check the keys from the token_keeper + if (OAuth_Token_Keeper.checkvalues()) + { + setNonce(); + setTimestamp(); + baseString = string.Format(baseFormat, + OAuth_Token_Keeper.oauth_consumer_key, + oauth_nonce, + OAuth_Token_Keeper.oauth_signature_method, + oauth_timestamp, + OAuth_Token_Keeper.oauth_token, + OAuth_Token_Keeper.oauth_version + ); + } + baseString += parameters; + //TODO: check to make sure base string doesn't end with & + baseString = string.Concat(http_verb + "&", Uri.EscapeDataString(resource_URL), "&", + Uri.EscapeDataString(baseString)); + } + + private string compositeKey() + { + string key; + if (OAuth_Token_Keeper.oauth_consumer_secret != null && OAuth_Token_Keeper.oauth_token_secret != null) + { + key = string.Concat(Uri.EscapeDataString(OAuth_Token_Keeper.oauth_consumer_secret), "&", + Uri.EscapeDataString(OAuth_Token_Keeper.oauth_token_secret)); + } + else + { + //Should probably add this to logging instead of returning an invalid key + key = "Problem with values in OAuth_Token_Keeper consumer_secret OR token_secret"; + } + return key; + } + + private void buildHash() + { + if (OAuth_Token_Keeper.oauth_signature_method != null) + { + //TODO: Need to check to make sure all the peices are set up correctly first + + if (OAuth_Token_Keeper.oauth_signature_method == Oauth_Signature_Methods.HMAC_SHA1) + { + using (var hasher = new HMACSHA1(Encoding.ASCII.GetBytes(compositeKey()))) + { + oauth_signature = Convert.ToBase64String(hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString))); + } + } + } + } + + private void buildHeader() + { + var headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", " + + "oauth_timestamp=\"{2}\", oauth_consumer_key=\"{3}\", " + + "oauth_token=\"{4}\", oauth_signature=\"{5}\", " + + "oauth_version=\"{6}\""; + + var authHeader = string.Format(headerFormat, + Uri.EscapeDataString(oauth_nonce), + Uri.EscapeDataString(OAuth_Token_Keeper.oauth_signature_method), + Uri.EscapeDataString(oauth_timestamp), + Uri.EscapeDataString(OAuth_Token_Keeper.oauth_consumer_key), + Uri.EscapeDataString(OAuth_Token_Keeper.oauth_token), + Uri.EscapeDataString(oauth_signature), + Uri.EscapeDataString(OAuth_Token_Keeper.oauth_version) + ); + } + } +} \ No newline at end of file diff --git a/Rest/OAuth/simpleTwitterHandler.cs b/Rest/OAuth/simpleTwitterHandler.cs new file mode 100644 index 0000000..b7a534b --- /dev/null +++ b/Rest/OAuth/simpleTwitterHandler.cs @@ -0,0 +1,202 @@ +using System; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using Golem.Rest.OAuth; + +namespace TwitterOAuth +{ + internal class simpleTwitterHandler + { + public static void postMessage(string message) + { + // oauth application keys + var oauth_token = OAuth_Token_Keeper.oauth_consumer_key; + var oauth_token_secret = OAuth_Token_Keeper.oauth_token_secret; + var oauth_consumer_key = OAuth_Token_Keeper.oauth_consumer_key; + var oauth_consumer_secret = OAuth_Token_Keeper.oauth_consumer_secret; + + // oauth implementation details + var oauth_version = "1.0"; + var oauth_signature_method = "HMAC-SHA1"; + + + // unique request details + var oauth_nonce = Convert.ToBase64String( + new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); + var timeSpan = DateTime.UtcNow + - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString(); + + // message api details + var status = message; + var resource_url = "https://api.twitter.com/1.1/statuses/update.json"; + + // create oauth signature + var baseFormat = "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" + + "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}&status={6}"; + + var baseString = string.Format(baseFormat, + oauth_consumer_key, + oauth_nonce, + oauth_signature_method, + oauth_timestamp, + oauth_token, + oauth_version, + Uri.EscapeDataString(status) + ); + + baseString = string.Concat("POST&", Uri.EscapeDataString(resource_url), "&", + Uri.EscapeDataString(baseString)); + + var compositeKey = string.Concat(Uri.EscapeDataString(oauth_consumer_secret), + "&", Uri.EscapeDataString(oauth_token_secret)); + + string oauth_signature; + using (var hasher = new HMACSHA1(Encoding.ASCII.GetBytes(compositeKey))) + { + oauth_signature = Convert.ToBase64String( + hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString))); + } + + //create the request header + var headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", " + + "oauth_timestamp=\"{2}\", oauth_consumer_key=\"{3}\", " + + "oauth_token=\"{4}\", oauth_signature=\"{5}\", " + + "oauth_version=\"{6}\""; + + var authHeader = string.Format(headerFormat, + Uri.EscapeDataString(oauth_nonce), + Uri.EscapeDataString(oauth_signature_method), + Uri.EscapeDataString(oauth_timestamp), + Uri.EscapeDataString(oauth_consumer_key), + Uri.EscapeDataString(oauth_token), + Uri.EscapeDataString(oauth_signature), + Uri.EscapeDataString(oauth_version) + ); + + + // make the request + var postBody = "status=" + Uri.EscapeDataString(status); + + ServicePointManager.Expect100Continue = false; + + var request = (HttpWebRequest) WebRequest.Create(resource_url); + request.Headers.Add("Authorization", authHeader); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + using (var stream = request.GetRequestStream()) + { + var content = Encoding.ASCII.GetBytes(postBody); + stream.Write(content, 0, content.Length); + } + var response = request.GetResponse(); + var responsetext = response.ToString(); + } + + public static string getTweets() + { + // oauth application keys + var oauth_token = OAuth_Token_Keeper.oauth_token; + var oauth_token_secret = OAuth_Token_Keeper.oauth_token_secret; + var oauth_consumer_key = OAuth_Token_Keeper.oauth_consumer_key; + var oauth_consumer_secret = OAuth_Token_Keeper.oauth_consumer_secret; + + // oauth implementation details + var oauth_version = "1.0"; + var oauth_signature_method = "HMAC-SHA1"; + + // unique request details + var oauth_nonce = Convert.ToBase64String( + new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); + var timeSpan = DateTime.UtcNow + - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString(); + + // message api details + //var status = "Updating status via REST API if this works"; + var resource_url = "https://api.twitter.com/1.1/statuses/user_timeline.json"; + + // create oauth signature + var baseFormat = "count=1&oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" + + "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}&screen_name=HackathonRamrod"; + + var baseString = string.Format(baseFormat, + oauth_consumer_key, + oauth_nonce, + oauth_signature_method, + oauth_timestamp, + oauth_token, + oauth_version + //Uri.EscapeDataString(status) + ); + + baseString = string.Concat("GET&", Uri.EscapeDataString(resource_url), "&", Uri.EscapeDataString(baseString)); + + var compositeKey = string.Concat(Uri.EscapeDataString(oauth_consumer_secret), + "&", Uri.EscapeDataString(oauth_token_secret)); + + string oauth_signature; + using (var hasher = new HMACSHA1(Encoding.ASCII.GetBytes(compositeKey))) + { + oauth_signature = Convert.ToBase64String( + hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString))); + } + + // create the request header + var headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", " + + "oauth_timestamp=\"{2}\", oauth_consumer_key=\"{3}\", " + + "oauth_token=\"{4}\", oauth_signature=\"{5}\", " + + "oauth_version=\"{6}\""; + + var authHeader = string.Format(headerFormat, + Uri.EscapeDataString(oauth_nonce), + Uri.EscapeDataString(oauth_signature_method), + Uri.EscapeDataString(oauth_timestamp), + Uri.EscapeDataString(oauth_consumer_key), + Uri.EscapeDataString(oauth_token), + Uri.EscapeDataString(oauth_signature), + Uri.EscapeDataString(oauth_version) + ); + + + // make the request + //var postBody = "status=" + Uri.EscapeDataString(status); + + ServicePointManager.Expect100Continue = false; + resource_url += "?screen_name=HackathonRamrod&count=1"; + var request = (HttpWebRequest) WebRequest.Create(resource_url); + request.Headers.Add("Authorization", authHeader); + request.Method = "GET"; + request.ContentType = "application/x-www-form-urlencoded"; + var response = (HttpWebResponse) request.GetResponse(); + var reader = new StreamReader(response.GetResponseStream()); + var objText = reader.ReadToEnd(); + + return objText; + } + + public static string humanTweet(string machineTweet) + { + var realtweet = ""; + var created = machineTweet.IndexOf("{\"created_at\":") + 15; + var tweetid = machineTweet.IndexOf("\"id\":", created) + 5; + var endtweetid = machineTweet.IndexOf(",", tweetid); + var tweet = machineTweet.LastIndexOf("\"text\":\"") + 8; + var endTweet = machineTweet.LastIndexOf("\",\"source\""); + + var createdcount = (tweetid - 7) - created; + var tweetidcount = endtweetid - tweetid; + var tweetcount = endTweet - tweet; + + realtweet += machineTweet.Substring(created, createdcount); + realtweet += "\n"; + realtweet += machineTweet.Substring(tweetid, tweetidcount); + realtweet += "\n"; + realtweet += machineTweet.Substring(tweet, tweetcount); + + return realtweet; + } + } +} \ No newline at end of file diff --git a/Rest/RestResponseVerify.cs b/Rest/RestResponseVerify.cs new file mode 100644 index 0000000..99cd819 --- /dev/null +++ b/Rest/RestResponseVerify.cs @@ -0,0 +1,78 @@ +using System.Net; +using HtmlAgilityPack; +using Golem.Core; +using RestSharp; + +namespace Golem.Rest +{ + /// + /// Contains methods to validate rest responses + /// + public class RestResponseVerify + { + private readonly IRestResponse response; + + public RestResponseVerify(IRestResponse response) + { + this.response = response; + } + + public RestResponseVerify BodyContainsText(string text) + { + if (response.Content.Contains(text)) + { +// TestContext.CurrentContext.IncrementAssertCount(); + } + else + { + TestBase.AddVerificationError(string.Format("Response body does not contain {0}. Actual : \r\n{1}", text, + response.Content)); + } + return this; + } + + public RestResponseVerify XpathOnBody(string xpath) + { + var doc = new HtmlDocument(); + doc.LoadHtml(response.Content); + if (doc.DocumentNode.SelectNodes(xpath) == null) + { + TestBase.AddVerificationError(string.Format("Xpath Validation Failed for '{0}'. Body : {1}", xpath, + response.Content)); + } + else + { + } +// TestContext.CurrentContext.IncrementAssertCount(); + return this; + } + + public RestResponseVerify HeadersContainsText(string text) + { + if (response.Headers.ToString().Contains(text)) + { +// TestContext.CurrentContext.IncrementAssertCount(); + } + else + { + TestBase.AddVerificationError("Response headers did not contain '" + text + "'. Actual value : " + + response.Headers); + } + return this; + } + + public RestResponseVerify ResponseCode(HttpStatusCode code) + { + if (response.StatusCode == code) + { +// TestContext.CurrentContext.IncrementAssertCount(); + } + else + { + TestBase.AddVerificationError("Response Code was not correct. Epected " + code + " actual : " + + response.StatusCode); + } + return this; + } + } +} \ No newline at end of file diff --git a/Rest/RestTestBase.cs b/Rest/RestTestBase.cs new file mode 100644 index 0000000..acadaf1 --- /dev/null +++ b/Rest/RestTestBase.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Net; +using NUnit.Framework; +using Golem.Core; +using Golem.Proxy; + +namespace Golem.Rest +{ + /// + /// Test Base class to be inherited by all test fixtures. Will automatically instantiate an object named Given + /// + public class RestTestBase : TestBase + { + protected IDictionary Tokens; + + protected Given Given + { + get + { + WebProxy proxy = null; + if (Config.settings.httpProxy.startProxy) + { + proxy = new WebProxy("localhost:" + TestBase.proxy.proxyPort); + } + return new Given(proxy); + } + } + + private void LogHttpTrafficMetrics() + { + //if (Config.settings.httpProxy.startProxy) + //{ + // TestBase.proxy.GetSessionMetrics(); + // TestLog.BeginSection("HTTP Metrics"); + // Log.Message("Number of Requests : " + TestBase.proxy.numSessions); + // Log.Message("Min Response Time : " + TestBase.proxy.minResponseTime); + // Log.Message("Max Response Time : " + TestBase.proxy.maxResponseTime); + // Log.Message("Avg Response Time : " + TestBase.proxy.avgResponseTime); + // TestLog.End(); + //} + } + + private void GetHTTPTrafficInfo() + { + //if (Config.settings.httpProxy.startProxy) + //{ + // string name = Common.GetShortTestName(80); + // TestBase.proxy.SaveSessionsToFile(); + // TestLog.Attach(new BinaryAttachment("HTTP_Traffic_" + name + ".saz", + // "application/x-fiddler-session-archive", File.ReadAllBytes(TestBase.proxy.GetSazFilePath()))); + + // LogHttpTrafficMetrics(); + + // TestBase.proxy.ClearSessionList(); + //} + } + + private void StartProxy() + { + try + { + if (Config.settings.httpProxy.startProxy) + { + proxy = new BrowserMobProxy(); + proxy.StartServer(); + proxy.CreateProxy(); + proxy.CreateHar(); + } + } + catch (Exception) + { + } + } + + private void QuitProxy() + { + if (Config.settings.httpProxy.startProxy) + { + proxy.QuitServer(); + } + } + + [SetUp] + public void SetUp() + { + Tokens = new Dictionary(); + StartProxy(); + } + + [TearDown] + public void TearDown() + { + QuitProxy(); + GetHTTPTrafficInfo(); + } + } +} \ No newline at end of file diff --git a/Rest/Tests.cs b/Rest/Tests.cs new file mode 100644 index 0000000..2be6640 --- /dev/null +++ b/Rest/Tests.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Net; +using NUnit.Framework; +using Golem.WebDriver; + +namespace Golem.Rest +{ + /// + /// Example tests on how to use the REST Framework + /// + internal class Tests : RestTestBase + { + [Test] + public void testSauce() + { + Given.Domain(string.Format("https://{0}:{1}@saucelabs.com", "bkitchener", + "998969ff-ad37-4b2e-9ad7-edacd982bc59")) + .When.Get("rest/v1/bkitchener/activity").Then.Verify().ResponseCode(HttpStatusCode.Accepted); + } + + [Test] + public void TestGetStringFromBody() + { + var id = Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER") + .Then.GetStringFromBody("//CUSTOMER[10]/text()"); + + Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER/" + id) + .Then.Verify().BodyContainsText("" + id + ""); + } + + [Test] + public void TestDynamicBody() + { + dynamic body = Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER") + .Then.GetBodyAsDynamic(); + string id = body.CUSTOMERList.CUSTOMER[20]; + + Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER/" + id) + .Then.Verify().BodyContainsText("" + id + ""); + } + + [Test] + public void TestResponseCode() + { + Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER/10") + .Then.Verify().ResponseCode(HttpStatusCode.OK); + } + + [Test] + public void TestFailedVerification() + { + Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER/asdfasdfsdfasdfasdfsadf") + .Then.Verify().ResponseCode(HttpStatusCode.OK); + + Assert.AreEqual(testData.VerificationErrors.Count, 1, "Expected 1 error"); + testData.VerificationErrors = new List(); + } + + [Test] + public void TestCRUD() + { + Given.Domain("http://www.thomas-bayer.com") + .Body( + @"201312NikolaiTesla111 Madison Avenue.New York") + .When.Post("/sqlrest/CUSTOMER") + .Then.Verify().ResponseCode(HttpStatusCode.Created); + + + Given.Domain("http://www.thomas-bayer.com") + .Body( + @"201312NikolaiTesla111 Madison Avenue.New York") + .When.Get("/sqlrest/CUSTOMER/201312") + .Then.Verify().BodyContainsText("201312").BodyContainsText("Nikolai201312EinsteinAlbert111 Madison Avenue.New York") + .When.Put("/sqlrest/CUSTOMER") + .Then.Verify().BodyContainsText("Einstein") + .BodyContainsText("Albert"); + + Given.Domain("http://www.thomas-bayer.com") + .When.Delete("/sqlrest/CUSTOMER/201312") + .Then.Verify().ResponseCode(HttpStatusCode.OK); + + Given.Domain("http://www.thomas-bayer.com") + .When.Get("/sqlrest/CUSTOMER/201312") + .Then.Verify().ResponseCode(HttpStatusCode.NotFound); + } + } +} \ No newline at end of file diff --git a/Rest/Then.cs b/Rest/Then.cs new file mode 100644 index 0000000..f9e5d60 --- /dev/null +++ b/Rest/Then.cs @@ -0,0 +1,87 @@ +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Newtonsoft.Json; +using Golem.Core; +using RestSharp; + +namespace Golem.Rest +{ + /// + /// Then contains post-operation commands such as validations and return statements; + /// + public class Then + { + public IRestClient client; + public IRestRequest request; + public IRestResponse response; + + public Then(IRestClient client, IRestRequest request, IRestResponse response) + { + this.client = client; + this.request = request; + this.response = response; + } + + public Then And + { + get { return this; } + } + + public string Body + { + get { return response.Content; } + } + + public RestResponseVerify Verify(int timeoutSec = -1) + { + if (timeoutSec == -1) timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new RestResponseVerify(response); + } + + public T GetBodyAs() + { + var generic = (RestResponse) response; + return generic.Data; + } + + public dynamic GetBodyAsDynamic() + { + dynamic content; + if (response.ContentType.Contains("json")) + { + content = SimpleJson.DeserializeObject(response.Content); + } + else + { + var xDoc = XDocument.Load(new StringReader(response.Content)); + + dynamic root = new ExpandoObject(); + + DynamicXml.Parse(root, xDoc.Elements().First()); + content = root; + } + return content; + } + + public string GetBodyAsString() + { + return response.Content; + } + + public string GetStringFromBody(string xpath) + { + var doc = new XmlDocument(); + + if (response.ContentType.Contains("json")) + doc = JsonConvert.DeserializeXmlNode(response.Content); + else + doc.LoadXml(response.Content); + + var node = doc.SelectSingleNode(xpath); + return node.InnerText; + } + } +} \ No newline at end of file diff --git a/Rest/When.cs b/Rest/When.cs new file mode 100644 index 0000000..61f41ba --- /dev/null +++ b/Rest/When.cs @@ -0,0 +1,74 @@ +using System; +using Golem.Core; +using RestSharp; + +namespace Golem.Rest +{ + /// + /// When contains operation execution methods, such as commands to actually fire off a Rest request. + /// + public class When + { + private readonly IRestClient client; + private readonly IRestRequest request; + private Object data; + private IRestResponse response; + + public When(IRestClient client, IRestRequest request, IRestResponse response) + { + this.client = client; + this.request = request; + this.response = response; + data = new Object(); + } + + public Then Then + { + get { return new Then(client, request, response); } + } + + public When And + { + get { return this; } + } + + private void Execute(string resource, Method method) + { + request.Method = method; + if (resource != "") request.Resource = resource; + Log.Message(string.Format("Executing {0} : {1}{2}", request.Method, client.BaseUrl, request.Resource)); + response = client.Execute(request); + Log.Message(string.Format("Received Response : {0}", response.StatusCode)); + } + + public When Get(string resource = "") + { + Execute(resource, Method.GET); + return this; + } + + public When Post(string resource = "") + { + Execute(resource, Method.POST); + return this; + } + + public When Put(string resource = "") + { + Execute(resource, Method.PUT); + return this; + } + + public When Delete(string resource = "") + { + Execute(resource, Method.DELETE); + return this; + } + + public When Options(string resource = "") + { + Execute(resource, Method.OPTIONS); + return this; + } + } +} \ No newline at end of file diff --git a/Tests/BrowserStackTest.cs b/Tests/BrowserStackTest.cs new file mode 100644 index 0000000..7623cd7 --- /dev/null +++ b/Tests/BrowserStackTest.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using OpenQA.Selenium; +using ProtoTest.Golem.Core; +using ProtoTest.Golem.WebDriver; + +namespace ProtoTest.Golem.Tests +{ + internal class BrowserStackTest : WebDriverTestBase + { + public void Setup() + { + /* + Add the following to App.config to test this stuff + + + + + + + + */ + } + + [Test] + public void Test() + { + driver.Navigate().GoToUrl("http://www.espn.com"); + var element = new Element("ESPN Element", By.XPath("//*[@class='espn-logo']//*[text()='ESPN']")); + element.WaitUntil(10).Visible(); + Common.Log("Successfully navigated to " + driver.Title); + } + } +} \ No newline at end of file diff --git a/Tests/BrowserTests.cs b/Tests/BrowserTests.cs new file mode 100644 index 0000000..207b0c1 --- /dev/null +++ b/Tests/BrowserTests.cs @@ -0,0 +1,95 @@ +using System.Linq; +using NUnit.Framework; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.IE; +using OpenQA.Selenium.PhantomJS; +using OpenQA.Selenium.Safari; +using OpenQA.Selenium.Edge; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; +using OpenQA.Selenium.Remote; + +namespace Golem.Tests +{ + internal class BrowserTests : TestBase + { + // [OneTimeSetUp] + // public void Init() + // { + // Config.settings.runTimeSettings.LaunchBrowser = false; + // } + // + // [OneTimeTearDown] + // public void Teardown() + // { + // Config.settings.runTimeSettings.LaunchBrowser = true; + // } + // + // [Test] + // public void TestIE() + // { + // driver = new InternetExplorerDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + // + // [Test] + // public void TestFF() + // { + // driver = new FirefoxDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + // + // [Test] + // public void TestChrome() + // { + // driver = new ChromeDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + // + // [Test] + // public void TestPhantomJS() + // { + // driver = new PhantomJSDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + // + // [Test] + // public void TestSafari() + // { + // driver = new SafariDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + // + // [Test] + // public void TestEdge() + // { + // driver = new EdgeDriver(); + // OpenPage("http://www.google.com/"); + // driver.Quit(); + // } + + [Test] + public void TestRemote() + { + var browserInfo = new BrowserInfo(WebDriverBrowser.Browser.Chrome); + + var count = Config.settings.runTimeSettings.Browsers.Count(); + if (count > 0) + { + browserInfo = Config.settings.runTimeSettings.Browsers.First(); + } + + var driver = new WebDriverBrowser().LaunchRemoteBrowser(browserInfo.browser, + "127.0.0.1"); + driver.Navigate().GoToUrl("http://www.google.com/"); + driver.Quit(); + } + } +} \ No newline at end of file diff --git a/Tests/ConfigTests.cs b/Tests/ConfigTests.cs new file mode 100644 index 0000000..f43bce5 --- /dev/null +++ b/Tests/ConfigTests.cs @@ -0,0 +1,42 @@ +using NUnit.Framework; +using Golem.Core; + +namespace Golem.Tests +{ + public class ConfigTests : TestBase + { + [SetUp] + public void init() + { + Config.settings.runTimeSettings.ElementTimeoutSec = 27; + } + [Test] + public void TestCustomConfigSettings() + { + var value = Config.GetConfigValue("CustomSetting", "default"); + Assert.AreEqual(value, "WasFound"); + } + + [Test] + public void TestConfigSettingsAreModifiyable() + { + var timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + Config.settings.runTimeSettings.ElementTimeoutSec = 1234; + Assert.AreEqual(Config.settings.runTimeSettings.ElementTimeoutSec, 1234); + Config.settings.runTimeSettings.ElementTimeoutSec = timeout; + } + + [Test] + public void TestConfigFileDefaults() + { + var value = Config.GetConfigValue("Testsdflksjdflsdkj", "-1"); + Assert.AreEqual(value, "-1"); + } + + [Test] + public void TestInit() + { + Assert.AreEqual(Config.settings.runTimeSettings.ElementTimeoutSec, 27); + } + } +} \ No newline at end of file diff --git a/Tests/Data/Data.csv b/Tests/Data/Data.csv new file mode 100644 index 0000000..11e5f71 --- /dev/null +++ b/Tests/Data/Data.csv @@ -0,0 +1,3 @@ +searchTerm, searchResult +Selenium, Selenium - Web Browser Automation +ProtoTest, ProtoTest \ No newline at end of file diff --git a/Tests/Data/SearchData.xml b/Tests/Data/SearchData.xml new file mode 100644 index 0000000..77feb6b --- /dev/null +++ b/Tests/Data/SearchData.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Tests/DesiredCapibilitiesTests.cs b/Tests/DesiredCapibilitiesTests.cs new file mode 100644 index 0000000..d60b7a8 --- /dev/null +++ b/Tests/DesiredCapibilitiesTests.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class DesiredCapibilitiesTests : WebDriverTestBase + { + [SetUp] + public override void SetUp() + { + var data = testData; + Config.settings.sauceLabsSettings.SauceLabsUsername = "bkitchener"; + Config.settings.sauceLabsSettings.SauceLabsAPIKey = "998969ff-ad37-4b2e-9ad7-edacd982bc59"; + Config.settings.sauceLabsSettings.UseSauceLabs = true; + Config.settings.runTimeSettings.RunOnRemoteHost = true; + Config.settings.runTimeSettings.HighlightFoundElements = false; + browserInfo = new BrowserInfo(WebDriverBrowser.Browser.Firefox); + base.SetUp(); + } + + [Test] + [Parallelizable] + public void TestSauceLabs() + { + var newData = testData; + driver.Navigate().GoToUrl("http://www.espn.com"); + var element = new Element("ESPN Element", By.XPath("//*[@class='espn-logo']//*[text()='ESPN']")); + element.WaitUntil(10).Visible(); + Log.Message("Successfully navigated to " + driver.Title); + } + } +} diff --git a/Tests/ElementTests.cs b/Tests/ElementTests.cs new file mode 100644 index 0000000..4e84522 --- /dev/null +++ b/Tests/ElementTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class ElementTests : WebDriverTestBase + { + [Test] + public void TestNoName() + { + var element = new Element(By.Id("Id")); + Assert.AreEqual(element.name, "Element"); + } + + [Test] + public void TestWithName() + { + var element = new Element("NameOfElement", By.Id("Id")); + Assert.AreEqual(element.name, "NameOfElement"); + } + + [Test] + public void TestDSL() + { + driver.Navigate().GoToUrl("http://www.google.com/"); + var element = new Element("NameOfElement", By.Name("q")); + element.SetText("ProtoTest"); + element.Verify().Value("ProtoTest").Submit(); + element.WaitUntil(20).Visible().Text = "Golem"; + element.Verify(20).Value("Golem"); + } + + [Test] + public void TestNotPresent() + { + driver.Navigate().GoToUrl("http://www.google.com/"); + var SearchField = new Element("SearchField", By.Name("q")); + var element = new Element("NotPresentElement", By.Name("z")); + SearchField.WaitUntil().Present(); + element.Verify().Not().Present(); + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/Google/GmailPage.cs b/Tests/PageObjects/Google/GmailPage.cs new file mode 100644 index 0000000..b8f775c --- /dev/null +++ b/Tests/PageObjects/Google/GmailPage.cs @@ -0,0 +1,18 @@ +using Golem.WebDriver; + +namespace Golem.Tests.PageObjects.Google +{ + public class GmailPage : BasePageObject + { + + public override void WaitForElements() + { +// EmailField.Verify().Visible(); + } + + public void LogIn(string user, string pass) + { + + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/Google/GoogleHomePage.cs b/Tests/PageObjects/Google/GoogleHomePage.cs new file mode 100644 index 0000000..f7c3739 --- /dev/null +++ b/Tests/PageObjects/Google/GoogleHomePage.cs @@ -0,0 +1,46 @@ +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Tests.PageObjects.Google +{ + public class GoogleHomePage : BasePageObject + { + private readonly Element feelingLuckyButton = new Element("ImFeelingLuckyButton", By.Name("btnI")); + private readonly Element gmailbutton = new Element("GmailButton", By.ClassName("gbts")); + private readonly Element googleLogo = new Element("GoogleLogo", By.Id("hplogo")); + private readonly Element searchButton = new Element("SearchButton", By.Name("btnK")); + private readonly Element searchField = new Element("SearchField", By.Name("q")); + private readonly Element signInButton = new Element("SignInButon", By.LinkText("Sign in")); + + public GmailPage GoToGmail() + { + gmailbutton.Click(); + return new GmailPage(); + } + + public GoogleResultsPage SearchFor(string text) + { + searchField.Text = text; + searchField.Submit(); + return new GoogleResultsPage(); + } + + public override void WaitForElements() + { + searchField.Verify().Present(); + googleLogo.Verify().Present(); + searchButton.Verify().Present(); + feelingLuckyButton.Verify().Present(); + signInButton.Verify().Present(); + } + + public void VerifyImages() + { + searchField.Verify().Image(); + googleLogo.Verify().Image(); + searchButton.Verify().Image(); + feelingLuckyButton.Verify().Image(); + signInButton.Verify().Image(); + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/Google/GoogleHomePage2.cs b/Tests/PageObjects/Google/GoogleHomePage2.cs new file mode 100644 index 0000000..238ffcd --- /dev/null +++ b/Tests/PageObjects/Google/GoogleHomePage2.cs @@ -0,0 +1,39 @@ +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Tests.PageObjects.Google +{ + public class GoogleHomePage2 : BasePageObject + { + private readonly By feelingLuckyButton = By.Name("btnI"); + private readonly By gmailbutton = By.ClassName("gbts"); + private readonly By googleLogo = By.Id("hplogo"); + private readonly By searchButton = By.Name("btnK"); + private readonly By searchField = By.Name("q"); + private readonly By signInButton = By.ClassName("gbit"); + + public GmailPage GoToGmail() + { + driver.FindElement(gmailbutton).Click(); + return new GmailPage(); + } + + public GoogleResultsPage SearchFor(string text) + { + var SearchField = driver.FindElement(searchField); + SearchField.Clear(); + SearchField.SendKeys(text); + SearchField.Submit(); + return new GoogleResultsPage(); + } + + public override void WaitForElements() + { + driver.WaitForPresent(searchField); + driver.WaitForPresent(searchButton); + driver.WaitForPresent(googleLogo); + driver.WaitForPresent(feelingLuckyButton); + driver.WaitForPresent(signInButton); + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/Google/GoogleResultsPage.cs b/Tests/PageObjects/Google/GoogleResultsPage.cs new file mode 100644 index 0000000..0cd3912 --- /dev/null +++ b/Tests/PageObjects/Google/GoogleResultsPage.cs @@ -0,0 +1,93 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using OpenQA.Selenium; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests.PageObjects.Google +{ + public class GoogleResultsPage : BasePageObject + { + public class SearchResultItem : BaseComponent + { + public Element Link => new Element(this, By.CssSelector("h3.r>a")); + + public Element Url => new Element(this, By.CssSelector("cite")); + + public Element Description => new Element(this, By.CssSelector("span.st")); + } + + public class GoogleResultsHeader : BaseComponent + { + public Element Logo => new Element(this, By.Id("logo")); + public Element Search => new Element(this, By.CssSelector("input")); + + public GoogleResultsHeader(By by) : base(by) + { + } + } + + private readonly Element Gmailbutton = new Element("GmailButton", By.PartialLinkText("Gmail")); + private readonly Element SearchButton = new Element("SearchButton", By.Name("btnG")); + private readonly Element SearchField = new Element("SearchField", By.Name("q")); + private readonly Element SignInButton = new Element("SignInButton", By.LinkText("Sign in")); + private Element GoogleLogo = new Element("GoogleLogo", By.XPath("//a[@title='Go to Google Home']")); + private Element searchResult; + + public Components SearchItem = new Components(By.CssSelector("div.g")); + public GoogleResultsHeader Header = new GoogleResultsHeader(By.Id("tsf")); + + public void clicktest(string text) + { + var component = SearchItem.First(x => x.Text.Contains(text)); + component.Description.Verify() + .Text("Selenium is a portable software testing framework for web applications"); + + var second = SearchItem.First(x => x.Text.Contains("GitHub")); + second.Description.Verify().Not().Text("wikipedia"); + + Header.Search.Verify().Visible(); + } + + public Element SearchResult(string text) + { + searchResult = new Element("SearchResultLink", By.PartialLinkText(text)); + return searchResult; + } + + public GoogleResultsPage SearchFor(string text) + { + SearchField.SetText(text); + SearchField.Submit(); + return new GoogleResultsPage(); + } + + public GoogleResultsPage VerifyResult(string text) + { + SearchResult(text).Verify().Present(); + return new GoogleResultsPage(); + } + + public SearchResultPage GoToResult(string text) + { + SearchResult(text).Click(); + return new SearchResultPage(); + } + + public GmailPage GoToGmail() + { + Gmailbutton.Click(); + return new GmailPage(); + } + + public override void WaitForElements() + { + SearchField.Verify().Present(); + //GoogleLogo.Verify().Present(); + SearchButton.Verify().Present(); + SignInButton.Verify().Present(); + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/Google/SearchResultPage.cs b/Tests/PageObjects/Google/SearchResultPage.cs new file mode 100644 index 0000000..3f1aba5 --- /dev/null +++ b/Tests/PageObjects/Google/SearchResultPage.cs @@ -0,0 +1,11 @@ +using Golem.WebDriver; + +namespace Golem.Tests.PageObjects.Google +{ + public class SearchResultPage : BasePageObject + { + public override void WaitForElements() + { + } + } +} \ No newline at end of file diff --git a/Tests/PageObjects/MSPaint/MSPaint_6.cs b/Tests/PageObjects/MSPaint/MSPaint_6.cs new file mode 100644 index 0000000..fa27761 --- /dev/null +++ b/Tests/PageObjects/MSPaint/MSPaint_6.cs @@ -0,0 +1,27 @@ +using System.Windows.Forms; +using Golem.Purple; +using Golem.Purple.PurpleElements; + +namespace Golem.Tests.PageObjects.MSPaint +{ + public class MSPaint_6 : BaseScreenObject + { + private readonly PurplePanel PaintArea = new PurplePanel("PaintArea", "Untitled - Paint/!BLANK!/!BLANK!"); + + private readonly PurpleButton TextButton = new PurpleButton("MSPaintTextButton", + "Untitled - Paint/Ribbon/Ribbon/!BLANK!/Ribbon/Lower Ribbon/!BLANK!/Home/Tools/Text"); + + public void PaintText(string text) + { + TextButton.Click(); + PaintArea.MoveCursor(PaintArea.Bounds.TopLeft); + PaintArea.LMB_Down(); + SendKeys.SendWait(text); + } + + public static MSPaint_6 PaintWindow() + { + return new MSPaint_6(); + } + } +} \ No newline at end of file diff --git a/Tests/Parallel/TestOne.cs b/Tests/Parallel/TestOne.cs new file mode 100644 index 0000000..cb46467 --- /dev/null +++ b/Tests/Parallel/TestOne.cs @@ -0,0 +1,33 @@ +using System.Configuration; +using NUnit.Framework; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.IE; +using OpenQA.Selenium.PhantomJS; +using OpenQA.Selenium.Safari; +using OpenQA.Selenium.Edge; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [Parallelizable] + internal class ParallelTestOne : WebDriverTestBase + { + + [Test] + public void TestOne() + { + driver.Navigate().GoToUrl("http://www.google.com"); + //Assert.Fail("Failing"); + } + + [Test] + public void TestTwo() + { + driver.Navigate().GoToUrl("http://www.gmail.com"); + } + + } +} \ No newline at end of file diff --git a/Tests/Parallel/TestThree.cs b/Tests/Parallel/TestThree.cs new file mode 100644 index 0000000..46c5e19 --- /dev/null +++ b/Tests/Parallel/TestThree.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.IE; +using OpenQA.Selenium.PhantomJS; +using OpenQA.Selenium.Safari; +using OpenQA.Selenium.Edge; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [Parallelizable] + internal class ParallelTestThree : WebDriverTestBase + { + + [Test] + public void TestOne() + { + driver.Navigate().GoToUrl("http://www.google.com"); + } + + [Test] + public void TestTwo() + { + driver.Navigate().GoToUrl("http://www.gmail.com"); + } + + } +} \ No newline at end of file diff --git a/Tests/Parallel/TestTwo.cs b/Tests/Parallel/TestTwo.cs new file mode 100644 index 0000000..04a19d8 --- /dev/null +++ b/Tests/Parallel/TestTwo.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.IE; +using OpenQA.Selenium.PhantomJS; +using OpenQA.Selenium.Safari; +using OpenQA.Selenium.Edge; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [Parallelizable] + internal class ParallelTestTwo : WebDriverTestBase + { + + [Test] + public void TestOne() + { + driver.Navigate().GoToUrl("http://www.google.com"); + } + + [Test] + public void TestTwo() + { + driver.Navigate().GoToUrl("http://www.gmail.com"); + } + + } +} \ No newline at end of file diff --git a/Tests/SauceLabsTests.cs b/Tests/SauceLabsTests.cs new file mode 100644 index 0000000..c156974 --- /dev/null +++ b/Tests/SauceLabsTests.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [Parallelizable] + internal class SauceLabsTests : WebDriverTestBase + { + [SetUp] + public override void SetUp() + { + Config.settings.sauceLabsSettings.SauceLabsUsername = "bkitchener"; + Config.settings.sauceLabsSettings.SauceLabsAPIKey = "998969ff-ad37-4b2e-9ad7-edacd982bc59"; + Config.settings.sauceLabsSettings.UseSauceLabs = true; + Config.settings.runTimeSettings.RunOnRemoteHost = true; + Config.settings.runTimeSettings.HighlightFoundElements = false; + this.browser = WebDriverBrowser.Browser.IE; + base.SetUp(); + } + + [Test] + [Parallelizable] + public void TestSauceLabs() + { + driver.Navigate().GoToUrl("http://www.espn.com"); + var element = new Element("ESPN Element", By.XPath("//*[@class='espn-logo']//*[text()='ESPN']")); + element.WaitUntil(10).Visible(); + Log.Message("Successfully navigated to " + driver.Title); + } + } +} \ No newline at end of file diff --git a/Tests/TestBasePageObject.cs b/Tests/TestBasePageObject.cs new file mode 100644 index 0000000..e52d277 --- /dev/null +++ b/Tests/TestBasePageObject.cs @@ -0,0 +1,23 @@ +using NUnit.Framework; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + public class TestBasePageObject : WebDriverTestBase + { + [Test] + public void TestWaitForElements() + { + var page = OpenPage("http://www.google.com"); + } + + [Test] + public void TestPageObjectHasDriver() + { + OpenPage("http://www.google.com"); + var page = new GoogleHomePage(); + Assert.IsNotNull(page.driver); + } + } +} \ No newline at end of file diff --git a/Tests/TestBaseTests.cs b/Tests/TestBaseTests.cs new file mode 100644 index 0000000..f45bc2d --- /dev/null +++ b/Tests/TestBaseTests.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; +using Golem.Core; + +namespace Golem.Tests +{ + internal class TestBaseTests : TestBase + { + [Test] + public void TestDataContainerCreated() + { + Assert.IsNotNull(testData); + Assert.IsNotNull(testData.VerificationErrors); + Assert.IsNotNull(testData.actions); + } + } +} \ No newline at end of file diff --git a/Tests/TestComponent.cs b/Tests/TestComponent.cs new file mode 100644 index 0000000..2e372e6 --- /dev/null +++ b/Tests/TestComponent.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + class TestComponent : WebDriverTestBase + { + [Test] + public void TestBaseComponentActsLikeElement() + { + var item = + OpenPage("https://www.google.com/#q=selenium") + .SearchItem.First(x => x.Text.Contains("wikipedia")); + item.Highlight(-1, "grey"); + item.Link.Verify().Visible(); + item.Description.Verify().Visible(); + item.Url.Verify().Visible(); + } + + [Test] + public void TestComponents() + { + var item = + OpenPage("https://www.google.com/#q=selenium") + .SearchItem.First(x => x.Text.Contains("wikipedia")); + item.Description.Verify().Text("Selenium is a portable software testing framework for web applications"); + } + + [Test] + public void TestHeader() + { + var item = + OpenPage("https://www.google.com/#q=selenium").Header.Search.Verify().Visible(); + } + + } +} diff --git a/Tests/TestDDT.cs b/Tests/TestDDT.cs new file mode 100644 index 0000000..441a8bb --- /dev/null +++ b/Tests/TestDDT.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class TestDDT : WebDriverTestBase + { + // [XmlData("//Search", FilePath = ".\\Tests\\Data\\SearchData.xml")] + // [Test] + // public void TestXml([Bind("@term")] string search, [Bind("@result")] string result) + // { + // Config.settings.runTimeSettings.FindHiddenElements = true; + // OpenPage("http://www.google.com/").SearchFor(search).VerifyResult(result); + // } + // + // [CsvData(FilePath = ".\\Tests\\Data\\Data.csv", HasHeader = true)] + // [Test] + // public void TestCSV(string term, string result) + // { + // OpenPage("http://www.google.com/").SearchFor(term).VerifyResult(result); + // } + [TestCase("Selenium", "Selenium - Web Browser Automation")] + [TestCase("Selenium", "Selenium (software) - Wikipedia, the free encyclopedia")] + public void TestCase(string term, string result) + { + OpenPage("http://www.google.com/").SearchFor(term).VerifyResult(result); + } + + [Test] + public void TestValues( + [Values("Selenium")] string term, + [Values("zSelenium - Web Browser Automation", "Selenium (software) - Wikipedia, the free encyclopedia")] string result) + { + OpenPage("http://www.google.com/").SearchFor(term).VerifyResult(result); + } + + +// public IEnumerable GetDataEnumerable() +// { +// yield return new Search {term = "Selenium", result = "Selenium - Web Browser Automation"}; +// yield return new Search {term = "ProtoTest", result = "ProtoTest"}; +// } +// + public static List GetDataList() + { + var searches = new List(); + searches.Add(new Search {term = "WebDriver", result = "Selenium WebDriver"}); + searches.Add(new Search {term = "Appium", result = "Appium: Mobile App Automation Made Awesome."}); + return searches; + } + + [Test, TestCaseSource("GetDataList")] + public void TestSource(Search search) + { + OpenPage("http://www.google.com/").SearchFor(search.term).VerifyResult(search.result); + } + + public class Search + { + public string result; + public string term; + } + } +} \ No newline at end of file diff --git a/Tests/TestDriver.cs b/Tests/TestDriver.cs new file mode 100644 index 0000000..326df91 --- /dev/null +++ b/Tests/TestDriver.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class TestDriver : WebDriverTestBase + { + [Test] + public void TestWebElementExtensions() + { + driver.Navigate().GoToUrl("http://google.com"); + driver.WaitForPresent(By.Name("q"), 20).Highlight(); + driver.WaitForNotVisible(By.Name("zas")); + } + + [Test] + public void TestWebDriverExtensions() + { + driver.Navigate().GoToUrl("http://google.com"); + driver.WaitForElementWithText("Advertising").Highlight(); + driver.FindElementWithText("Advertising").Highlight(); + } + } +} \ No newline at end of file diff --git a/Tests/TestElements.cs b/Tests/TestElements.cs new file mode 100644 index 0000000..9e206e3 --- /dev/null +++ b/Tests/TestElements.cs @@ -0,0 +1,45 @@ +using System.Linq; +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class TestElements : WebDriverTestBase + { + [Test] + public void TestElementAPI() + { + driver.Navigate().GoToUrl("http://www.google.com"); + var elements = new Element(driver.FindElements(By.Name("q"))); + elements.First().Click(); + } + + [Test] + public void TestLinq() + { + driver.Navigate().GoToUrl("http://www.google.com"); + var elements = new Element(driver.FindElements(By.XPath("//*"))); + elements.First(x => x.GetAttribute("name") == "q" && x.Displayed).Click(); + } + + [Test] + public void testCount() + { + driver.Navigate().GoToUrl("http://www.google.com"); + var elements = new Element(driver.FindElements(By.Name("q"))); + driver.Navigate().GoToUrl("http://www.google.com"); + Assert.AreEqual(1, elements.Where(x => x.IsStale()).Count()); + } + + [Test] + public void TestBy() + { + var elements = new Element(By.Name("q")); + driver.Navigate().GoToUrl("http://www.google.com"); + elements.ForEach(x => x.Click()); + driver.Navigate().GoToUrl("http://www.google.com"); + elements.ForEach(x => x.SendKeys("test")); + } + } +} \ No newline at end of file diff --git a/Tests/TestImageComparison.cs b/Tests/TestImageComparison.cs new file mode 100644 index 0000000..c37bbcd --- /dev/null +++ b/Tests/TestImageComparison.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [TestFixture] + public class TestImageComparison : WebDriverTestBase + { + [Test] + public static void TestImages() + { + OpenPage("http://www.google.com/").VerifyImages(); + } + } +} \ No newline at end of file diff --git a/Tests/TestLog.cs b/Tests/TestLog.cs new file mode 100644 index 0000000..7156e43 --- /dev/null +++ b/Tests/TestLog.cs @@ -0,0 +1,36 @@ +using System.Drawing.Imaging; +using System.IO; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.Extensions; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class TestLog : WebDriverTestBase + { + [Test] + public void TestLogFile() + { + Log.Message("This is a test"); + } + + [Test] + public void TestLogImage() + { + driver.Navigate().GoToUrl("http://www.google.com"); + + Log.Image(driver.GetScreenshot()); + } + + + [Test] + public void TestLogVideo() + { + driver.Navigate().GoToUrl("http://www.google.com"); + + Log.Video(testData.recorder.Video); + } + } +} \ No newline at end of file diff --git a/Tests/TestMSPaint6.cs b/Tests/TestMSPaint6.cs new file mode 100644 index 0000000..53ee0ed --- /dev/null +++ b/Tests/TestMSPaint6.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using Golem.Core; +using Golem.Purple; +using Golem.Tests.PageObjects.MSPaint; + +namespace Golem.Tests +{ + public class TestMSPaint6 : PurpleTestBase + { + [OneTimeSetUp] + public void setup() + { + Config.settings.purpleSettings.Purple_windowTitle = "Untitled - Paint"; + Config.settings.purpleSettings.ProcessName = "Paint"; + Config.settings.purpleSettings.Purple_blankValue = "!BLANK!"; + Config.settings.purpleSettings.Purple_Delimiter = "/"; + Config.settings.purpleSettings.appPath = "%windir%\\system32\\mspaint.exe"; + Config.settings.purpleSettings.Purple_ElementTimeoutWaitSeconds = 30; + } + + [Test] + public void PaintExample() + { + MSPaint_6.PaintWindow().PaintText("ProtoTest Purple"); + } + } +} \ No newline at end of file diff --git a/Tests/TestMultiThreading.cs b/Tests/TestMultiThreading.cs new file mode 100644 index 0000000..25b417a --- /dev/null +++ b/Tests/TestMultiThreading.cs @@ -0,0 +1,41 @@ +using NUnit.Framework; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [Parallelizable] + internal class TestMultiThreading : WebDriverTestBase + { + [Test] + public void TestThreadedRepeat1() + { + OpenPage("http://www.google.com"); + } + + [Test, Parallelizable] + public void TestThreadedRepeat2() + { + OpenPage("http://www.google.com"); + } + + [Test, Parallelizable] + public void TestThreadedRepeat3() + { + OpenPage("http://www.google.com"); + } + + [Test, Parallelizable] + public void TestThreadedRepea4t() + { + OpenPage("http://www.google.com"); + } + + [Test, Parallelizable] + public void TestThreadedRepeat5() + { + OpenPage("http://www.google.com"); + } + } +} \ No newline at end of file diff --git a/Tests/TestPageObjects.cs b/Tests/TestPageObjects.cs new file mode 100644 index 0000000..b9b3eea --- /dev/null +++ b/Tests/TestPageObjects.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + [TestFixture] + internal class TestPageObjects : WebDriverTestBase + { + [Test] + public void TestPageObject1() + { + OpenPage("http://www.google.com/"). + SearchFor("Something"). + VerifyResult("Something"); + } + [Test] + public void TestPageObject2() + { + OpenPage("http://www.google.com/"). + SearchFor("ProtoTest"). + VerifyResult("ProtoTest"); + } + [Test] + public void TestPageObject3() + { + OpenPage("http://www.google.com/"). + SearchFor("Selenium"). + VerifyResult("Web Browser Automation"); + } + } +} \ No newline at end of file diff --git a/Tests/TestProxy.cs b/Tests/TestProxy.cs new file mode 100644 index 0000000..4622f3b --- /dev/null +++ b/Tests/TestProxy.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using NUnit.Framework.Internal.Commands; +using Golem.Core; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class TestProxy : WebDriverTestBase + { + [Test] + public void TestProxyNotNull() + { + Assert.IsNotNull(proxy); + } + + [Test] + public void TestProxyWorks() + { + OpenPage("http://www.google.com/"); + } + + [Test] + public void TestProxyFilter() + { + OpenPage("http://www.google.com/"); + var entries = proxy.FilterEntries("www.google.com"); + Assert.AreEqual(10, entries.Count); + Assert.AreEqual("http://www.google.com/", entries[0].Request.Url); + } + + [Test] + public void TestHTTPValidation() + { + OpenPage("http://www.google.com/"); + proxy.VerifyRequestMade("http://www.google.com/"); + } + + [Test] + public void TestResponseCodeValidation() + { + OpenPage("http://www.google.com/"); + proxy.VerifyNoErrorsCodes(); + } + } +} \ No newline at end of file diff --git a/Tests/TestSource.cs b/Tests/TestSource.cs new file mode 100644 index 0000000..a82bb27 --- /dev/null +++ b/Tests/TestSource.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests +{ + + [TestFixtureSource("GetName")] + class TestSource + { + private string name; + + public TestSource(string name) + { + this.name = name; + } + + public TestSource() + { + } + + public static IEnumerable GetName() + { + yield return "one"; + yield return "two"; + } + + [Test] + public void testone() + { + TestContext.WriteLine("Pass"); + } + } +} diff --git a/Tests/TestWebDriverTestBase.cs b/Tests/TestWebDriverTestBase.cs new file mode 100644 index 0000000..0111c16 --- /dev/null +++ b/Tests/TestWebDriverTestBase.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using Golem.Tests.PageObjects.Google; +using Golem.WebDriver; +//using Castle.Core.Logging; + +namespace Golem.Tests +{ + internal class TestWebDriverTestBase : WebDriverTestBase + { + [Test] + public void TestPageObjectCreater() + { + var page = OpenPage("http://www.google.com"); + Assert.IsInstanceOf(page); + } + + [Test] + public void TestDriverNotNull() + { + Assert.IsNotNull(driver); + } + + [Test] + public void TestDefaultBrowser() + { + Assert.AreEqual(browser, WebDriverBrowser.Browser.Chrome); + } + + [Test] + public void TestEventFiringDriverLaunches() + { + Assert.IsInstanceOf(driver); + } + } +} \ No newline at end of file diff --git a/Tests/VerificationTests.cs b/Tests/VerificationTests.cs new file mode 100644 index 0000000..002e1d1 --- /dev/null +++ b/Tests/VerificationTests.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Golem.Core; +using Golem.WebDriver; + +namespace Golem.Tests +{ + internal class VerificationTests : TestBase + { + [Test] + public void TestVerificationCount() + { + Assert.AreEqual(testData.VerificationErrors.Count, 0); + AddVerificationError("Test error"); + Assert.AreEqual(testData.VerificationErrors.Count, 1); + testData.VerificationErrors = new List(); + } + + [Test] + public void TestAssertionCount() + { + var result = TestContext.CurrentContext.Result; +// Assert.AreEqual(TestContext.CurrentContext.Result.PassCount, 0); +// AddVerificationError("Test Error"); +// Assert.AreEqual(TestContext.CurrentContext.Test., 2); + testData.VerificationErrors = new List(); + } + } +} \ No newline at end of file diff --git a/WebDriver/BaseComponent.cs b/WebDriver/BaseComponent.cs new file mode 100644 index 0000000..b943c79 --- /dev/null +++ b/WebDriver/BaseComponent.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using OpenQA.Selenium; +using Golem.Core; + +namespace Golem.WebDriver +{ + public abstract class BaseComponent : Element + { + public Element root { get; set; } + + public BaseComponent() + { + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public BaseComponent(By by) + { + this.root = new Element(by); + this.@by = by; + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public BaseComponent(Element element) + { + this.root = element; + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public BaseComponent(By by, Frame frame) + { + this.root = new Element(by, frame); + this.frame = frame; + this.@by = by; + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public BaseComponent(BaseComponent component, By by, Frame frame) + { + this.root = new Element(component, by, frame); + this.frame = frame; + this.@by = by; + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public BaseComponent(BaseComponent component, By by) + { + this.root = new Element(component, by); + this.@by = by; + this.name = TestBase.GetCurrentClassName(); + this.WaitForElements(); + } + + public override IWebElement GetElement() + { + IWebElement element2; + if (this.root == null) + { + element2 = base.GetElement(); + } + else + { + element2 = this.root.GetElement(); + } + + return element2; + + } + + /// + /// Create an element verification for some condition. + /// + /// A new ElementVerification for the element + public ElementVerification Verify() + { + timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new ElementVerification(this, timeoutSec, false); + } + + /// + /// Wait for some condition on the element + /// + /// A new ElementVerification for the element + public ElementVerification WaitUntil(int timeoutSec) + { + this.timeoutSec = timeoutSec; + return new ElementVerification(this, this.timeoutSec, true); + } + + public virtual void WaitForElements() + { + + } + + } +} \ No newline at end of file diff --git a/WebDriver/BasePageObject.cs b/WebDriver/BasePageObject.cs new file mode 100644 index 0000000..4379750 --- /dev/null +++ b/WebDriver/BasePageObject.cs @@ -0,0 +1,95 @@ +using System; +using System.Reflection; +using OpenQA.Selenium; +using Golem.Core; + +namespace Golem.WebDriver +{ + /// + /// BasePageObject should be inherited by all page objects used in the framework. It represents either a base + /// component or page in an application. You must implement void WaitForElements(), which should contain checks for + /// ajax elements being present/not present. It contains a static reference to the WebDriverTestBase.driver object + /// + public abstract class BasePageObject + { + public string className; + public string currentMethod; + public string url; + + public BasePageObject() + { + driver = WebDriverTestBase.driver; + className = GetType().Name; + if (Config.settings.runTimeSettings.AutoWaitForElements) + { + try + { + WaitForElements(); + } + catch (Exception e) + { + throw new WebDriverException(string.Format("The {0} Page failed to load : " + e.Message, className)); + } + } + TestBase.testData.actions.addAction(TestBase.GetCurrentClassAndMethodName()); + } + + public BasePageObject(IWebDriver driver) + { + this.driver = driver; + className = GetType().Name; + if (Config.settings.runTimeSettings.AutoWaitForElements) + { + WaitForElements(); + } + TestBase.testData.actions.addAction(TestBase.GetCurrentClassAndMethodName()); + } + + private void GetElementNAmes() + { +// var props = this.GetType().GetProperties(); +// foreach (var prop in props) +// { +// if(prop.PropertyType.Name=="Element") +// { +// var value = prop.GetValue(this, null) as Element; +// value.Set_Name(prop.Name); +//// prop.SetValue(this, value, null); +// Log.Message("Set name to " + value.name); +// } +// +// } +// props = this.GetType().GetProperties(); +// foreach (var prop in props) +// { +// if (prop.PropertyType.Name == "Element") +// { +// var value = prop.GetValue(this, null) as Element; +// Log.Message("Set name to " + value.name); +// } + +// } + // // Get the type of FieldsClass. + // Type fieldsType = this.GetType(); + // + // // Get an array of FieldInfo objects. + // FieldInfo[] fields = fieldsType.GetFields(BindingFlags.Public + // | BindingFlags.Instance); + // // Display the values of the fields. + // Console.WriteLine("Displaying the values of the fields of {0}:", + // fieldsType); + // for (int i = 0; i < fields.Length; i++) + // { + // var text = string.Format(" {0}:\t'{1}'", + // fields[i].Name, fields[i].GetValue(this)); + // + // } + } + + public IWebDriver driver { get; set; } + + public virtual void WaitForElements() + { + } + } +} \ No newline at end of file diff --git a/WebDriver/ByE.cs b/WebDriver/ByE.cs new file mode 100644 index 0000000..78123c5 --- /dev/null +++ b/WebDriver/ByE.cs @@ -0,0 +1,37 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver +{ + /// + /// Contains By Extensions for the OpenQA.Selenium.By class + /// + public class ByE + { + public static By Text(string text) + { + return By.XPath("//*[text()='" + text + "']"); + } + + public static By PartialText(string text) + { + return By.XPath("//*[contains(text(),'" + text + "')]"); + } + + public static By PartialAttribute(string tag, string attribute, string value) + { + string attr; + + // put a '@' in front of the attribute if the user did not + if (!attribute.StartsWith("@")) + { + attr = string.Format("@{0}", attribute); + } + else + { + attr = attribute; + } + + return By.XPath(string.Format("//{0}[contains({1},'{2}')]", tag, attr, value)); + } + } +} \ No newline at end of file diff --git a/WebDriver/Components.cs b/WebDriver/Components.cs new file mode 100644 index 0000000..a19888b --- /dev/null +++ b/WebDriver/Components.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenQA.Selenium; +using Golem.Core; + +namespace Golem.WebDriver +{ + public class Components : IEnumerable where T : BaseComponent, new() + { + private Element root; + + public Components() + { + } + + public Components(By by) + { + this.root = new Element(by); + } + + public Components(By by, Frame frame) + { + this.root = new Element(by, frame); + } + + public IEnumerator GetEnumerator() + { + foreach (var ele in this.root) + { + var el = new T(); + el.root = ele; + yield return el; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Create an element verification for some condition. + /// + /// A new ElementVerification for the element + public ElementVerification Verify() + { + var timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new ElementVerification(this.root, timeoutSec, false); + } + + /// + /// Wait for some condition on the element + /// + /// A new ElementVerification for the element + public ElementVerification WaitUntil() + { + var timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new ElementVerification(this.root, timeoutSec, true); + } + + /// + /// Create an element verification for some condition. + /// + /// A new ElementVerification for the element + public ElementVerification Verify(int timeoutSec) + { + return new ElementVerification(this.root, timeoutSec, false); + } + + /// + /// Wait for some condition on the element + /// + /// A new ElementVerification for the element + public ElementVerification WaitUntil(int timeoutSec) + { + return new ElementVerification(this.root, timeoutSec, true); + } + } +} diff --git a/WebDriver/Element.cs b/WebDriver/Element.cs new file mode 100644 index 0000000..143abe5 --- /dev/null +++ b/WebDriver/Element.cs @@ -0,0 +1,925 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Internal; +using OpenQA.Selenium.Support.UI; +using Golem.Core; +using Golem.WebDriver.Elements.Images; + +namespace Golem.WebDriver +{ + /// + /// Provides a simplified API to the IWebELement. Can be instantiated in a class header. + /// Will automatically find the IWebElement each time it is used, not when it is instantiated. + /// + public class Element : IWrapsDriver, IWrapsElement, IEnumerable + { + protected IWebElement _element; + protected IEnumerable _elements; + protected internal Element root; + protected internal Frame frame; + protected ElementImages _images; + public By by; + public string name; + public string pageObjectName = ""; + public int timeoutSec; + private StackFrame[] stackFrames; + public void GetName() + { + this.pageObjectName = TestBase.GetCurrentClassName(); + var stackTrace = new StackTrace(); // get call stack + stackFrames = stackTrace.GetFrames(); // get method calls (frames) + var t = this.GetType(); + + foreach (var stackFrame in stackFrames) + { + var method = stackFrame.GetMethod(); + Type type = method.ReflectedType; + if (method.Name.Contains("get_")) + { + this.name = $"{ this.pageObjectName}." + method.Name.Replace("get_", "").Replace("()", ""); + if (name.Contains("ClickOrdersLink")) + { + foreach(var stack in stackFrames) + { + Log.Message(stack.GetMethod().Name); + } + } + return; + } + + if ((type.IsSubclassOf(typeof(Element)) && + (!method.IsConstructor))) + { + this.name = $"{ this.pageObjectName}." + method.Name; + if (name.Contains("ClickOrdersLick")) + { + foreach (var stack in stackFrames) + { + Log.Message(stack.GetMethod().Name); + } + } + return; + } + + if ((type.IsSubclassOf(typeof(BaseComponent)))) + { + this.name = $"{ this.pageObjectName}." + type.Name; + if (name.Contains("ClickOrdersLink")) + { + foreach (var stack in stackFrames) + { + Log.Message(stack.GetMethod().Name); + } + } + return; + } + } + } + + public Element() + { + GetName(); + pageObjectName = TestBase.GetCurrentClassName(); + timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + public Element(ReadOnlyCollection elements) + { + GetName(); + var eles = new List(); + foreach (var ele in elements) + { + eles.Add(new Element(ele)); + } + this.elements = eles; + } + + /// + /// Construct an element using an existing element + /// + /// + public Element(IWebElement element) + { + GetName(); + this.element = element; + + pageObjectName = TestBase.GetCurrentClassName(); + timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + /// + /// Construct an element using an existing element + /// + /// + public Element(IWebElement element, By by) + { + GetName(); + this.element = element; + this.by = by; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + /// + /// Construct an element + /// + /// Human readable name of the element + /// By locator + public Element(string name, By locator) + { + GetName(); + this.name = name; + this.by = locator; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + /// + /// Construct an element + /// + /// By locator + public Element(By locator) + { + GetName(); + this.by = locator; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + /// + /// Construct an element within an iframe + /// + /// Human readable name of the element + /// By locator + public Element(string name, By locator, Frame frame) + { + this.frame = frame; + this.name = name; + this.by = locator; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + + + /// + /// Construct an element + /// + /// By locator + public Element(By locator, Frame frame) + { + GetName(); + this.frame = frame; + this.by = locator; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + public Element(BaseComponent root, By locator, Frame frame=null) + { + GetName(); + this.root = root; + this.frame = frame; + this.by = locator; + this.pageObjectName = TestBase.GetCurrentClassName(); + this.timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + } + + + protected IEnumerable elements + { + get + { + if (@by != null) + { + if (frame != null) + { + Log.Message($"Looking in frame {frame.name} {frame.GetHtml()}"); + WebDriverTestBase.driver.SwitchTo().Frame(frame.WrappedElement); + } + else + { + Log.Message("Looking in default frame"); + WebDriverTestBase.driver.SwitchTo().DefaultContent(); + } + + var newList = new List(); + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking for Elements {this.pageObjectName}.{this.name} ({this.@by})"); + var eles = root != null ? root.FindElements(@by) : driver.FindElements(@by); + foreach (var ele in eles) + { + var nele = new Element(ele); + newList.Add(nele); + } + _elements = newList; + } + + return _elements; + } + set { _elements = value; } + } + + public IEnumerator GetEnumerator() + { + foreach (var ele in elements) + { + yield return ele; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void ForEach(Action action) + { + this.ToList().ForEach(action); + } + + + protected IWebDriver driver + { + get { return TestBase.testData.driver; } + set { TestBase.testData.driver = value; } + } + + public ElementImages Images + { + get { return _images ?? (_images = new ElementImages(this)); } + } + + protected IWebElement element + { + get + { + _element = GetElement(); + return _element; + } + set { _element = value; } + } + + /// + /// Is the element present on the page, but not necesarily displayed and visible? + /// + public bool Present + { + get + { + try + { + return element.Enabled; + } + catch (NoSuchElementException e) + { + return false; + } + catch (StaleElementReferenceException e) + { + return false; + } + catch (InvalidOperationException e) + { + return false; + } + } + } + + /// + /// Is the element present and displayed on the page? + /// + public bool Displayed + { + get + { + try + { + if (!Present) return false; + return element.Displayed; + } + catch (NoSuchElementException e) + { + return false; + } + catch (StaleElementReferenceException e) + { + return false; + } + } + } + + /// + /// Is the element present on the page and able to be interacted with? + /// + public bool Enabled + { + get + { + if (!Present) return false; + return element.Enabled; + } + } + + /// + /// Get the upper-left (x,y) coordinates of the element relative to the upper-left corner of the page. + /// + public Point Location + { + get { return element.Location; } + } + + public Frame Frame + { + get + { + return this.frame; + } + set { this.frame = value; } + } + + /// + /// Checks if the element is selected on the page. + /// + public bool Selected + { + get + { + if (!Present) return false; + return element.Selected; + } + } + + /// + /// Return an object containing the size of the element (height, width). + /// + public Size Size + { + get { return element.Size; } + } + + /// + /// Return the tag name of the element. + /// + public string TagName + { + get { return element.TagName; } + } + + /// + /// Property to get and set the Text for the element. + /// + public string Text + { + get + { + var text = element.Text; + return text; + } + set + { + element.Clear(); + element.SendKeys(value); + } + } + + /// + /// Returns the first element found by the locator. + /// + /// The locator to use. + /// The IWebElement found. + public IWebElement FindElement(By by) + { + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + try + { + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking for Child ({by}) of Element {this.name} ({this.@by})"); + var eles = element.FindElements(by); + if (eles.Count > 0) + return eles[0]; + Common.Delay(1000); + } + catch (StaleElementReferenceException e) + { + } + } + throw new NoSuchElementException( + $"Child ({by}) of Element {this.name} ({this.@by}) was not present after {timeoutSec} seconds"); + } + + /// + /// Return a collection of elements found by the locator. + /// + /// The locator to use. + /// Collection of IWebElements found. + public ReadOnlyCollection FindElements(By by) + { + + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking for Children ({by}) of Element {this.name} ({this.@by})"); + + var childelements = element.FindElements(by); + return childelements; + } + + /// + /// Clears the contents of the element\\. + /// + public Element Clear() + { + try + { + element.Clear(); + return this; + } + catch (Exception e) + { + Log.Warning("Could not clear " + e.Message); + return this; + } + + } + + /// + /// Click the element and optionally highlights the element if set in the application configuration settings. + /// + public Element Click() + { + try + { + element.Click(); + } + catch (InvalidOperationException e) + { + element.ScrollIntoView().Click(); + } + return this; + } + + /// + /// Submit this element to the web server and optionally highlights the element if set in the application configuration + /// settings. + /// + public Element Submit() + { + element.Submit(); + return this; + } + + /// + /// Simulates typing text into the element and optionally highlights the element if set in the application + /// configuration settings. + /// + /// Text to send + public Element SendKeys(string text) + { + try + { + element.SendKeys(text); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException("Cannot sendKeys that element, please verify it is an input or text field" + e.Message); + } + + return this; + } + + /// + /// Get the value of the requested attribute for the element + /// + /// The attribute name + /// + public string GetAttribute(string attribute) + { + try + { + return element.GetAttribute(attribute); + } + catch (Exception) + { + return ""; + } + } + + /// + /// Get the value of a CSS property for the element + /// + /// The CSS property name + /// + public string GetCssValue(string propertyName) + { + return element.GetCssValue(propertyName); + } + + public IWebDriver WrappedDriver + { + get { return driver; } + private set { driver = value; } + } + + public IWebElement WrappedElement + { + get { return element; } + private set { element = value; } + } + + public bool IsPresent(int timeoutSec) + { + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + try + { + var eles = driver.FindElements(by); + if (eles.Count > 0) + return true; + Common.Delay(1000); + } + catch (StaleElementReferenceException e) + { + } + } + return false; + } + + public bool IsDisplayed(int timeoutSec) + { + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + try + { + var eles = driver.FindElements(by).ToList(); + if (eles.Any(ele => ele.Displayed)) + { + return true; + } + Common.Delay(1000); + } + catch (StaleElementReferenceException e) + { + } + catch (InvalidOperationException e) + { + + } + } + return false; + } + + public Element SelectOption(string option) + { + new SelectElement(this.GetElement()).SelectByText(option); + return this; + } + + public Element SelectOptionByPartialText(string text) + { + var s_element = new SelectElement(this.GetElement()); + + foreach (var option in s_element.Options.Where(option => option.Text.Contains(text))) + { + option.Click(); + break; + } + + return this; + } + + + /// + /// Returns the first element found by the locator. + /// + /// The locator to use. + /// The IWebElement found. + public IWebElement FindElement(Element childElement) + { + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + try + { + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking for Child {childElement.name} ({childElement.@by}) of Element {this.name} ({this.@by})"); + var eles = element.FindElements(childElement.by); + if (eles.Count > 0) + return eles[0]; + Common.Delay(1000); + } + catch (StaleElementReferenceException e) + { + } + } + throw new NoSuchElementException($"Child Element {childElement.name} ({childElement.@by}) of {this.name} ({this.@by}) was not present after {timeoutSec} seconds"); + } + + public ReadOnlyCollection FindElements(Element element) + { + return FindElements(element.by); + } + + /// + /// Create an element verification for some condition. + /// + /// A new ElementVerification for the element + public ElementVerification Verify() + { + timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new ElementVerification(this, timeoutSec, false); + } + + /// + /// Wait for some condition on the element + /// + /// A new ElementVerification for the element + public ElementVerification WaitUntil(int timeoutSec) + { + this.timeoutSec = timeoutSec; + return new ElementVerification(this, this.timeoutSec, true); + } + + /// + /// Create an element verification for some condition + /// + /// + /// timeout that overrides the default timeout set in the configuration settings class or + /// App.config file + /// + /// A new ElementVerification for the element + public ElementVerification Verify(int timeoutSec) + { + this.timeoutSec = timeoutSec; + return new ElementVerification(this, this.timeoutSec, false); + } + + /// + /// Wait for some condition on the element + /// + /// + /// Optional timeout that overrides the default timeout set in the configuration settings class or + /// App.config file + /// + /// A new ElementVerification for the element + public ElementVerification WaitUntil() + { + timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + return new ElementVerification(this, timeoutSec, true); + } + + public Element Delay(int ms) + { + Common.Delay(ms); + return this; + } + + public virtual IWebElement GetElement() + { +// try +// { + if (_element.IsStale()) + { + + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking for Element {this.name} {this.@by}"); + if (root != null) + { + var root_ele = root.GetElement(); + if (frame != null) + { + frame.GetElement(); + Log.Message( + $"{TestBase.GetCurrentClassAndMethodName()}: Looking in frame : {frame.@by}: {frame.GetHtml()}"); + driver.SwitchTo().Frame(frame.GetElement()); + } + _element = root_ele.WaitForPresent(@by, timeoutSec); + } + else + { + if (frame != null) + { + frame.GetElement(); + Log.Message( + $"{TestBase.GetCurrentClassAndMethodName()}: Looking in frame : {frame.@by}: {frame.GetHtml()}"); + driver.SwitchTo().Frame(frame.GetElement()); + } + else + { + Log.Message($"{TestBase.GetCurrentClassAndMethodName()}: Looking in default frame"); + driver.SwitchTo().DefaultContent(); + } + _element = driver.WaitForPresent(@by, timeoutSec); + } + } + return _element; +// } +// catch (NoSuchElementException e) +// { +// var message = $"Could not find element '{name}' ({@by}) after {timeoutSec} seconds {GetFrameMessage()}"; +// throw new NoSuchElementException(message); +// } + } + + private string GetFrameMessage() + { + if (frame != null) + { + return " In frame " + frame.name; + } + return ""; + } + + /// + /// Clear a checked element (radio or checkbox) + /// + public Element ClearChecked() + { + element.ClearChecked(); + return this; + } + + /// + /// Highlight the element on the page + /// + public Element Highlight(int ms = 30, string color = "yellow") + { + element.Highlight(ms, color); + return this; + } + + /// + /// Set the checkbox element + /// + /// if true, check it; if false, uncheck it + /// The element reference + public Element SetCheckbox(bool isChecked) + { + if (element.Selected != isChecked) + { + element.Click(); + } + return this; + } + + public bool IsStale() + { + return element.IsStale(); + } + + public Element SetText(string value) + { + Clear(); + SendKeys(value); + var element_value = this.GetAttribute("value"); + if (value != element_value) + { + Clear(); + Thread.Sleep(1000); + SendKeys(value); + } + return this; + } + + public string GetHtml() + { + return element.GetHtml(); + } + + /// + /// If there are multiple elements that can be found using the same locator, + /// find one that is displayed and enabled. + /// + /// The element found + public Element GetVisibleElement() + { + element = driver.FindVisibleElement(@by); + return this; + } + + /// + /// Move the mouse over the element + /// + public Element MouseOver() + { + element.MouseOver(); + return this; + } + + public Element ScrollIntoView() + { + element.ScrollIntoView(); + return this; + } + + /// + /// WithParam swaps out {0} in the locator with the value entered + /// This allows us to adjust for params with specific strings + /// + /// + /// + public Element WithParam(string value) + { + if (by == null) + throw new Exception("WithParams only works with Elements instantiated using a By locator"); + var oldBy = by.ToString(); + var toks = oldBy.Split(':'); + var type = toks[0]; + var locator = toks[1]; + var newlocator = locator.Replace("{0}", value); + if (type.Contains("ClassName")) + { + by = By.ClassName(newlocator); + } + if (type.Contains("XPath")) + { + by = By.XPath(newlocator); + } + if (type.Contains("Id")) + { + by = By.Id(newlocator); + } + if (type.Contains("PartialLink")) + { + by = By.PartialLinkText(newlocator); + } + if (type.Contains("LinkText")) + { + by = By.LinkText(newlocator); + } + if (type.Contains("Name")) + { + by = By.Name(newlocator); + } + if (type.Contains("CssSelector")) + { + by = By.CssSelector(newlocator); + } + if (type.Contains("TagName")) + { + by = By.TagName(newlocator); + } + return this; + } + + /// + /// Swaps out {0},{1},{2}..etc with the values in values array + /// //div[contains(text(),'{0}) and contains(@class,'{1})] + /// with values[] = {"textOfElement","classofElement"} becomes + /// //div[contains(text(),'textOfElement') and contains(@class,'classOfElement)] + /// will not work if element was instantiated with an existing IWebELement instead of a By locator. + /// + /// + /// + public Element WithParams(string[] values) + { + if (by == null) + throw new Exception("WithParams only works with Elements instantiated using a By locator"); + var oldBy = by.ToString(); + var toks = oldBy.Split(':'); + var type = toks[0]; + var locator = toks[1]; + for (var i = 0; i < values.Length; i++) + { + locator = locator.Replace("{" + i + "}", values[i]); + } + if (type.Contains("ClassName")) + { + by = By.ClassName(locator); + } + if (type.Contains("XPath")) + { + by = By.XPath(locator); + } + if (type.Contains("Id")) + { + by = By.Id(locator); + } + if (type.Contains("PartialLink")) + { + by = By.PartialLinkText(locator); + } + if (type.Contains("LinkText")) + { + by = By.LinkText(locator); + } + if (type.Contains("Name")) + { + by = By.Name(locator); + } + if (type.Contains("CssSelector")) + { + by = By.CssSelector(locator); + } + if (type.Contains("TagName")) + { + by = By.TagName(locator); + } + return this; + } + } +} \ No newline at end of file diff --git a/WebDriver/ElementVerification.cs b/WebDriver/ElementVerification.cs new file mode 100644 index 0000000..bea4567 --- /dev/null +++ b/WebDriver/ElementVerification.cs @@ -0,0 +1,345 @@ +using System; +using System.Drawing; +using System.Linq; +using NUnit.Framework; +using OpenQA.Selenium; +using Golem.Core; + +namespace Golem.WebDriver +{ + /// + /// Methods for performing non-terminating validations, and Wait commands. + /// + public class ElementVerification + { + private const string errorMessage = "{0}: {1}({2}): {3} after {4} seconds"; + private readonly Element element; + private readonly bool failTest; + private readonly bool isTrue = true; + private bool condition; + private string message; + private string notMessage; + private int timeoutSec; + + public ElementVerification(Element element, int timeoutSec, bool failTest = false, bool isTrue = true) + { + this.element = element; + this.timeoutSec = timeoutSec; + this.failTest = failTest; + this.isTrue = isTrue; + } + + public ElementVerification Not() + { + element.timeoutSec = 1; + if (element.root != null) + { + element.root.timeoutSec = 1; + } + if (element.frame != null) + { + element.frame.timeoutSec = 1; + } + return new ElementVerification(element, timeoutSec, failTest, false); + } + + public ElementVerification Not(int timeoutSec) + { + element.timeoutSec = 1; + if (element.root != null) + { + element.root.timeoutSec = 1; + } + if (element.frame != null) + { + element.frame.timeoutSec = 1; + } + this.timeoutSec = timeoutSec; + return new ElementVerification(element, timeoutSec, failTest, false); + } + + private void VerificationFailed(string message = "", Image image = null) + { + if (message == "") message = GetErrorMessage(); + if (failTest) + { + Assert.Fail(message); + } + else + { + if (image == null) + { + TestBase.AddVerificationError(message); + } + else + { + TestBase.AddVerificationError(message, image); + } + } + } + + private string GetErrorMessage() + { + string newMessage; + newMessage = isTrue ? notMessage : message; + return string.Format(errorMessage, TestBase.GetCurrentClassAndMethodName(), element.name, element.by, + newMessage, timeoutSec); + } + + private string GetSuccessMessage() + { + string newMessage; + var correctMessage = "{0}: {1}({2}): {3}"; + newMessage = isTrue ? message : notMessage; + return string.Format(correctMessage, TestBase.GetCurrentClassAndMethodName(), element.name, element.by, + newMessage); + } + + public Element ChildElement(By bylocator) + { + message = "is found"; + notMessage = "not found"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = element.FindElements(bylocator).Count > 0; + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + VerificationFailed(); + return element; + } + + public Element Present() + { + message = "is present"; + notMessage = "not present"; + + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = element.Present; + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + VerificationFailed(); + return element; + } + + public Element Visible() + { + message = "is visible"; + notMessage = "not visible"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = element.Displayed; + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + VerificationFailed(); + return element; + } + + public Element Count(int value) + { + message = "count not '" + value + "'"; + notMessage = "count was not '" + value + "'"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var newText = element.Text; + condition = (element.Present) && (element.Count() == value); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". It was : '" + element.Count() + "'"; + VerificationFailed(); + return element; + } + + public Element CountGreaterThan(int value) + { + message = "count not greater than '" + value + "'"; + notMessage = "count was not less than '" + value + "'"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var newText = element.Text; + condition = (element.Present) && (element.Count() > value); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". It was : '" + element.Count() + "'"; + VerificationFailed(); + return element; + } + + public Element CountLessThan(int value) + { + message = "count not less than '" + value + "'"; + notMessage = "count was not greater than '" + value + "'"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var newText = element.Text; + condition = (element.Present) && (element.Count() < value); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". It was : '" + element.Count() + "'"; + VerificationFailed(); + return element; + } + + public Element Text(string text) + { + message = "contains text '" + text + "'"; + notMessage = "doesn't contain text '" + text + "'"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var newText = element.Text; + condition = (element.Present) && (element.Text.Contains(text)); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". Actual : " + element.Text; + VerificationFailed(); + return element; + } + + public Element Value(string text) + { + message = "has value " + text; + notMessage = "doesn't have value " + text; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = (element.Present) && (element.GetAttribute("value").Contains(text)); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". Actual : " + element.GetAttribute("value"); + VerificationFailed(); + return element; + } + + public Element Attribute(string attribute, string value) + { + message = "has attribute " + attribute + " with value " + value; + notMessage = "doesn't have attribute " + attribute + " with value " + value; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = (element.Present) && (element.GetAttribute(attribute).Contains(value)); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". Actual : " + element.GetAttribute(attribute); + VerificationFailed(); + return element; + } + + public Element CSS(string attribute, string value) + { + message = "has css " + attribute + " with value " + value; + notMessage = "doesn't have css " + attribute + " with value " + value; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = (element.Present) && (element.GetAttribute(attribute).Contains(value)); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + notMessage += ". Actual : " + element.GetCssValue(attribute); + VerificationFailed(); + return element; + } + + public Element Selected() + { + message = "is selected"; + notMessage = "isn't selected"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = (element.Present) && (element.Selected); + if (condition == isTrue) + { + Log.Message("!--Verification Passed " + GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + VerificationFailed(); + return element; + } + + public Element Image() + { + message = "image matches"; + notMessage = "image is {0} different"; + var then = DateTime.Now.AddSeconds(timeoutSec); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + condition = (element.Present) && (element.Images.ImagesMatch()); + if (condition == isTrue) + { +// TestContext.CurrentContext.IncrementAssertCount(); + Log.Message(GetSuccessMessage()); + return element; + } + Common.Delay(1000); + } + + notMessage = string.Format(notMessage, element.Images.differenceString); + VerificationFailed( + string.Format("{0}: {1}({2}): {3}", TestBase.GetCurrentClassAndMethodName(), element.name, + element.by, + notMessage), element.Images.GetMergedImage()); + + return element; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Images/ElementImages.cs b/WebDriver/Elements/Images/ElementImages.cs new file mode 100644 index 0000000..802d825 --- /dev/null +++ b/WebDriver/Elements/Images/ElementImages.cs @@ -0,0 +1,188 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using Golem.Core; + +namespace Golem.WebDriver.Elements.Images +{ + public class ElementImages + { + public static bool UpdateImages = Config.settings.imageCompareSettings.updateImages; + private readonly Image liveImage; + private readonly Image storedImage; + public float difference; + public string differenceString; + public Element element; + + public ElementImages(Element element) + { + this.element = element; + CreateDirectory(); + liveImage = GetImage(); + storedImage = GetStoredImage(); + } + + private string ImageLocation + { + get + { + return AppDomain.CurrentDomain.BaseDirectory.Replace(@"\bin\Debug", "").Replace(@"\bin\Release", "") + + "\\ElementImages\\" + element.pageObjectName + "_" + + element.name.Replace(" ", "") + ".bmp"; + } + } + + public Image GetDifferenceImage() + { + var bmp = new Bitmap(liveImage.Width, liveImage.Height); + + return liveImage.GetDifferenceOverlayImage(storedImage) + .Resize(storedImage.Width, storedImage.Height); + } + + public bool ImagesMatch() + { + if ((!File.Exists(ImageLocation)) || (UpdateImages)) + { + UpdateImage(); + } + difference = ImageComparer.ImageComparePercentage(storedImage, liveImage, + Config.settings.imageCompareSettings.fuzziness); + differenceString = (difference*100).ToString("0.##\\%"); + + return difference < Config.settings.imageCompareSettings.accuracy; + } + + public Image GetMergedImage() + { + var overlayImage = OverlayImages(liveImage, GetDifferenceImage()); + var mergedImage = CombineImages(storedImage, liveImage, overlayImage); + + return mergedImage; + } + + private Image CombineImages(Image image1, Image image2, Image image3) + { + var newWidth = image1.Width + image2.Width + image3.Width; + var newHeight = image1.Height; + var bmp = new Bitmap(newWidth, newHeight); + using (var gr = Graphics.FromImage(bmp)) + { + gr.DrawImage(image1, new Point(0, 0)); + gr.DrawImage(image2, new Point(image1.Width, 0)); + gr.DrawImage(image3, new Point(image2.Width + image1.Width, 0)); + } + + return bmp; + } + + public Image GetStoredImage() + { + if (File.Exists(ImageLocation)) + { + return Image.FromFile(ImageLocation); + } + + return GetImage(); + } + + public Image OverlayImages(Image imageBackground, Image imageOverlay) + { + imageOverlay = imageOverlay.Resize(imageBackground.Width, imageBackground.Height); + Image img = new Bitmap(imageBackground.Width, imageBackground.Height); + using (var gr = Graphics.FromImage(img)) + { + gr.DrawImage(imageBackground, new Point(0, 0)); + gr.DrawImage(imageOverlay, new Point(0, 0)); + } + + return img; + } + + public void CreateDirectory() + { + if (!Directory.Exists(Common.GetCodeDirectory() + "\\ElementImages")) + { + Directory.CreateDirectory(Common.GetCodeDirectory() + "\\ElementImages"); + } + } + + public void DeleteOldImage() + { + if (File.Exists(ImageLocation)) + { + File.Delete(ImageLocation); + } + } + + public void UpdateImage() + { + using (var image = GetImage()) + { + SaveImage(image); + } + } + + private void SaveImage(Image image) + { + try + { + DeleteOldImage(); + using (var tempImage = new Bitmap(image)) + { + tempImage.Save(ImageLocation, ImageFormat.Bmp); + } + } + catch (Exception e) + { + Log.Message("Exception saving image : " + e.Message); + } + } + + public Image GetImage() + { + var size = new Size(element.Size.Width, element.Size.Height); + if (element.Displayed == false) + { + throw new BadImageFormatException(string.Format( + "Could not create image for element {0} as it is hidden", element.name)); + } + var cropRect = new Rectangle(element.Location, size); + var screenShot = TestBase.testData.driver.GetScreenshot(); + + // Trim the crop to not extend off the screenshot, preventing OutOfMemoryException. + if (cropRect.X < 0) + { + cropRect.X = 0; + } + if (cropRect.Y < 0) + { + cropRect.Y = 0; + } + if (cropRect.X + cropRect.Width > screenShot.Width) + { + cropRect.Width = screenShot.Width - cropRect.X; + } + if (cropRect.Y + cropRect.Height > screenShot.Height) + { + cropRect.Height = screenShot.Height - cropRect.Y; + } + + return cropImage(screenShot, cropRect); + } + + private Image cropImage(Image img, Rectangle cropArea) + { + var bmpImage = new Bitmap(img); + var bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat); + + return bmpCrop; + } + + public void AttachImage() + { + Log.Image(GetImage()); + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Images/Histogram.cs b/WebDriver/Elements/Images/Histogram.cs new file mode 100644 index 0000000..b91911c --- /dev/null +++ b/WebDriver/Elements/Images/Histogram.cs @@ -0,0 +1,154 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace Golem.WebDriver.Elements.Images +{ + /// + /// A class which facilitates working with RGB histograms + /// It encapsulates a Bitmap and lets you get information about the Bitmap + /// + public class Histogram + { + private static readonly Pen[] p = {Pens.Red, Pens.Green, Pens.Blue}; + + public Histogram(Bitmap bitmap) + { + Bitmap = bitmap; + Red = new byte[256]; + Green = new byte[256]; + Blue = new byte[256]; + CalculateHistogram(); + } + + /// + /// Constructs a new Histogram from a file, given its path + /// + /// The path to the image to work with + public Histogram(string filePath) : this((Bitmap) Image.FromFile(filePath)) + { + } + + /// + /// The red values in an image + /// + public byte[] Red { get; private set; } + + /// + /// The green values in an image + /// + public byte[] Green { get; private set; } + + /// + /// The blue values in an image + /// + public byte[] Blue { get; private set; } + + /// + /// The bitmap to get histogram info for + /// + public Bitmap Bitmap { get; private set; } + + /// + /// Calculates the values in the histogram + /// + private void CalculateHistogram() + { + var newBmp = (Bitmap) Bitmap.Resize(16, 16); + Color c; + for (var x = 0; x < newBmp.Width; x++) + { + for (var y = 0; y < newBmp.Height; y++) + { + c = newBmp.GetPixel(x, y); + Red[c.R]++; + Green[c.G]++; + Blue[c.B]++; + } + } + } + + /// + /// Gets a bitmap with the RGB histograms + /// + /// Three histograms for R, G and B values in the Histogram + public Bitmap Visualize() + { + var oneColorHeight = 100; + var margin = 10; + + float[] maxValues = {Red.Max(), Green.Max(), Blue.Max()}; + byte[][] values = {Red, Green, Blue}; + + + var histogramBitmap = new Bitmap(276, oneColorHeight*3 + margin*4); + var g = Graphics.FromImage(histogramBitmap); + g.FillRectangle(Brushes.White, 0, 0, histogramBitmap.Width, histogramBitmap.Height); + var yOffset = margin + oneColorHeight; + + for (var i = 0; i < 256; i++) + { + for (var color = 0; color < 3; color++) + { + g.DrawLine(p[color], margin + i, yOffset*(color + 1), margin + i, + yOffset*(color + 1) - (values[color][i]/maxValues[color])*oneColorHeight); + } + } + + for (var i = 0; i < 3; i++) + { + g.DrawString(p[i].Color.ToKnownColor() + ", max value: " + maxValues[i], SystemFonts.SmallCaptionFont, + Brushes.Silver, margin + 11, yOffset*i + margin + margin + 1); + g.DrawString(p[i].Color.ToKnownColor() + ", max value: " + maxValues[i], SystemFonts.SmallCaptionFont, + Brushes.Black, margin + 10, yOffset*i + margin + margin); + g.DrawRectangle(p[i], margin, yOffset*i + margin, 256, oneColorHeight); + } + g.Dispose(); + + return histogramBitmap; + } + + /// + /// Gives a human-readable representation of the RGB values in the histogram + /// + /// a human-readable representation of the RGB values in the histogram + public override string ToString() + { + var sb = new StringBuilder(); + + for (var i = 0; i < 256; i++) + { + sb.Append(string.Format("RGB {0,3} : ", i) + + string.Format("({0,3},{1,3},{2,3})", Red[i], Green[i], Blue[i])); + sb.AppendLine(); + } + + return sb.ToString(); + } + + /// + /// Gets the variance between two histograms (http://en.wikipedia.org/wiki/Variance) as a percentage of the maximum + /// possible variance: 256 (for a white image compared to a black image) + /// + /// the histogram to compare this one to + /// A percentage which tells how different the two histograms are + public float GetVariance(Histogram histogram) + { + // + double diffRed = 0, diffGreen = 0, diffBlue = 0; + for (var i = 0; i < 256; i++) + { + diffRed += Math.Pow(Red[i] - histogram.Red[i], 2); + diffGreen += Math.Pow(Green[i] - histogram.Green[i], 2); + diffBlue += Math.Pow(Blue[i] - histogram.Blue[i], 2); + } + + diffRed /= 256; + diffGreen /= 256; + diffBlue /= 256; + const double maxDiff = 512; + return (float) (diffRed/maxDiff + diffGreen/maxDiff + diffBlue/maxDiff)/3; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Images/ImageComparer.cs b/WebDriver/Elements/Images/ImageComparer.cs new file mode 100644 index 0000000..4b58a87 --- /dev/null +++ b/WebDriver/Elements/Images/ImageComparer.cs @@ -0,0 +1,193 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using Golem.Core; + +namespace Golem.WebDriver.Elements.Images +{ + public class ImageComparer + { + public static bool ImageCompareString(Image firstImage, Image secondImage, float failurePercent) + { + var ms1 = new MemoryStream(); + var ms2 = new MemoryStream(); + + firstImage.Save(ms1, ImageFormat.Bmp); + var firstBitmap = Convert.ToBase64String(ms1.ToArray()); + + secondImage.Save(ms2, ImageFormat.Bmp); + var secondBitmap = Convert.ToBase64String(ms2.ToArray()); + + ms1.Dispose(); + ms2.Dispose(); + + var total = 0; + for (var i = 0; i < firstBitmap.Length; i++) + { + if (firstBitmap[i] != secondBitmap[i]) + { + total++; + } + } + var failure = total/(float) firstBitmap.Length; + return failure < failurePercent; + } + + public static bool ImageCompareArray(Image firstImage, Image secondImage) + { + var images_match = false; + string firstPixel; + string secondPixel; + firstImage = Common.ScaleImage(firstImage); + secondImage = Common.ScaleImage(secondImage); + + if (firstImage.Size.Width > secondImage.Size.Width) + { + firstImage = Common.ResizeImage(firstImage, secondImage.Size.Width, secondImage.Size.Height); + } + if (secondImage.Size.Width > firstImage.Size.Width) + { + secondImage = Common.ResizeImage(secondImage, firstImage.Size.Width, firstImage.Size.Height); + } + + var firstBmp = new Bitmap(firstImage); + var secondBmp = new Bitmap(secondImage); + + if (firstBmp.Width == secondBmp.Width + && firstBmp.Height == secondBmp.Height) + { + for (var i = 0; i < firstBmp.Width; i++) + { + for (var j = 0; j < firstBmp.Height; j++) + { + firstPixel = firstBmp.GetPixel(i, j).ToString(); + secondPixel = secondBmp.GetPixel(i, j).ToString(); + if (firstPixel != secondPixel) + { + // pixels do not match, bail... + return false; + } + } + } + + // all pixels match + images_match = true; + } + + return images_match; + } + + public static float ImageComparePercentage(Image firstImage, Image secondImage, byte fuzzyness = 10) + { + return firstImage.PercentageDifference(secondImage, fuzzyness); + } + + public static float ImageCompareAverageHash(Image firstImage, Image secondImage) + { + var diff = 0; + var first = GetHash(firstImage).ToCharArray(); + var second = GetHash(secondImage).ToCharArray(); + for (var i = 0; i < first.Length; i++) + { + if (first[i] != second[i]) + { + diff++; + } + } + Log.Message("The Hamming Distance is " + diff); + Log.Message("The Hamming Difference is " + ((float) diff/first.Length)); + + return ((float) diff/first.Length); + } + + public static string GetHash(Image image) + { + var hashString = ""; + var newImage = new Bitmap(image.Resize(8, 8).GetGrayScaleVersion()); + + var average = GetAverageColor(newImage).ToArgb(); + for (var x = 0; x < newImage.Width; x++) + { + for (var y = 0; y < newImage.Height; y++) + { + if (newImage.GetPixel(x, y).ToArgb() < average) + { + hashString += "0"; + } + else + { + hashString += "1"; + } + } + } + + return hashString; + } + + public static Image GetHashImage(Image image) + { + var newImage = new Bitmap(image.Resize(8, 8).GetGrayScaleVersion()); + + var average = GetAverageColor(newImage).ToArgb(); + var bmp = new Bitmap(8, 8); + for (var x = 0; x < bmp.Width; x++) + { + for (var y = 0; y < bmp.Height; y++) + { + if (newImage.GetPixel(x, y).ToArgb() < average) + { + bmp.SetPixel(x, y, Color.Black); + } + else + { + bmp.SetPixel(x, y, Color.White); + } + } + } + + return bmp; + } + + public static long GetHashCodeInt64(string input) + { + var s1 = input.Substring(0, input.Length/2); + var s2 = input.Substring(input.Length/2); + + var x = ((long) s1.GetHashCode()) << 0x20 | s2.GetHashCode(); + + return x; + } + + public static Color GetAverageColor(Bitmap bmp) + { + //Used for tally + var r = 0; + var g = 0; + var b = 0; + + var total = 0; + + for (var x = 0; x < bmp.Width; x++) + { + for (var y = 0; y < bmp.Height; y++) + { + var clr = bmp.GetPixel(x, y); + + r += clr.R; + g += clr.G; + b += clr.B; + + total++; + } + } + + //Calculate average + r /= total; + g /= total; + b /= total; + + return Color.FromArgb(r, g, b); + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Images/ImageExtensions.cs b/WebDriver/Elements/Images/ImageExtensions.cs new file mode 100644 index 0000000..37e6876 --- /dev/null +++ b/WebDriver/Elements/Images/ImageExtensions.cs @@ -0,0 +1,331 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; + +// Created in 2012 by Jakob Krarup (www.xnafan.net). +// Use, alter and redistribute this code freely, +// but please leave this comment :) + +namespace Golem.WebDriver.Elements.Images +{ + /// + /// A class with extensionmethods for comparing images + /// + public static class ImageTool + { + //the font to use for the DifferenceImages + private static readonly Font DefaultFont = new Font("Arial", 8); + //the brushes to use for the DifferenceImages + private static readonly Brush[] brushes = new Brush[256]; + //the colormatrix needed to grayscale an image + //http://www.switchonthecode.com/tutorials/csharp-tutorial-convert-a-color-image-to-grayscale + private static readonly ColorMatrix ColorMatrix = new ColorMatrix(new[] + { + new[] {.3f, .3f, .3f, 0, 0}, + new[] {.59f, .59f, .59f, 0, 0}, + new[] {.11f, .11f, .11f, 0, 0}, + new float[] {0, 0, 0, 1, 0}, + new float[] {0, 0, 0, 0, 1} + }); + + //Create the brushes in varying intensities + static ImageTool() + { + for (var i = 0; i < 256; i++) + { + brushes[i] = new SolidBrush(Color.FromArgb(i/2, 255, 2, 2)); + } + } + + /// + /// Gets the difference between two images as a percentage + /// + /// The first image + /// The image to compare to + /// How big a difference (out of 255) will be ignored - the default is 3. + /// The difference between the two images as a percentage + public static float PercentageDifference(this Image img1, Image img2, byte threshold = 3) + { + var differences = img1.GetDifferences(img2); + + var diffPixels = 0; + + foreach (byte b in differences) + { + if (b > threshold) + { + diffPixels++; + } + } + + return diffPixels/256f; + } + + /// + /// Gets an image which displays the differences between two images + /// + /// The first image + /// The image to compare with + /// + /// Whether to adjust the color indicating maximum difference (usually 255) to the maximum difference found in this + /// case. + /// E.g. if the maximum difference found is 12, then a true value in adjustColorSchemeToMaxDifferenceFound would result + /// in 0 being black, 6 being dark pink, and 12 being bright pink. + /// A false value would still have differences of 255 as bright pink resulting in the 12 difference still being very + /// dark. + /// + /// Whether to write percentages in each of the 255 squares (true) or the absolute value (false) + /// an image which displays the differences between two images + public static Bitmap GetDifferenceImage(this Image img1, Image img2, + bool adjustColorSchemeToMaxDifferenceFound = false, bool absoluteText = false) + { + //create a 16x16 tiles image with information about how much the two images differ + var cellsize = 16; //each tile is 16 pixels wide and high + var bmp = new Bitmap(16*cellsize + 1, 16*cellsize + 1); + //16 blocks * 16 pixels + a borderpixel at left/bottom + + var g = Graphics.FromImage(bmp); + //g.FillRectangle(Brushes, 0, 0, bmp.Width, bmp.Height); + byte[,] differences = img1.GetDifferences(img2); + byte maxDifference = 255; + + //if wanted - adjust the color scheme, by finding the new maximum difference + if (adjustColorSchemeToMaxDifferenceFound) + { + maxDifference = 0; + foreach (var b in differences) + { + if (b > maxDifference) + { + maxDifference = b; + } + } + + if (maxDifference == 0) + { + maxDifference = 1; + } + } + + for (var y = 0; y < differences.GetLength(1); y++) + { + for (var x = 0; x < differences.GetLength(0); x++) + { + var cellValue = differences[x, y]; + string cellText = null; + + if (absoluteText) + { + cellText = cellValue.ToString(); + } + else + { + cellText = string.Format("{0}%", (int) cellValue); + } + + var percentageDifference = (float) differences[x, y]/maxDifference; + var colorIndex = (int) (255*percentageDifference); + + g.FillRectangle(brushes[colorIndex], x*cellsize, y*cellsize, cellsize, cellsize); + g.DrawRectangle(Pens.Blue, x*cellsize, y*cellsize, cellsize, cellsize); + var size = g.MeasureString(cellText, DefaultFont); + g.DrawString(cellText, DefaultFont, Brushes.Black, x*cellsize + cellsize/2 - size.Width/2 + 1, + y*cellsize + cellsize/2 - size.Height/2 + 1); + g.DrawString(cellText, DefaultFont, Brushes.White, x*cellsize + cellsize/2 - size.Width/2, + y*cellsize + cellsize/2 - size.Height/2); + } + } + + return bmp; + } + + /// + /// Gets an image which displays the differences between two images + /// + /// The first image + /// The image to compare with + /// + /// Whether to adjust the color indicating maximum difference (usually 255) to the maximum difference found in this + /// case. + /// E.g. if the maximum difference found is 12, then a true value in adjustColorSchemeToMaxDifferenceFound would result + /// in 0 being black, 6 being dark pink, and 12 being bright pink. + /// A false value would still have differences of 255 as bright pink resulting in the 12 difference still being very + /// dark. + /// + /// Whether to write percentages in each of the 255 squares (true) or the absolute value (false) + /// an image which displays the differences between two images + public static Bitmap GetDifferenceOverlayImage(this Image img1, Image img2) + { + var bmp = new Bitmap(16*16 + 1, 16*16 + 1); + var g = Graphics.FromImage(bmp); + byte[,] differences = img1.GetDifferences(img2); + byte maxDifference = 1; + foreach (var b in differences) + { + if (b > maxDifference) + maxDifference = b; + } + for (var y = 0; y < differences.GetLength(1); y++) + { + for (var x = 0; x < differences.GetLength(0); x++) + { + var percentageDifference = (float) differences[x, y]/maxDifference; + var colorIndex = (int) (255*percentageDifference); + g.FillRectangle(new SolidBrush(Color.FromArgb(colorIndex, 255, 2, 2)), x*16, y*16, 16, 16); + } + } + return bmp; + } + + /// + /// Finds the differences between two images and returns them in a doublearray + /// + /// The first image + /// The image to compare with + /// the differences between the two images as a doublearray + public static byte[,] GetDifferences(this Image img1, Image img2) + { + var thisOne = (Bitmap) img1.Resize(16, 16).GetGrayScaleVersion(); + var theOtherOne = (Bitmap) img2.Resize(16, 16).GetGrayScaleVersion(); + var differencesRed = new byte[16, 16]; + Console.WriteLine(); + + for (var y = 0; y < 16; y++) + { + for (var x = 0; x < 16; x++) + { + differencesRed[x, y] = (byte) Math.Abs(thisOne.GetPixel(x, y).R - theOtherOne.GetPixel(x, y).R); + } + } + return differencesRed; + } + + /// + /// Converts an image to grayscale + /// + /// The image to grayscale + /// A grayscale version of the image + public static Image GetGrayScaleVersion(this Image original) + { + //http://www.switchonthecode.com/tutorials/csharp-tutorial-convert-a-color-image-to-grayscale + //create a blank bitmap the same size as original + var newBitmap = new Bitmap(original.Width, original.Height); + + //get a graphics object from the new image + var g = Graphics.FromImage(newBitmap); + + //create some image attributes + var attributes = new ImageAttributes(); + + //set the color matrix attribute + attributes.SetColorMatrix(ColorMatrix); + + //draw the original image on the new image + //using the grayscale color matrix + g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height), + 0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes); + + //dispose the Graphics object + g.Dispose(); + + return newBitmap; + } + + public static Image GetAverageColorValue(this Image original) + { + var newBitmap = new Bitmap(original.Width, original.Height); + + return newBitmap; + } + + /// + /// Resizes an image + /// + /// The image to resize + /// The new width in pixels + /// The new height in pixels + /// A resized version of the original image + public static Image Resize(this Image originalImage, int newWidth, int newHeight) + { + Image smallVersion = new Bitmap(newWidth, newHeight); + using (var g = Graphics.FromImage(smallVersion)) + { + g.SmoothingMode = SmoothingMode.HighQuality; + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.DrawImage(originalImage, 0, 0, newWidth, newHeight); + } + + return smallVersion; + } + + /// + /// Helpermethod to print a doublearray of + /// + /// The type of doublearray + /// The doublearray to print + public static void ToConsole(this T[,] doubleArray) + { + for (var y = 0; y < doubleArray.GetLength(0); y++) + { + Console.Write("["); + for (var x = 0; x < doubleArray.GetLength(1); x++) + { + Console.Write("{0,3},", doubleArray[x, y]); + } + Console.WriteLine("]"); + } + } + + /// + /// Gets a bitmap with the RGB histograms of a bitmap + /// + /// The bitmap to get the histogram for + /// A bitmap with the histogram for R, G and B values + public static Bitmap GetRgbHistogramBitmap(this Bitmap bmp) + { + return new Histogram(bmp).Visualize(); + } + + /// + /// Get a histogram for a bitmap + /// + /// The bitmap to get the histogram for + /// A histogram for the bitmap + public static Histogram GetRgbHistogram(this Bitmap bmp) + { + return new Histogram(bmp); + } + + /// + /// Gets the difference between two images as a percentage + /// + /// The difference between the two images as a percentage + /// The path to the first image + /// The path to the second image + /// How big a difference (out of 255) will be ignored - the default is 3. + /// The difference between the two images as a percentage + public static float GetPercentageDifference(string image1Path, string image2Path, byte threshold = 3) + { + if (CheckFile(image1Path) && CheckFile(image2Path)) + { + var img1 = Image.FromFile(image1Path); + var img2 = Image.FromFile(image2Path); + + return img1.PercentageDifference(img2, threshold); + } + return -1; + } + + private static bool CheckFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException("File '" + filePath + "' not found!"); + } + return true; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Button.cs b/WebDriver/Elements/Types/Button.cs new file mode 100644 index 0000000..81cd582 --- /dev/null +++ b/WebDriver/Elements/Types/Button.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Button : Element + { + public Button(By bylocator) : base(bylocator) + { + } + + public Button(string name, By bylocator) : base(name, bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Checkbox.cs b/WebDriver/Elements/Types/Checkbox.cs new file mode 100644 index 0000000..78f0559 --- /dev/null +++ b/WebDriver/Elements/Types/Checkbox.cs @@ -0,0 +1,25 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Checkbox : Element + { + public Checkbox(By bylocator) : base(bylocator) + { + @by = bylocator; + } + + public Checkbox(string name, By bylocator) : base(name, bylocator) + { + } + + public new Checkbox SetCheckbox(bool isChecked) + { + if (element.Selected != isChecked) + { + element.Click(); + } + return this; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Dropdown.cs b/WebDriver/Elements/Types/Dropdown.cs new file mode 100644 index 0000000..f0f056d --- /dev/null +++ b/WebDriver/Elements/Types/Dropdown.cs @@ -0,0 +1,22 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; + +namespace Golem.WebDriver.Elements.Types +{ + public class Dropdown : Element + { + public Dropdown(By bylocator) : base(bylocator) + { + } + + public Dropdown(string name, By bylocator) : base(name, bylocator) + { + } + + public Dropdown SelectOption(string option) + { + new SelectElement(element).SelectByText(option); + return this; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Field.cs b/WebDriver/Elements/Types/Field.cs new file mode 100644 index 0000000..2b6f269 --- /dev/null +++ b/WebDriver/Elements/Types/Field.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Field : Element + { + public Field(By bylocator) : base(bylocator) + { + } + + public Field(string name, By bylocator) : base(name, bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Image.cs b/WebDriver/Elements/Types/Image.cs new file mode 100644 index 0000000..04a7782 --- /dev/null +++ b/WebDriver/Elements/Types/Image.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Image : Element + { + public Image(By bylocator) : base(bylocator) + { + } + + public Image(string name, By bylocator) : base(name, bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Link.cs b/WebDriver/Elements/Types/Link.cs new file mode 100644 index 0000000..9bbd953 --- /dev/null +++ b/WebDriver/Elements/Types/Link.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Link : Element + { + public Link(string name, By bylocator) : base(name, bylocator) + { + } + + public Link(By bylocator) : base(bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Panel.cs b/WebDriver/Elements/Types/Panel.cs new file mode 100644 index 0000000..93f0451 --- /dev/null +++ b/WebDriver/Elements/Types/Panel.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Panel : Element + { + public Panel(By bylocator) : base(bylocator) + { + } + + public Panel(string name, By bylocator) : base(name, bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Radio.cs b/WebDriver/Elements/Types/Radio.cs new file mode 100644 index 0000000..df3dd87 --- /dev/null +++ b/WebDriver/Elements/Types/Radio.cs @@ -0,0 +1,21 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Radio : Element + { + public Radio(By bylocator) : base(bylocator) + { + } + + public Radio(string name, By bylocator) : base(name, bylocator) + { + } + + public Radio SetValue(string value) + { + element.FindElement(By.XPath("//*[@value='" + value + "']")).Click(); + return this; + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Types/Text.cs b/WebDriver/Elements/Types/Text.cs new file mode 100644 index 0000000..4cf6b9b --- /dev/null +++ b/WebDriver/Elements/Types/Text.cs @@ -0,0 +1,15 @@ +using OpenQA.Selenium; + +namespace Golem.WebDriver.Elements.Types +{ + public class Text : Element + { + public Text(By bylocator) : base(bylocator) + { + } + + public Text(string name, By bylocator) : base(name, bylocator) + { + } + } +} \ No newline at end of file diff --git a/WebDriver/Elements/Validation/ValidationElement.cs b/WebDriver/Elements/Validation/ValidationElement.cs new file mode 100644 index 0000000..bcc606f --- /dev/null +++ b/WebDriver/Elements/Validation/ValidationElement.cs @@ -0,0 +1,60 @@ +using OpenQA.Selenium; +using Golem.Core; + +namespace Golem.WebDriver.Elements.Validation +{ + /// + /// Validation Element class. Provides the functionality of an Element with the + /// added benefit of being able to verify form validations + /// + public class ValidationElement : Element + { + protected By locatorValidation; + + public ValidationElement() + { + } + + /// + /// Constructor + /// + /// Description of the element + /// ByPurplePath for the element + /// ByPurplePath for the form validation + public ValidationElement(string name, By locatorElement, By locatorValidation) : base(name, locatorElement) + { + this.locatorValidation = locatorValidation; + } + + /// + /// Verifies the text exists for the validation element. + /// The form validation error must be on screen before you call this API. + /// + /// The text to verify + /// Number iterations in seconds to retry finding the element + /// this + public ValidationElement VerifyTextValidation(string text, int timeoutSec = 0) + { + if (timeoutSec == 0) timeoutSec = Config.settings.runTimeSettings.ElementTimeoutSec; + for (var i = 0; i <= timeoutSec; i++) + { + if (driver.FindElements(locatorValidation).Count != 0) + { + if (driver.FindElement(locatorValidation).Text.Contains(text)) + { +// TestContext.CurrentContext.IncrementAssertCount(); + return this; + } + } + else + { + Common.Delay(1000); + } + } + + TestBase.AddVerificationError(TestBase.GetCurrentClassAndMethodName() + ": Element : " + name + " (" + + locatorValidation + ") not present after " + timeoutSec + " seconds"); + return this; + } + } +} \ No newline at end of file diff --git a/WebDriver/EventFiringWebDriver.cs b/WebDriver/EventFiringWebDriver.cs new file mode 100644 index 0000000..fd81927 --- /dev/null +++ b/WebDriver/EventFiringWebDriver.cs @@ -0,0 +1,1513 @@ +// Type: OpenQA.Selenium.Support.Events.EventFiringWebDriver +// Assembly: WebDriver.Support, Version=2.40.0.0, Culture=neutral +// MVID: 9FAA975A-389C-466A-AE2E-96ABC7996728 +// Assembly location: C:\Users\Brian\Documents\GitHub\Golem\Golem\packages\Selenium.Support.2.40.0\lib\net40\WebDriver.Support.dll + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Drawing; +using System.Linq; +using OpenQA.Selenium; +using OpenQA.Selenium.Internal; +using OpenQA.Selenium.Support.Events; + +namespace Golem.WebDriver +{ + /// + /// A wrapper around an arbitrary WebDriver instance which supports registering for + /// events, e.g. for logging purposes. + /// + public class EventFiringWebDriver : IWebDriver, ISearchContext, IDisposable, IJavaScriptExecutor, ITakesScreenshot, + IWrapsDriver + { + /// + /// Initializes a new instance of the EventFiringWebDriver class. + /// + /// The driver to register events for. + public EventFiringWebDriver(IWebDriver parentDriver) + { + WrappedDriver = parentDriver; + } + + /// + /// Executes JavaScript in the context of the currently selected frame or window. + /// + /// The JavaScript code to execute. + /// The arguments to the script. + /// + /// The value returned by the script. + /// + /// + /// + /// The + /// + /// method executes JavaScript in the context of + /// the currently selected frame or window. This means that "document" will refer + /// to the current document. If the script has a return value, then the following + /// steps will be taken: + /// + /// + /// + /// + /// + /// For an HTML element, this method returns a + /// + /// + /// + /// For a number, a is returned + /// + /// + /// For a boolean, a is returned + /// + /// + /// For all other cases a is returned. + /// + /// + /// + /// For an array,we check the first element, and attempt to return a + /// of that type, following the rules above. Nested + /// lists are not + /// supported. + /// + /// + /// + /// + /// If the value is null or there is no return value, + /// is returned. + /// + /// + /// + /// + /// + /// Arguments must be a number (which will be converted to a ), + /// a , a or a + /// . + /// An exception will be thrown if the arguments do not meet these criteria. + /// The arguments will be made available to the JavaScript via the "arguments" magic + /// variable, as if the function were called via "Function.apply" + /// + /// + public object ExecuteScript(string script, params object[] args) + { + var javaScriptExecutor = WrappedDriver as IJavaScriptExecutor; + if (javaScriptExecutor == null) + throw new NotSupportedException("Underlying driver instance does not support executing javascript"); + object obj; + try + { + var objArray = UnwrapElementArguments(args); + var e = new WebDriverScriptEventArgs(WrappedDriver, script); + OnScriptExecuting(e); + obj = javaScriptExecutor.ExecuteScript(script, objArray); + OnScriptExecuted(e); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + return obj; + } + + /// + /// Executes JavaScript asynchronously in the context of the currently selected frame or window. + /// + /// The JavaScript code to execute. + /// The arguments to the script. + /// + /// The value returned by the script. + /// + public object ExecuteAsyncScript(string script, params object[] args) + { + var javaScriptExecutor = WrappedDriver as IJavaScriptExecutor; + if (javaScriptExecutor == null) + throw new NotSupportedException("Underlying driver instance does not support executing javascript"); + object obj; + try + { + var objArray = UnwrapElementArguments(args); + var e = new WebDriverScriptEventArgs(WrappedDriver, script); + OnScriptExecuting(e); + obj = javaScriptExecutor.ExecuteAsyncScript(script, objArray); + OnScriptExecuted(e); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + return obj; + } + + /// + /// Gets a object representing the image of the page on the screen. + /// + /// + /// A object containing the image. + /// + public Screenshot GetScreenshot() + { + var takesScreenshot = WrappedDriver as ITakesScreenshot; + if (WrappedDriver == null) + throw new NotSupportedException("Underlying driver instance does not support taking screenshots"); + return takesScreenshot.GetScreenshot(); + } + + /// + /// Gets or sets the URL the browser is currently displaying. + /// + /// + /// Setting the property will load a new web + /// page in the current browser window. + /// This is done using an HTTP GET operation, and the method will block until the + /// load is complete. This will follow redirects issued either by the server or + /// as a meta-redirect from within the returned HTML. Should a meta-redirect "rest" + /// for any duration of time, it is best to wait until this timeout is over, since + /// should the underlying page change while your test is executing the results of + /// future calls against this interface will be against the freshly loaded page. + /// + /// + /// + public string Url + { + get + { + var str = string.Empty; + try + { + return WrappedDriver.Url; + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + set + { + try + { + var e = new WebDriverNavigationEventArgs(WrappedDriver, value); + OnNavigating(e); + WrappedDriver.Url = value; + OnNavigated(e); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + } + + /// + /// Gets the title of the current browser window. + /// + public string Title + { + get + { + var str = string.Empty; + try + { + return WrappedDriver.Title; + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + } + + /// + /// Gets the source of the page last loaded by the browser. + /// + /// + /// If the page has been modified after loading (for example, by JavaScript) + /// there is no guarantee that the returned text is that of the modified page. + /// Please consult the documentation of the particular driver being used to + /// determine whether the returned text reflects the current state of the page + /// or the text last sent by the web server. The page source returned is a + /// representation of the underlying DOM: do not expect it to be formatted + /// or escaped in the same way as the response sent from the web server. + /// + public string PageSource + { + get + { + var str = string.Empty; + try + { + return WrappedDriver.PageSource; + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + } + + /// + /// Gets the current window handle, which is an opaque handle to this + /// window that uniquely identifies it within this driver instance. + /// + public string CurrentWindowHandle + { + get + { + var str = string.Empty; + try + { + return WrappedDriver.CurrentWindowHandle; + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + } + + /// + /// Gets the window handles of open browser windows. + /// + public ReadOnlyCollection WindowHandles + { + get + { + try + { + return WrappedDriver.WindowHandles; + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + } + + /// + /// Close the current window, quitting the browser if it is the last window currently open. + /// + public void Close() + { + try + { + WrappedDriver.Close(); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + + /// + /// Quits this driver, closing every associated window. + /// + public void Quit() + { + try + { + WrappedDriver.Quit(); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + + /// + /// Instructs the driver to change its settings. + /// + /// + /// An object allowing the user to change + /// the settings of the driver. + /// + public IOptions Manage() + { + return new EventFiringOptions(this); + } + + /// + /// Instructs the driver to navigate the browser to another location. + /// + /// + /// An object allowing the user to access + /// the browser's history and to navigate to a given URL. + /// + public INavigation Navigate() + { + return new EventFiringNavigation(this); + } + + /// + /// Instructs the driver to send future commands to a different frame or window. + /// + /// + /// An object which can be used to select + /// a frame or window. + /// + public ITargetLocator SwitchTo() + { + return new EventFiringTargetLocator(this); + } + + /// + /// Find the first using the given method. + /// + /// The locating mechanism to use. + /// + /// The first matching on the current context. + /// + /// If no element matches the criteria. + public IWebElement FindElement(By by) + { + try + { + var e = new FindElementEventArgs(WrappedDriver, by); + OnFindingElement(e); + var element = WrappedDriver.FindElements(by).FirstOrDefault(x => x.Displayed); + OnFindElementCompleted(e); + var f = new FoundElementEventArgs(WrappedDriver, element, by); + OnFoundElement(f); + return WrapElement(element); + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + } + + /// + /// Find all IWebElements within the current context + /// using the given mechanism. + /// + /// The locating mechanism to use. + /// + /// A of all + /// WebElements + /// matching the current criteria, or an empty list if nothing matches. + /// + public ReadOnlyCollection FindElements(By by) + { + var list = new List(); + try + { + var e = new FindElementEventArgs(WrappedDriver, by); + OnFindingElement(e); + var elements = WrappedDriver.FindElements(by).Where(x => x.Displayed); + OnFindElementCompleted(e); + foreach (var underlyingElement in elements) + { + var webElement = WrapElement(underlyingElement); + list.Add(webElement); + var f = new FoundElementEventArgs(WrappedDriver, underlyingElement, by); + OnFoundElement(f); + } + } + catch (Exception ex) + { + OnException(new WebDriverExceptionEventArgs(WrappedDriver, ex)); + throw; + } + return list.AsReadOnly(); + } + + /// + /// Frees all managed and unmanaged resources used by this instance. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the wrapped by this EventsFiringWebDriver instance. + /// + public IWebDriver WrappedDriver { get; private set; } + + /// + /// Fires before the driver begins navigation. + /// + public event EventHandler Navigating; + + /// + /// Fires after the driver completes navigation + /// + public event EventHandler Navigated; + + /// + /// Fires before the driver begins navigation back one entry in the browser history list. + /// + public event EventHandler NavigatingBack; + + /// + /// Fires after the driver completes navigation back one entry in the browser history list. + /// + public event EventHandler NavigatedBack; + + /// + /// Fires before the driver begins navigation forward one entry in the browser history list. + /// + public event EventHandler NavigatingForward; + + /// + /// Fires after the driver completes navigation forward one entry in the browser history list. + /// + public event EventHandler NavigatedForward; + + /// + /// Fires before the driver clicks on an element. + /// + public event EventHandler ElementClicking; + + /// + /// Fires after the driver has clicked on an element. + /// + public event EventHandler ElementClicked; + + /// + /// Fires before the driver changes the value of an element via Clear(), SendKeys() or Toggle(). + /// + public event EventHandler ElementValueChanging; + + /// + /// Fires after the driver has changed the value of an element via Clear(), SendKeys() or Toggle(). + /// + public event EventHandler ElementValueChanged; + + /// + /// Fires before the driver starts to find an element. + /// + public event EventHandler FindingElement; + + /// + /// Fires after the driver completes finding an element. + /// + public event EventHandler FindElementCompleted; + + /// + /// Fires after the driver completes finding an element. + /// + public event EventHandler FoundElement; + + /// + /// Fires before a script is executed. + /// + public event EventHandler ScriptExecuting; + + /// + /// Fires after a script is executed. + /// + public event EventHandler ScriptExecuted; + + /// + /// Fires when an exception is thrown. + /// + public event EventHandler ExceptionThrown; + + /// + /// Frees all managed and, optionally, unmanaged resources used by this instance. + /// + /// + /// to dispose of only managed resources; + /// to dispose of managed and unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) + return; + WrappedDriver.Dispose(); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigating(WebDriverNavigationEventArgs e) + { + if (Navigating == null) + return; + Navigating(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigated(WebDriverNavigationEventArgs e) + { + if (Navigated == null) + return; + Navigated(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigatingBack(WebDriverNavigationEventArgs e) + { + if (NavigatingBack == null) + return; + NavigatingBack(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigatedBack(WebDriverNavigationEventArgs e) + { + if (NavigatedBack == null) + return; + NavigatedBack(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigatingForward(WebDriverNavigationEventArgs e) + { + if (NavigatingForward == null) + return; + NavigatingForward(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnNavigatedForward(WebDriverNavigationEventArgs e) + { + if (NavigatedForward == null) + return; + NavigatedForward(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnElementClicking(WebElementEventArgs e) + { + if (ElementClicking == null) + return; + ElementClicking(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnElementClicked(WebElementEventArgs e) + { + if (ElementClicked == null) + return; + ElementClicked(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnElementValueChanging(WebElementEventArgs e) + { + if (ElementValueChanging == null) + return; + ElementValueChanging(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnElementValueChanged(WebElementEventArgs e) + { + if (ElementValueChanged == null) + return; + ElementValueChanged(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnFindingElement(FindElementEventArgs e) + { + if (FindingElement == null) + return; + FindingElement(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnFindElementCompleted(FindElementEventArgs e) + { + if (FindElementCompleted == null) + return; + FindElementCompleted(this, e); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected virtual void OnFoundElement(FoundElementEventArgs e) + { + if (FoundElement == null) + return; + FoundElement(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnScriptExecuting(WebDriverScriptEventArgs e) + { + if (ScriptExecuting == null) + return; + ScriptExecuting(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnScriptExecuted(WebDriverScriptEventArgs e) + { + if (ScriptExecuted == null) + return; + ScriptExecuted(this, e); + } + + /// + /// Raises the event. + /// + /// + /// A that contains the event + /// data. + /// + protected virtual void OnException(WebDriverExceptionEventArgs e) + { + if (ExceptionThrown == null) + return; + ExceptionThrown(this, e); + } + + private static object[] UnwrapElementArguments(object[] args) + { + var list = new List(); + foreach (var obj in args) + { + var firingWebElement = obj as EventFiringWebElement; + if (firingWebElement != null) + list.Add(firingWebElement.WrappedElement); + else + list.Add(obj); + } + return list.ToArray(); + } + + private IWebElement WrapElement(IWebElement underlyingElement) + { + return new EventFiringWebElement(this, underlyingElement); + } + + /// + /// Provides a mechanism for Navigating with the driver. + /// + private class EventFiringNavigation : INavigation + { + private readonly EventFiringWebDriver parentDriver; + private readonly INavigation wrappedNavigation; + + /// + /// Initializes a new instance of the EventFiringNavigation class + /// + /// Driver in use + public EventFiringNavigation(EventFiringWebDriver driver) + { + parentDriver = driver; + wrappedNavigation = parentDriver.WrappedDriver.Navigate(); + } + + /// + /// Move the browser back + /// + public void Back() + { + try + { + var e = new WebDriverNavigationEventArgs(parentDriver); + parentDriver.OnNavigatingBack(e); + wrappedNavigation.Back(); + parentDriver.OnNavigatedBack(e); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Move the browser forward + /// + public void Forward() + { + try + { + var e = new WebDriverNavigationEventArgs(parentDriver); + parentDriver.OnNavigatingForward(e); + wrappedNavigation.Forward(); + parentDriver.OnNavigatedForward(e); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Navigate to a url for your test + /// + /// String of where you want the browser to go to + public void GoToUrl(string url) + { + try + { + var e = new WebDriverNavigationEventArgs(parentDriver, url); + parentDriver.OnNavigating(e); + wrappedNavigation.GoToUrl(url); + parentDriver.OnNavigated(e); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Navigate to a url for your test + /// + /// Uri object of where you want the browser to go to + public void GoToUrl(Uri url) + { + if (url == null) + throw new ArgumentNullException("url", "url cannot be null"); + try + { + var e = new WebDriverNavigationEventArgs(parentDriver, url.ToString()); + parentDriver.OnNavigating(e); + wrappedNavigation.GoToUrl(url); + parentDriver.OnNavigated(e); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Refresh the browser + /// + public void Refresh() + { + try + { + wrappedNavigation.Refresh(); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + } + + /// + /// Provides a mechanism for setting options needed for the driver during the test. + /// + private class EventFiringOptions : IOptions + { + private readonly IOptions wrappedOptions; + + public ILogs Logs + { + get { return wrappedOptions.Logs; } + } + + /// + /// Initializes a new instance of the EventFiringOptions class + /// + /// Instance of the driver currently in use + public EventFiringOptions(EventFiringWebDriver driver) + { + wrappedOptions = driver.WrappedDriver.Manage(); + } + + /// + /// Gets an object allowing the user to manipulate cookies on the page. + /// + public ICookieJar Cookies + { + get { return wrappedOptions.Cookies; } + } + + /// + /// Gets an object allowing the user to manipulate the currently-focused browser window. + /// + /// + /// "Currently-focused" is defined as the browser window having the window handle + /// returned when IWebDriver.CurrentWindowHandle is called. + /// + public IWindow Window + { + get { return wrappedOptions.Window; } + } + + /// + /// Provides access to the timeouts defined for this driver. + /// + /// + /// An object implementing the interface. + /// + public ITimeouts Timeouts() + { + return new EventFiringTimeouts(wrappedOptions); + } + } + + /// + /// Provides a mechanism for finding elements on the page with locators. + /// + private class EventFiringTargetLocator : ITargetLocator + { + private readonly EventFiringWebDriver parentDriver; + private readonly ITargetLocator wrappedLocator; + + /// + /// Initializes a new instance of the EventFiringTargetLocator class + /// + /// The driver that is currently in use + public EventFiringTargetLocator(EventFiringWebDriver driver) + { + parentDriver = driver; + wrappedLocator = parentDriver.WrappedDriver.SwitchTo(); + } + + /// + /// Move to a different frame using its index + /// + /// The index of the + /// + /// A WebDriver instance that is currently in use + /// + public IWebDriver Frame(int frameIndex) + { + try + { + return wrappedLocator.Frame(frameIndex); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Move to different frame using its name + /// + /// name of the frame + /// + /// A WebDriver instance that is currently in use + /// + public IWebDriver Frame(string frameName) + { + try + { + return wrappedLocator.Frame(frameName); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Move to a frame element. + /// + /// a previously found FRAME or IFRAME element. + /// + /// A WebDriver instance that is currently in use. + /// + public IWebDriver Frame(IWebElement frameElement) + { + try + { + return wrappedLocator.Frame((frameElement as IWrapsElement).WrappedElement); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Change to the Window by passing in the name + /// + /// name of the window that you wish to move to + /// + /// A WebDriver instance that is currently in use + /// + public IWebDriver Window(string windowName) + { + try + { + return wrappedLocator.Window(windowName); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Change the active frame to the default + /// + /// + /// Element of the default + /// + public IWebDriver DefaultContent() + { + try + { + return wrappedLocator.DefaultContent(); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Finds the active element on the page and returns it + /// + /// + /// Element that is active + /// + public IWebElement ActiveElement() + { + try + { + return wrappedLocator.ActiveElement(); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + /// + /// Switches to the currently active modal dialog for this particular driver instance. + /// + /// + /// A handle to the dialog. + /// + public IAlert Alert() + { + try + { + return wrappedLocator.Alert(); + } + catch (Exception ex) + { + parentDriver.OnException(new WebDriverExceptionEventArgs(parentDriver, ex)); + throw; + } + } + + public IWebDriver ParentFrame() + { + return wrappedLocator.DefaultContent(); + } + } + + /// + /// Defines the interface through which the user can define timeouts. + /// + private class EventFiringTimeouts : ITimeouts + { + private readonly ITimeouts wrappedTimeouts; + + /// + /// Initializes a new instance of the EventFiringTimeouts class + /// + /// The object to wrap. + public EventFiringTimeouts(IOptions options) + { + wrappedTimeouts = options.Timeouts(); + } + + /// + /// Specifies the amount of time the driver should wait when searching for an + /// element if it is not immediately present. + /// + /// A structure defining the amount of time to wait. + /// + /// A self reference + /// + /// + /// When searching for a single element, the driver should poll the page + /// until the element has been found, or this timeout expires before throwing + /// a . When searching for multiple elements, + /// the driver should poll the page until at least one element has been found + /// or this timeout has expired. + /// + /// Increasing the implicit wait timeout should be used judiciously as it + /// will have an adverse effect on test run time, especially when used with + /// slower location strategies like XPath. + /// + /// + public ITimeouts ImplicitlyWait(TimeSpan timeToWait) + { + return wrappedTimeouts.ImplicitlyWait(timeToWait); + } + + /// + /// Specifies the amount of time the driver should wait when executing JavaScript asynchronously. + /// + /// A structure defining the amount of time to wait. + /// + /// A self reference + /// + public ITimeouts SetScriptTimeout(TimeSpan timeToWait) + { + return wrappedTimeouts.SetScriptTimeout(timeToWait); + } + + /// + /// Specifies the amount of time the driver should wait for a page to load when setting the + /// property. + /// + /// A structure defining the amount of time to wait. + /// + /// A self reference + /// + public ITimeouts SetPageLoadTimeout(TimeSpan timeToWait) + { + wrappedTimeouts.SetPageLoadTimeout(timeToWait); + return this; + } + + public TimeSpan ImplicitWait { get; set; } + public TimeSpan AsynchronousJavaScript { get; set; } + public TimeSpan PageLoad { get; set; } + } + + /// + /// EventFiringWebElement allows you to have access to specific items that are found on the page + /// + private class EventFiringWebElement : IWebElement, ISearchContext, IWrapsElement, IWrapsDriver + { + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The instance hosting this + /// element. + /// + /// The to wrap for event firing. + public EventFiringWebElement(EventFiringWebDriver driver, IWebElement element) + { + WrappedElement = element; + ParentDriver = driver; + } + + /// + /// Gets the underlying EventFiringWebDriver for this element. + /// + protected EventFiringWebDriver ParentDriver { get; private set; } + + /// + /// Gets the DOM Tag of element + /// + public string TagName + { + get + { + var str = string.Empty; + try + { + return WrappedElement.TagName; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets the text from the element + /// + public string Text + { + get + { + var str = string.Empty; + try + { + return WrappedElement.Text; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets a value indicating whether an element is currently enabled + /// + public bool Enabled + { + get + { + try + { + return WrappedElement.Enabled; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets a value indicating whether this element is selected or not. This operation only applies to input elements such + /// as checkboxes, options in a select and radio buttons. + /// + public bool Selected + { + get + { + try + { + return WrappedElement.Selected; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets the Location of an element and returns a Point object + /// + public Point Location + { + get + { + var point = new Point(); + try + { + return WrappedElement.Location; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets the of the + /// element on the page + /// + public Size Size + { + get + { + var size = new Size(); + try + { + return WrappedElement.Size; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Gets a value indicating whether the element is currently being displayed + /// + public bool Displayed + { + get + { + try + { + return WrappedElement.Displayed; + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + } + + /// + /// Method to clear the text out of an Input element + /// + public void Clear() + { + try + { + var e = new WebElementEventArgs(ParentDriver.WrappedDriver, WrappedElement); + ParentDriver.OnElementValueChanging(e); + WrappedElement.Clear(); + ParentDriver.OnElementValueChanged(e); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// Method for sending native key strokes to the browser + /// + /// String containing what you would like to type onto the screen + public void SendKeys(string text) + { + try + { + var e = new WebElementEventArgs(ParentDriver.WrappedDriver, WrappedElement); + ParentDriver.OnElementValueChanging(e); + WrappedElement.SendKeys(text); + ParentDriver.OnElementValueChanged(e); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// If this current element is a form, or an element within a form, then this will be submitted to the remote server. + /// If this causes the current page to change, then this method will block until the new page is loaded. + /// + public void Submit() + { + try + { + WrappedElement.Submit(); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// Click this element. If this causes a new page to load, this method will block until + /// the page has loaded. At this point, you should discard all references to this element + /// and any further operations performed on this element will have undefined behavior unless + /// you know that the element and the page will still be present. If this element is not + /// clickable, then this operation is a no-op since it's pretty common for someone to + /// accidentally miss the target when clicking in Real Life + /// + public void Click() + { + try + { + var e = new WebElementEventArgs(ParentDriver.WrappedDriver, WrappedElement); + ParentDriver.OnElementClicking(e); + WrappedElement.Click(); + ParentDriver.OnElementClicked(e); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// If this current element is a form, or an element within a form, then this will be submitted to the remote server. + /// If this causes the current page to change, then this method will block until the new page is loaded. + /// + /// Attribute you wish to get details of + /// + /// The attribute's current value or null if the value is not set. + /// + public string GetAttribute(string attributeName) + { + var str = string.Empty; + try + { + return WrappedElement.GetAttribute(attributeName); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// Method to return the value of a CSS Property + /// + /// CSS property key + /// + /// string value of the CSS property + /// + public string GetCssValue(string propertyName) + { + var str = string.Empty; + try + { + return WrappedElement.GetCssValue(propertyName); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// Finds the first element in the page that matches the object + /// + /// By mechanism to find the element + /// + /// IWebElement object so that you can interaction that object + /// + public IWebElement FindElement(By by) + { + try + { + var e = new FindElementEventArgs(ParentDriver.WrappedDriver, WrappedElement, by); + ParentDriver.OnFindingElement(e); + WrappedElement.Highlight(100,"green"); + var element = WrappedElement.FindElement(by); + ParentDriver.OnFindElementCompleted(e); + var f = new FoundElementEventArgs(ParentDriver.WrappedDriver, element, by); + ParentDriver.OnFoundElement(f); + return ParentDriver.WrapElement(element); + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + } + + /// + /// Finds the elements on the page by using the object and returns a + /// ReadOnlyCollection of the Elements on the page + /// + /// By mechanism to find the element + /// + /// ReadOnlyCollection of IWebElement + /// + public ReadOnlyCollection FindElements(By by) + { + var list = new List(); + try + { + var e = new FindElementEventArgs(ParentDriver.WrappedDriver, WrappedElement, by); + ParentDriver.OnFindingElement(e); + WrappedElement.Highlight(100, "green"); + var elements = WrappedElement.FindElements(by); + ParentDriver.OnFindElementCompleted(e); + foreach (var underlyingElement in elements) + { + var webElement = ParentDriver.WrapElement(underlyingElement); + list.Add(webElement); + var f = new FoundElementEventArgs(ParentDriver.WrappedDriver, webElement, by); + ParentDriver.OnFoundElement(f); + } + } + catch (Exception ex) + { + ParentDriver.OnException(new WebDriverExceptionEventArgs(ParentDriver, ex)); + throw; + } + return list.AsReadOnly(); + } + + public IWebDriver WrappedDriver + { + get { return ParentDriver; } + } + + /// + /// Gets the underlying wrapped . + /// + public IWebElement WrappedElement { get; private set; } + } + } +} \ No newline at end of file diff --git a/WebDriver/EventedWebDriver.cs b/WebDriver/EventedWebDriver.cs new file mode 100644 index 0000000..94f2971 --- /dev/null +++ b/WebDriver/EventedWebDriver.cs @@ -0,0 +1,142 @@ +using System; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.Events; +using Golem.Core; + +namespace Golem.WebDriver +{ + public class EventedWebDriver + { + private const string errorMessage = "{0}: {1} ({2}) {3}"; + public EventFiringWebDriver driver; + + public EventedWebDriver(IWebDriver driver) + { + this.driver = new EventFiringWebDriver(driver); + RegisterEvents(); + } + + public IWebDriver RegisterEvents() + { + driver.ElementClicking += driver_ElementClicking; + driver.ElementClicked += driver_ElementClicked; + driver.ExceptionThrown += driver_ExceptionThrown; + driver.FindingElement += driver_FindingElement; + driver.Navigating += driver_Navigating; + driver.Navigated += driver_Navigated; + driver.ElementValueChanged += driver_ElementValueChanged; + driver.ElementValueChanging += driver_ElementValueChanging; + driver.FindElementCompleted += driver_FindElementCompleted; + driver.FoundElement += driver_FoundElement; + return driver; + } + + private void driver_ElementClicked(object sender, WebElementEventArgs e) + { + + Common.Delay(Config.settings.runTimeSettings.CommandDelayMs); + } + + private void driver_Navigated(object sender, WebDriverNavigationEventArgs e) + { + Common.Delay(Config.settings.runTimeSettings.CommandDelayMs); + driver.WaitForJQuery(); + + } + + private void driver_FoundElement(object sender, FoundElementEventArgs e) + { + if (Config.settings.runTimeSettings.HighlightFoundElements) + { + e.Element.Highlight(); + } + } + + private void driver_FindElementCompleted(object sender, FindElementEventArgs e) + { +// TestContext.CurrentContext.IncrementAssertCount(); + } + + private void driver_ElementValueChanging(object sender, WebElementEventArgs e) + { + try + { + driver.WaitForJQuery(); + e.Element.Highlight(30, "red"); + } + catch (Exception) + { + } + } + + private void driver_ElementValueChanged(object sender, WebElementEventArgs e) + { + try + { + + if (Config.settings.reportSettings.commandLogging) + { + Log.Message(GetLogMessage("Typing", e, e.Element.GetAttribute("value"))); + } + Common.Delay(Config.settings.runTimeSettings.CommandDelayMs); + } + catch (Exception) + { + } + } + + private void driver_Navigating(object sender, WebDriverNavigationEventArgs e) + { + if (Config.settings.reportSettings.commandLogging) + { + Log.Message(string.Format("Navigating to url {0}", e.Url)); + } + driver.WaitForJQuery(); + + } + + private void driver_ExceptionThrown(object sender, WebDriverExceptionEventArgs e) + { + } + + private void driver_FindingElement(object sender, FindElementEventArgs e) + { + driver.WaitForJQuery(); + Common.Delay(Config.settings.runTimeSettings.CommandDelayMs); + if (Config.settings.reportSettings.commandLogging) + { + Log.Message(GetLogMessage("Finding", e)); + } + } + + private void driver_ElementClicking(object sender, WebElementEventArgs e) + { + if (Config.settings.reportSettings.commandLogging) + { + Log.Message(GetLogMessage("Click", e)); + } + + if (e.Element == null) + { + throw new NoSuchElementException(string.Format("Element '{0}' not present, cannot click on it", + e.Element)); + } + + driver.WaitForJQuery(); + e.Element.Highlight(30, "red"); + } + + private string GetLogMessage(string command, WebElementEventArgs e = null, string param = "") + { + if (param != "") param = "'" + param + "'"; + return string.Format(errorMessage, TestBase.GetCurrentClassAndMethodName(), command, e.Element.GetHtml(), param); + } + + private string GetLogMessage(string command, FindElementEventArgs e = null, string param = "") + { + if (param != "") param = "'" + param + "'"; + return string.Format(errorMessage, TestBase.GetCurrentClassAndMethodName(), command, e.FindMethod, + param); + } + } +} \ No newline at end of file diff --git a/WebDriver/FoundElementEventArgs.cs b/WebDriver/FoundElementEventArgs.cs new file mode 100644 index 0000000..03e66f6 --- /dev/null +++ b/WebDriver/FoundElementEventArgs.cs @@ -0,0 +1,44 @@ +// Type: OpenQA.Selenium.Support.Events.FindElementEventArgs +// Assembly: WebDriver.Support, Version=2.40.0.0, Culture=neutral +// MVID: 9FAA975A-389C-466A-AE2E-96ABC7996728 +// Assembly location: C:\Users\Brian\Documents\GitHub\Golem\Golem\packages\Selenium.Support.2.40.0\lib\net40\WebDriver.Support.dll + +using System; +using OpenQA.Selenium; + +namespace Golem.WebDriver +{ + /// + /// Provides data for events related to finding elements. + /// + public class FoundElementEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The WebDriver instance used in finding elements. + /// The element that was found + /// The object containing the method used to find elements. + public FoundElementEventArgs(IWebDriver driver, IWebElement element, OpenQA.Selenium.By method) + { + Driver = driver; + Element = element; + FindMethod = method; + } + + /// + /// Gets the WebDriver instance used in finding elements. + /// + public IWebDriver Driver { get; private set; } + + /// + /// Gets the element that was found + /// + public IWebElement Element { get; private set; } + + /// + /// Gets the object containing the method used to find elements. + /// + public By FindMethod { get; private set; } + } +} \ No newline at end of file diff --git a/WebDriver/Frame.cs b/WebDriver/Frame.cs new file mode 100644 index 0000000..5ca01ed --- /dev/null +++ b/WebDriver/Frame.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenQA.Selenium; + +namespace Golem.WebDriver +{ + public class Frame : Element + { + public Frame(By by) : base(by) + { + } + } +} diff --git a/WebDriver/ScreenshotRemoteDriver.cs b/WebDriver/ScreenshotRemoteDriver.cs new file mode 100644 index 0000000..6c610e2 --- /dev/null +++ b/WebDriver/ScreenshotRemoteDriver.cs @@ -0,0 +1,58 @@ +using System; +using OpenQA.Selenium; +using OpenQA.Selenium.Remote; + +namespace Golem.WebDriver +{ + /// + /// implements the GetScreenshot() method to su pport remote screenshots. + /// + public class ScreenshotRemoteWebDriver : RemoteWebDriver, + ITakesScreenshot, IHasTouchScreen + { + public ScreenshotRemoteWebDriver(ICommandExecutor commandExecutor, ICapabilities desiredCapabilities) + : base(commandExecutor, desiredCapabilities) + { + } + + public ScreenshotRemoteWebDriver(ICapabilities desiredCapabilities) : base(desiredCapabilities) + { + } + + public ScreenshotRemoteWebDriver(Uri remoteAddress, ICapabilities desiredCapabilities) + : base(remoteAddress, desiredCapabilities) + { + } + + public ScreenshotRemoteWebDriver(Uri remoteAddress, ICapabilities desiredCapabilities, TimeSpan commandTimeout) + : base(remoteAddress, desiredCapabilities, commandTimeout) + { + } + + /// + /// Gets the device representing the touch screen. + /// + public ITouchScreen TouchScreen { get; private set; } + + /// + /// Gets a object representing the image of the page on the screen. + /// + /// A object containing the image. + public Screenshot GetScreenshot() + { + try + { + // Get the screenshot as base64. + var screenshotResponse = Execute(DriverCommand.Screenshot, null); + var base64 = screenshotResponse.Value.ToString(); + + // ... and convert it. + return new Screenshot(base64); + } + catch (Exception) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/WebDriver/VerificationError.cs b/WebDriver/VerificationError.cs new file mode 100644 index 0000000..d29c4ca --- /dev/null +++ b/WebDriver/VerificationError.cs @@ -0,0 +1,28 @@ +using System.Drawing; + +namespace Golem.WebDriver +{ + /// + /// Holds an error message and a screenshot. + /// + public class VerificationError + { + public string errorText; + public Image screenshot; + + public VerificationError(string errorText, bool takeScreenshot) + { + this.errorText = errorText; + if (takeScreenshot) + { + screenshot = WebDriverTestBase.driver.GetScreenshot(); + } + } + + public VerificationError(string errorText, Image screenshot) + { + this.errorText = errorText; + this.screenshot = screenshot; + } + } +} \ No newline at end of file diff --git a/WebDriver/WebDriverBrowser.cs b/WebDriver/WebDriverBrowser.cs new file mode 100644 index 0000000..51a566e --- /dev/null +++ b/WebDriver/WebDriverBrowser.cs @@ -0,0 +1,311 @@ +using System; +using System.Configuration; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.IE; +using OpenQA.Selenium.PhantomJS; +using OpenQA.Selenium.Remote; +using OpenQA.Selenium.Safari; +using OpenQA.Selenium.Edge; +using Golem.Core; + +namespace Golem.WebDriver +{ + /// + /// Contains all functionality relating to launching the webdriver browsers. + /// + public class WebDriverBrowser + { + public enum Browser + { + Firefox, + Chrome, + IE, + Edge, + Safari, + PhantomJS, + Android, + IPhone, + IPad, + None + } + + public IWebDriver driver; + + public static Browser getBrowserFromString(string name) + { + return (Browser) Enum.Parse(typeof(Browser), name); + } + + public IWebDriver LaunchBrowser(Browser browser) + { + switch (browser) + { + case Browser.IE: + driver = StartIEBrowser(); + break; + case Browser.Edge: + driver = StartEdgeBrowser(); + break; + case Browser.Chrome: + driver = StartChromeBrowser(); + break; + case Browser.Safari: + driver = StartSafariBrowser(); + break; + case Browser.PhantomJS: + driver = StartPhantomJSBrowser(); + break; + default: + driver = StartFirefoxBrowser(); + break; + } + + // deleting cookies throws an exception when trying to delete cookies before site is loaded + try + { + driver.Manage().Cookies.DeleteAllCookies(); + } + catch (Exception e) + { + + TestContext.WriteLine(e.Message); + } + + SetBrowserSize(); + var eDriver = new EventedWebDriver(driver); + + return eDriver.driver; + } + + private void SetBrowserSize() + { + var resolution = Config.settings.runTimeSettings.BrowserResolution; + if (resolution.Contains("Default")) + { + Log.Message("Maximizing Browser"); + driver.Manage().Window.Maximize(); + } + else + { + Log.Message($"Setting Browser Size to {resolution}"); + driver.Manage().Window.Size = Common.GetSizeFromResolution(resolution); + } + } + + public IWebDriver StartFirefoxBrowser() + { + // var capabilities = new DesiredCapabilities(); + FirefoxProfile profile = new FirefoxProfile(); + var proxy = new OpenQA.Selenium.Proxy(); + if (Config.settings.httpProxy.useProxy) + { + proxy.HttpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.SslProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.FtpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + // capabilities.SetCapability("proxy", proxy); + profile.SetProxyPreferences(proxy); + } + + // this is first solution from http://stackoverflow.com/questions/33937067/firefox-webdriver-opens-first-run-page-all-the-time + // firefoxProfile.SetPreference("browser.startup.homepage", "about:blank"); + // firefoxProfile.SetPreference("startup.homepage_welcome_url", "about:blank"); + // firefoxProfile.SetPreference("startup.homepage_welcome_url.additional", "about:blank"); + // capabilities.SetCapability(FirefoxDriver.ProfileCapabilityName, firefoxProfile.ToBase64String()); + + // this is second solution from http://stackoverflow.com/questions/33937067/firefox-webdriver-opens-first-run-page-all-the-time + // firefoxProfile.SetPreference("xpinstall.signatures.required", false); + // firefoxProfile.SetPreference("toolkit.telemetry.reportingpolicy.firstRun", false); + + // FirefoxProfile profile = new FirefoxProfile(); + profile.SetPreference("browser.startup.homepage_override.mstone", "ignore"); + profile.SetPreference("startup.homepage_welcome_url", "about:blank"); + profile.SetPreference("startup.homepage_welcome_url.additional", "about:blank"); + profile.SetPreference("browser.startup.homepage", "about:blank"); + + return new FirefoxDriver(profile); + } + + public IWebDriver StartChromeBrowser() + { + var options = new ChromeOptions(); + + // Add the WebDriver proxy capability. + if (Config.settings.httpProxy.useProxy) + { + var proxy = new OpenQA.Selenium.Proxy(); + proxy.HttpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.SslProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.FtpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + options.Proxy = proxy; + } + + if (!string.IsNullOrEmpty(Config.settings.browserSettings.UserAgent)) + { + options.AddArgument($"--user-agent={Config.settings.browserSettings.UserAgent}"); + } + + return new ChromeDriver(options); + } + + public IWebDriver StartIEBrowser() + { + var options = new InternetExplorerOptions(); + options.IntroduceInstabilityByIgnoringProtectedModeSettings = true; + options.IgnoreZoomLevel = true; + if (Config.settings.httpProxy.useProxy) + { + var proxy = new OpenQA.Selenium.Proxy(); + proxy.HttpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.SslProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.FtpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + options.Proxy = proxy; + options.UsePerProcessProxy = true; + } + + return new InternetExplorerDriver(options); + } + + public IWebDriver StartEdgeBrowser() + { + var options = new EdgeOptions(); + return new EdgeDriver(options); + } + + public IWebDriver StartPhantomJSBrowser() + { + var options = new PhantomJSOptions(); + + if (Config.settings.httpProxy.useProxy) + { + var proxy = new OpenQA.Selenium.Proxy(); + proxy.HttpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.SslProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.FtpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + options.AddAdditionalCapability("proxy", proxy); + } + + return new PhantomJSDriver(options); + } + + public IWebDriver StartSafariBrowser() + { + var options = new SafariOptions(); + + // Add the WebDriver proxy capability. + if (Config.settings.httpProxy.useProxy) + { + var proxy = new OpenQA.Selenium.Proxy(); + proxy.HttpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.SslProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + proxy.FtpProxy = Config.settings.httpProxy.proxyUrl + ":" + TestBase.proxy.proxyPort; + options.AddAdditionalCapability("proxy", proxy); + } + + return new SafariDriver(options); + } + + public DesiredCapabilities GetCapabilitiesForBrowser(Browser browser) + { + switch (browser) + { + case Browser.PhantomJS: + return DesiredCapabilities.PhantomJS(); + case Browser.IE: + return DesiredCapabilities.InternetExplorer(); + case Browser.Edge: + return DesiredCapabilities.Edge(); + case Browser.Chrome: + return DesiredCapabilities.Chrome(); + case Browser.Safari: + return DesiredCapabilities.Safari(); + case Browser.Android: + return DesiredCapabilities.Android(); + case Browser.IPhone: + var capabilities = DesiredCapabilities.IPhone(); + capabilities.SetCapability("device", "iphone"); + capabilities.SetCapability("app", "safari"); + return capabilities; + case Browser.IPad: + return DesiredCapabilities.IPad(); + case Browser.Firefox: + default: + return DesiredCapabilities.Firefox(); + } + } + + public IWebDriver LaunchRemoteBrowser(Browser browser, string host) + { + String URIStr = null; + + if (Config.settings.sauceLabsSettings.UseSauceLabs) + { + + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("username", Config.settings.sauceLabsSettings.SauceLabsUsername); + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("accessKey", Config.settings.sauceLabsSettings.SauceLabsAPIKey); + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("name", TestContext.CurrentContext.Test.FullName); + Log.Message(string.Format("Starting {0}:{1} browser on SauceLabs : {2}", browser, + WebDriverTestBase.testData.browserInfo.capabilities, + Config.settings.sauceLabsSettings.SauceLabsUrl)); + var sauceLabs = new Uri(Config.settings.sauceLabsSettings.SauceLabsUrl); + driver = new RemoteWebDriver(sauceLabs, WebDriverTestBase.testData.browserInfo.capabilities); + driver.Manage().Cookies.DeleteAllCookies(); + SetBrowserSize(); + return new EventedWebDriver(driver).driver; + } + + if (host.Equals(Config.settings.browserStackSettings.BrowserStackRemoteURL)) + { + var user = Config.settings.browserStackSettings.BrowserStack_User; + var key = Config.settings.browserStackSettings.BrowserStack_Key; + var os = Config.settings.browserStackSettings.BrowserStack_OS; + var os_version = Config.settings.browserStackSettings.BrowserStack_OS_Version; + + if (user == null) + { + throw new ConfigurationErrorsException( + "Framework configured to use BrowserStack, however 'BrowserStack_User' is not defined in App.config"); + } + if (key == null) + { + throw new ConfigurationErrorsException( + "Framework configured to use BrowserStack, however 'BrowserStack_Key' is not defined in App.config"); + } + if (os == null) + { + throw new ConfigurationErrorsException( + "Framework configured to use BrowserStack, however 'BrowserStack_OS' is not defined in App.config"); + } + if (os_version == null) + { + throw new ConfigurationErrorsException( + "Framework configured to use BrowserStack, however 'BrowserStack_OS_Version' is not defined in App.config"); + } + + // Browser stack does not require a remote host port + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("browserstack.user", user); + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("browserstack.key", key); + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("os", os); + WebDriverTestBase.testData.browserInfo.capabilities.SetCapability("os_version", os_version); + + URIStr = string.Format("http://{0}/wd/hub", host); + Log.Message(string.Format("Starting {0} browser on host : {1}", browser, host)); + } + else + { + URIStr = string.Format("http://{0}:{1}/wd/hub", host, Config.settings.runTimeSettings.RemoteHostPort); + Log.Message(string.Format("Starting {0} browser on host : {1}:{2}", browser, host, + Config.settings.runTimeSettings.RemoteHostPort)); + } + + var remoteAddress = new Uri(URIStr); + driver = new ScreenshotRemoteWebDriver(remoteAddress, + WebDriverTestBase.testData.browserInfo.capabilities); + driver.Manage().Cookies.DeleteAllCookies(); + SetBrowserSize(); + return new EventedWebDriver(driver).driver; + } + } +} \ No newline at end of file diff --git a/WebDriver/WebDriverExtensions.cs b/WebDriver/WebDriverExtensions.cs new file mode 100644 index 0000000..d0089e7 --- /dev/null +++ b/WebDriver/WebDriverExtensions.cs @@ -0,0 +1,568 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; +using OpenQA.Selenium.Internal; +using OpenQA.Selenium.Support.Extensions; +using OpenQA.Selenium.Support.UI; +using Golem.Core; + +namespace Golem.WebDriver +{ + /// + /// Extension methods added to the IWebDriver and IWebElement API's + /// + public static class WebDriverExtensions + { + public static ElementVerification Verify(this IWebElement element, int timeout = -1) + { + try + { + if (timeout == -1) + timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var GolemElement = (Element) element; + return new ElementVerification(GolemElement, timeout, false); + } + catch (Exception) + { + return new ElementVerification(new Element(element), Config.settings.runTimeSettings.ElementTimeoutSec, + false); + } + } + + public static ElementVerification WaitUntil(this IWebElement element, int timeout = -1) + { + try + { + if (timeout == -1) + timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var GolemElement = (Element) element; + return new ElementVerification(GolemElement, timeout, true); + } + catch (Exception) + { + return new ElementVerification(new Element(element), Config.settings.runTimeSettings.ElementTimeoutSec, + false); + } + } + + public static IWebElement Hide(this IWebElement element) + { + return + (IWebElement) + WebDriverTestBase.driver.ExecuteJavaScript("arguments[0].style.visibility='hidden';return;", element); + } + + public static IWebElement Show(this IWebElement element) + { + return + (IWebElement) + WebDriverTestBase.driver.ExecuteJavaScript("arguments[0].style.visibility='visible';return;", + element); + } + + public static IWebElement FindInSiblings(this IWebElement element, By by) + { + return element.GetParent().FindElement(by); + } + + public static IWebElement FindInChildren(this IWebElement element, By by) + { + return element.FindElement(by); + } + + public static IWebElement GetParent(this IWebElement element) + { + var driver = WebDriverTestBase.driver; + return element.FindElement(By.XPath("..")); + } + + public static string GetHtml(this IWebElement element) + { + try + { + return element.GetAttribute("outerHTML"); + } + catch + { + return ""; + } + } + + public static string GetHtml(this IWebElement element, int length) + { + try + { + var html = element.GetAttribute("innerHTML").Replace("\r\n", ""); + if (html.Length <= length) + return html; + var halfLength = length/2; + var start = html.Substring(0, halfLength); + var end = html.Substring((html.Length - halfLength), halfLength); + return string.Format("{0}...{1}", start, end); + } + catch (Exception e) + { + return "HTML Not found " + e.Message; + } + } + + public static bool IsStale(this IWebElement element) + { + try + { + if (element == null) + return true; + var enabled = element.Enabled; + return false; + } + catch (Exception e) + { + return true; + } + } + + public static void Highlight(this IWebElement element, int ms = 30, string color="yellow") + { + try + { + var jsDriver = ((IJavaScriptExecutor) ((IWrapsDriver) element).WrappedDriver); + var originalElementBorder = (string) jsDriver.ExecuteScript("return arguments[0].style.background", element); + jsDriver.ExecuteScript(string.Format("arguments[0].style.background='{0}'; return;", color), element); + if (ms >= 0) + { + if (ms > 1000) + { + var bw = new BackgroundWorker(); + bw.DoWork += (obj, e) => Unhighlight(element, originalElementBorder, ms); + bw.RunWorkerAsync(); + } + else + { + Unhighlight(element, originalElementBorder, ms); + } + } + } + catch (Exception e) + { + Log.Warning(e.Message); + } + } + + public static void WaitForJQuery(this IWebDriver driver, int timeout=-1) + { + if (timeout == -1) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var value = driver.ExecuteJavaScript("(typeof jQuery === \"undefined\" || jQuery.active==0);"); + bool condition = (bool) value; + if (condition == true) + { + return; + } + else + { + Log.Message("Waiting for JQuery"); + Common.Delay(1000); + } + + } + + } + + private static void Unhighlight(IWebElement element, string border, int timeMs) + { + try + { + var jsDriver = ((IJavaScriptExecutor) ((IWrapsDriver) element).WrappedDriver); + Thread.Sleep(timeMs); + jsDriver.ExecuteScript("arguments[0].style.background='" + border + "'; return;", element); + } + catch (Exception e) + { + } + } + + /// + /// Clear a checked element (radio or checkbox) + /// + /// + public static void ClearChecked(this IWebElement element) + { + var jsDriver = ((IJavaScriptExecutor) ((IWrapsDriver) element)); + jsDriver.ExecuteScript("arguments[0].checked=false;", element); + } + + public static void MouseOver(this IWebElement element) + { + var driver = WebDriverTestBase.driver; + var action = new Actions(driver).MoveToElement(element); + Thread.Sleep(2000); + action.Build().Perform(); + } + + public static IWebElement WaitForPresent(this IWebElement element, By by, int timeout = 0, string elementName = "Element") + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var eles = element.FindElements(by); + if (eles.Count > 0) + { + var ele = eles.FirstOrDefault(x => x.Displayed); + if (ele == null) + { + return eles[0]; + } + else + { + return ele; + } + } + Common.Delay(1000); + } + throw new NoSuchElementException(string.Format("Element ({0}) was not present after {1} seconds", + @by, timeout)); + } + + public static IWebElement WaitForPresent(this IWebDriver driver, By by, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var eles = driver.FindElements(by); + if (eles.Count > 0) + { + var ele = eles.FirstOrDefault(x => x.Displayed); + if (ele == null) + { + return eles[0]; + } + else + { + return ele; + } + } + + + Common.Delay(1000); + } + throw new NoSuchElementException(string.Format("Element ({0}) was not present after {1} seconds", + @by, timeout)); + } + + public static void WaitForNotPresent(this IWebDriver driver, By by, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var eles = driver.FindElements(by); + if (eles.Count == 0) + return; + Common.Delay(1000); + } + throw new InvalidElementStateException(string.Format("Element ({0}) was still present after {1} seconds", + @by, timeout)); + } + + public static IWebElement WaitForVisible(this IWebDriver driver, By by, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var eles = driver.FindElements(by).Where(x => x.Displayed).ToList(); + if (eles.Count > 0) + return eles.FirstOrDefault(x => x.Displayed); + Common.Delay(1000); + } + throw new ElementNotVisibleException(string.Format("Element ({0}) was not visible after {1} seconds", + @by, timeout)); + } + + public static void WaitForNotVisible(this IWebDriver driver, OpenQA.Selenium.By by, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + var then = DateTime.Now.AddSeconds(timeout); + for (var now = DateTime.Now; now < then; now = DateTime.Now) + { + var eles = driver.FindElements(by).Where(x => x.Displayed).ToList(); + if (eles.Count == 0) + return; + Common.Delay(1000); + } + throw new ElementNotVisibleException(string.Format("Element ({0}) was still visible after {1} seconds", + @by, timeout)); + } + + public static IWebElement FindElementWithText(this IWebDriver driver, string text) + { + return driver.FindElement(By.XPath("//*[text()=\"" + text + "\"]")); + } + + public static IWebElement WaitForElementWithText(this IWebDriver driver, string text, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.ElementTimeoutSec; + return driver.WaitForPresent(By.XPath("//*[text()=\"" + text + "\"]")); + } + + public static void VerifyElementPresent(this IWebDriver driver, OpenQA.Selenium.By by, bool isPresent = true) + { + var count = driver.FindElements(by).Count; + Verify(isPresent && count == 0, "VerifyElementPresent Failed : Element : " + @by + + (isPresent ? " found" : " not found")); + } + + public static void Verify(bool condition, string message) + { + if (!condition) + { + TestBase.AddVerificationError(message); + } + else + { +// TestContext.CurrentContext.IncrementAssertCount(); + } + } + + public static IWebElement SelectOption(this IWebElement element, string option) + { + new SelectElement(element).SelectByText(option); + return element; + } + + public static IWebElement SelectOptionByPartialText(this IWebElement element, string text) + { + var s_element = new SelectElement(element); + + foreach (var option in s_element.Options.Where(option => option.Text.Contains(text))) + { + option.Click(); + break; + } + + return element; + } + + public static IWebElement FindVisibleElement(this IWebDriver driver, OpenQA.Selenium.By by) + { + var elements = driver.FindElements(by); + foreach (var ele in elements.Where(ele => (ele.Displayed) && (ele.Enabled))) + { + return ele; + } + throw new ElementNotVisibleException("No element visible for : " + @by); + } + + public static Rectangle GetRect(this IWebElement element) + { + try + { + var jsDriver = ((IJavaScriptExecutor) WebDriverTestBase.driver); + var originalElementBorder = (string) jsDriver.ExecuteScript("return arguments[0].style.border", element); + return (Rectangle) jsDriver.ExecuteScript("return arguments[0].getBoundingClientRect();", element); + } + catch (Exception) + { + return new Rectangle(); + } + } + + public static string GetScreenshot(this IWebDriver driver, string path) + { + driver.TakeScreenshot().SaveAsFile(path, ImageFormat.Png); + return path; + } + + public static Image GetScreenshot(this IWebDriver driver) + { + Image screen_shot = null; + + try + { + if (driver == null) return screen_shot; + var ss = ((ITakesScreenshot) driver).GetScreenshot(); + var ms = new MemoryStream(ss.AsByteArray); + screen_shot = Image.FromStream(ms); + ms.Dispose(); + } + catch (Exception e) + { + Log.Error("Failed to take screenshot: " + e.Message); + } + + return screen_shot; + } + + public static void SetText(this IWebElement element, string text) + { + element.Clear(); + element.SendKeys(text); + if (element.GetAttribute("value") != text) + { + element.Clear(); + element.SendKeys(text); + } + } + + + public static object ExecuteJavaScript(this IWebDriver driver, string script) + { + try + { + var js = (IJavaScriptExecutor) driver; + return js.ExecuteScript("return " + script); + } + catch (Exception) + { + return null; + } + } + + public static object ExecuteJavaScript(this IWebDriver driver, string script, params object[] args) + { + var js = (IJavaScriptExecutor) driver; + return js.ExecuteScript(script, args); + } + + public static void JavaWindowScroll(this IWebDriver driver, int xCord, int yCord) + { + var js = (IJavaScriptExecutor) driver; + js.ExecuteScript("window.scrollBy(" + xCord + "," + yCord + ");"); + } + + public static IWebElement ScrollIntoView(this IWebElement element) + { + var js = (IJavaScriptExecutor) WebDriverTestBase.driver; + js.ExecuteScript("arguments[0].scrollIntoView(); return;", element); + return element; + } + + public static IWebElement JS_Click(this IWebElement element) + { + var js = (IJavaScriptExecutor)WebDriverTestBase.driver; + js.ExecuteScript("arguments[0].click(); return;", element); + return element; + } + + public static IWebElement ClickAt(this IWebElement element) + { + var js = (IJavaScriptExecutor)WebDriverTestBase.driver; + js.ExecuteScript($"document.elementFromPoint({element.Location.X}, {element.Location.Y}).click(); return;", element); + return element; + } + + public static void SelectNewWindow(this IWebDriver driver, int timeout = 0) + { + if (timeout == 0) timeout = Config.settings.runTimeSettings.OpenWindowTimeoutSec; + + try + { + var currentHandle = driver.CurrentWindowHandle; + var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeout)); + wait.Until(d => (driver.WindowHandles.Count() > 1)); + + foreach (var handle in (driver.WindowHandles)) + { + if (handle != currentHandle) + { + driver.SwitchTo().Window(handle); + } + } + } + catch (Exception) + { + } + } + + public static void SetOrientation(this IWebDriver driver, ScreenOrientation orientation) + { + ((IRotatable) driver).Orientation = orientation; + } + + public static void SetBrowserSize(this IWebDriver driver, Size size) + { + driver.Manage().Window.Size = size; + } + + public static IWebDriver GetWrappedDriver(this IWebElement element) + { + return ((IWrapsDriver) element).WrappedDriver; + } + + public static IWebElement DragToOffset(this IWebElement element, int x, int y) + { + var action = new Actions(WebDriverTestBase.driver); + action.DragAndDropToOffset(element, x, y).Build().Perform(); + return element; + } + + public static Image GetImage(this IWebElement element) + { + var size = new Size(element.Size.Width, element.Size.Height); + if (element.Displayed == false || element.Location.X < 0 || element.Location.Y < 0) + { + throw new BadImageFormatException(string.Format( + "Could not create image for element as it is hidden")); + } + var cropRect = new Rectangle(element.Location, size); + using (var screenShot = TestBase.testData.driver.GetScreenshot()) + { + if (cropRect.X < 0) + { + cropRect.X = 0; + } + if (cropRect.Y < 0) + { + cropRect.Y = 0; + } + if (cropRect.X + cropRect.Width > screenShot.Width) + { + cropRect.Width = screenShot.Width - cropRect.X; + } + if (cropRect.Y + cropRect.Height > screenShot.Height) + { + cropRect.Height = screenShot.Height - cropRect.Y; + } + + try + { + using (var bmpImage = new Bitmap(screenShot)) + { + using (var bmpCrop = bmpImage.Clone(cropRect, bmpImage.PixelFormat)) + { + return bmpCrop; + } + + } + + } + catch (Exception e) + { + return null; + } + } + } + + public static IWebElement ClickWithOffset(this IWebElement element, int x, int y) + { + var action = new Actions(WebDriverTestBase.driver); + action.MoveToElement(element, x, y).Click().Build().Perform(); + return element; + } + + public static void Sleep(this IWebDriver driver, int timeoutMs) + { + Thread.Sleep(timeoutMs); + } + } +} \ No newline at end of file diff --git a/WebDriver/WebDriverHost.cs b/WebDriver/WebDriverHost.cs new file mode 100644 index 0000000..898212c --- /dev/null +++ b/WebDriver/WebDriverHost.cs @@ -0,0 +1,37 @@ +using OpenQA.Selenium.Remote; + +namespace Golem.WebDriver +{ + public class BrowserInfo + { + public WebDriverBrowser.Browser browser; + public DesiredCapabilities capabilities = new DesiredCapabilities(); + + public BrowserInfo(WebDriverBrowser.Browser browser, string capabilities) + { + this.browser = browser; + this.capabilities = GetCapsFromString(capabilities); + } + + public BrowserInfo(WebDriverBrowser.Browser browser) + { + this.browser = browser; + this.capabilities = new WebDriverBrowser().GetCapabilitiesForBrowser(browser); + } + + public DesiredCapabilities GetCapsFromString(string caps) { + DesiredCapabilities endCaps = new WebDriverBrowser().GetCapabilitiesForBrowser(browser); + if(caps=="null") return endCaps; + string[] capabilities = caps.Split(','); + foreach (var cap in capabilities) + { + string[] toks = cap.Split('='); + string key = toks[0]; + string value = toks[1]; + endCaps.SetCapability(key,value); + } + return endCaps; + + } + } +} \ No newline at end of file diff --git a/WebDriver/WebDriverRecorder.cs b/WebDriver/WebDriverRecorder.cs new file mode 100644 index 0000000..b1537d6 --- /dev/null +++ b/WebDriver/WebDriverRecorder.cs @@ -0,0 +1,74 @@ +using System.ComponentModel; +using System.Drawing; +using System.Threading; +using Gallio.Common.Media; +using Golem.Core; +using Timer = System.Timers.Timer; + +namespace Golem.WebDriver +{ + public class WebDriverRecorder + { + public int fps; + public int frameDelayMs; + public Bitmap lastImage; + public BackgroundWorker screenshotGetter; + public Size screensize; + public int ticks; + public Timer timer; + public Video video; + public BackgroundWorker videoBuilder; + + public WebDriverRecorder(int fps) + { + this.fps = fps; + if (Config.settings.runTimeSettings.Browser == WebDriverBrowser.Browser.Android) + { + screensize = new Size(300, 500); + } + else + { + screensize = new Size(1024, 768); + } + + frameDelayMs = 1000/fps; + //Log.Message("The current dimensions are : " + screensize.Width + " x " + screensize.Height); + video = new FlashScreenVideo(new FlashScreenVideoParameters(screensize.Width, screensize.Height, fps)); + + screenshotGetter = new BackgroundWorker(); + screenshotGetter.DoWork += screenshotGetter_DoWork; + screenshotGetter.WorkerSupportsCancellation = true; + screenshotGetter.RunWorkerAsync(); + + videoBuilder = new BackgroundWorker(); + videoBuilder.DoWork += videoBuilder_DoWork; + videoBuilder.WorkerSupportsCancellation = true; + videoBuilder.RunWorkerAsync(); + } + + private void screenshotGetter_DoWork(object sender, DoWorkEventArgs e) + { + while (!screenshotGetter.CancellationPending) + { + var bitmap = new Bitmap(TestBase.testData.driver.GetScreenshot()); + lastImage = new Bitmap(Common.ResizeImage(bitmap, screensize.Width, screensize.Height)); + } + } + + private void videoBuilder_DoWork(object sender, DoWorkEventArgs e) + { + while (!videoBuilder.CancellationPending) + { + if (lastImage != null) + video.AddFrame(new BitmapVideoFrame(lastImage)); + Thread.Sleep(frameDelayMs); + } + } + + public void Stop() + { + screenshotGetter.CancelAsync(); + videoBuilder.CancelAsync(); + } + } +} \ No newline at end of file diff --git a/WebDriver/WebDriverTestBase.cs b/WebDriver/WebDriverTestBase.cs new file mode 100644 index 0000000..12a39e7 --- /dev/null +++ b/WebDriver/WebDriverTestBase.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Edge; +using Golem.Core; +using Golem.WebDriver; +using TestContext = NUnit.Framework.TestContext; + +namespace Golem +{ + + /// + /// This class should be inherited by all webdriver tests. It will automatically launch a browser and include the + /// Driver object in each test. + /// + public class WebDriverTestBase : TestBase + { + + public WebDriverTestBase(BrowserInfo browser) + { + this.browserInfo = browser; + } + + public WebDriverTestBase() : base() + { + + } + + protected static Object browserLocker = new object(); + protected BrowserInfo browserInfo = new BrowserInfo(Config.settings.runTimeSettings.Browser); + + public static IWebDriver driver + { + get { return testData.driver; } + set { testData.driver = value; } + } + + public WebDriverBrowser.Browser browser + { + get { return browserInfo.browser; } + set { browserInfo.browser = value; } + } + + protected static IEnumerable GetBrowsers() + { + + return Config.settings.runTimeSettings.Browsers; + } + + public static T OpenPage(string url) + { + driver.Navigate().GoToUrl(url); + //navigate twice due to IE bug + if (Config.settings.runTimeSettings.Browser == WebDriverBrowser.Browser.IE) + driver.Navigate().GoToUrl(url); + return (T) Activator.CreateInstance(typeof (T)); + } + + /// + /// Take a screenshot and embed it within the TestLog. + /// + /// Associate a message with the screenshot (optional) + public static void LogScreenShot(string message = null) + { + var screenshot = testData.driver.GetScreenshot(); + if (screenshot != null) + { + if (message != null) Log.Message("!------- " + message + " --------!"); + + Log.Image(screenshot); + } + } + + private void LogScreenshotIfTestFailed() + { + if ((Config.settings.reportSettings.screenshotOnError) && + (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)) + { + var screenshot = testData.driver.GetScreenshot(); + if (screenshot != null) + { + var path = Log.Image(screenshot); + testData.ScreenshotPath = path; + } + } + } + + public void LogHtmlIfTestFailed() + { + try + { + if ((Config.settings.reportSettings.htmlOnError) && (Common.GetTestOutcome() != TestStatus.Passed)) + { + var source = driver.PageSource; + Log.Html("HTML_" + Common.GetShortTestName(95), source); + } + } + catch (Exception e) + { + Log.Warning("Error caught trying to get page source: " + e.Message); + } + } + + public void QuitBrowser() + { + if (Config.settings.runTimeSettings.LaunchBrowser) + { + if (driver != null) + { + driver.Quit(); + driver = null; + Log.Message(browser + " Browser Closed"); + } + } + } + + public void LaunchBrowser() + { + lock (browserLocker) + { + if (Config.settings.runTimeSettings.LaunchBrowser) + { + if (Config.settings.runTimeSettings.RunOnRemoteHost) + { + driver = new WebDriverBrowser().LaunchRemoteBrowser(browser, + Config.settings.runTimeSettings.HostIp); + } + else + { + driver = new WebDriverBrowser().LaunchBrowser(browser); + } + + Log.Message(browser + " Browser Launched"); + testData.actions.addAction(Common.GetCurrentTestName() + " : " + browser + " Browser Launched"); + } + } + } + + [SetUp] + public virtual void SetUp() + { + testData.browserInfo = browserInfo; + LaunchBrowser(); + } + + [TearDown] + public override void TearDownTestBase() + { + UpdateSAuceLabsWithTestStatus(); + LogScreenshotIfTestFailed(); + LogHtmlIfTestFailed(); + QuitBrowser(); + base.TearDownTestBase(); + } + + private void UpdateSAuceLabsWithTestStatus() + { + if (Config.settings.sauceLabsSettings.UseSauceLabs) + { + var passed = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed; + driver.ExecuteJavaScript("sauce:job-result=" + (passed ? "passed" : "failed")); + } + } + } +} \ No newline at end of file diff --git a/chromedriver.exe b/chromedriver.exe new file mode 100644 index 0000000..1a20921 Binary files /dev/null and b/chromedriver.exe differ diff --git a/dashboard.css b/dashboard.css new file mode 100644 index 0000000..18e456c --- /dev/null +++ b/dashboard.css @@ -0,0 +1,135 @@ +/* + * Base structure + */ + +/* Move down content because we have a fixed navbar that is 50px tall */ +body { + padding-top: 50px; +} + + +/* + * Global add-ons + */ + +.sub-header { + padding-bottom: 10px; + padding-left: 10px; + border-bottom: 1px solid #eee; +} + +/* + * Top navigation + * Hide default border to remove 1px line. + */ +.navbar-fixed-top { + border: 0; +} + +/* + * Sidebar + */ + +/* Hide for mobile, show later */ +.sidebar { + display: none; +} +@media (min-width: 768px) { + .sidebar { + position: fixed; + top: 51px; + bottom: 0; + left: 0; + z-index: 1000; + display: block; + padding: 20px; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ + background-color: #f5f5f5; + border-right: 1px solid #eee; + } +} + +/* Sidebar navigation */ +.nav-sidebar { + margin-right: -21px; /* 20px padding + 1px border */ + margin-bottom: 20px; + margin-left: -20px; +} +.nav-sidebar > li > a { + padding-right: 20px; + padding-left: 20px; +} +.nav-sidebar > .active > a, +.nav-sidebar > .active > a:hover, +.nav-sidebar > .active > a:focus { + color: #fff; + background-color: #428bca; +} + + +/* + * Main content + */ + +.main { + padding: 20px; +} +@media (min-width: 768px) { + .main { + padding-right: 40px; + padding-left: 40px; + } +} +.main .page-header { + margin-top: 0; +} + + +/* + * Placeholder dashboard ideas + */ + +.placeholders { + margin-bottom: 30px; + text-align: center; +} +.placeholders h4 { + margin-bottom: 0; +} +.placeholder { + margin-bottom: 20px; +} +.placeholder img { + display: inline-block; + border-radius: 50%; +} +.log-table { + border: 1px; +} +.log-error { + background-color: red; +} +.Failed { + background-color: red; +} +.screenshot { + width: 50%; +} +.status { + margin: 5px; +} +.exception-message { + font-size: 120%; + margin: 5px; +} +.exception-stacktrace { + margin: 5px; +} +.log-video { + margin: 5px; +} +.log-timestamp { + width: 10%; +} + diff --git a/geckodriver.exe b/geckodriver.exe new file mode 100644 index 0000000..3cae106 Binary files /dev/null and b/geckodriver.exe differ diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..8f3bda9 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,33 @@ +param($installPath, $toolsPath, $package, $project) +Write-Host "InstallPath: $installPath" +Write-Host "ToolsPath: $toolsPath" +Write-Host "Package: $package" +Write-Host "Project: $project" + +$CopyChromeDriver = "`nxcopy /y `"`$(ProjectDir)chromedriver.exe`" `"`$(TargetDir)`"" +$CopyIEDriver = "`nxcopy /y `"`$(ProjectDir)IEDriverServer.exe`" `"`$(TargetDir)`"" +$CopyPhantomJSDriver = "`nxcopy /y `"`$(ProjectDir)phantomjs.exe`" `"`$(TargetDir)`"" +$CopyCSS = "`nxcopy /y `"`$(ProjectDir)dashboard.css`" `"`$(TargetDir)`"" +$CopyServer = "`nxcopy /y `"`$(ProjectDir)selenium-server-standalone.jar`" `"`$(TargetDir)`"" + +# Get the current Post Build Event cmd +$currentPostBuildCmd = $project.Properties.Item("PostBuildEvent").Value + +# Append our post build command if it's not already there +if (!$currentPostBuildCmd.Contains($CopyChromeDriver)) { + $project.Properties.Item("PostBuildEvent").Value += $CopyChromeDriver +} +if (!$currentPostBuildCmd.Contains($CopyPhantomJSDriver)) { + $project.Properties.Item("PostBuildEvent").Value += $CopyPhantomJSDriver +} +if (!$currentPostBuildCmd.Contains($CopyIEDriver)) { + $project.Properties.Item("PostBuildEvent").Value += $CopyIEDriver +} +if (!$currentPostBuildCmd.Contains($CopyCSS)) { + $project.Properties.Item("PostBuildEvent").Value += $CopyCSS +} +if (!$currentPostBuildCmd.Contains($CopyServer)) { + $project.Properties.Item("PostBuildEvent").Value += $CopyServer +} +$mobFile = $project.ProjectItems.Item("Proxy").ProjectItems.Item("browsermob-proxy-2.1.0-beta-6-bin.zip") +$mobFile.Properties.Item("CopyToOutputDirectory").Value = 2 \ No newline at end of file diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..4f1226b --- /dev/null +++ b/packages.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phantomjs.exe b/phantomjs.exe new file mode 100644 index 0000000..c644886 Binary files /dev/null and b/phantomjs.exe differ diff --git a/selenium-server-standalone.jar b/selenium-server-standalone.jar new file mode 100644 index 0000000..4cd6585 Binary files /dev/null and b/selenium-server-standalone.jar differ