├── .gitignore ├── Framework ├── FW.cs ├── Framework.csproj ├── FwConfig.cs ├── Logging │ └── Logger.cs ├── Models │ ├── Card.cs │ ├── IceSpiritCard.cs │ └── MirrorCard.cs ├── Selenium │ ├── Driver.cs │ ├── DriverFactory.cs │ ├── Element.cs │ ├── Elements.cs │ ├── Wait.cs │ ├── WaitConditions.cs │ └── Window.cs └── Services │ ├── ApiCardService.cs │ ├── ICardService.cs │ └── InMemoryCardService.cs ├── README.md ├── Royale.Tests ├── Base │ └── TestBase.cs ├── CardTests.cs ├── CopyDeckTests.cs └── Royale.Tests.csproj ├── Royale ├── Pages │ ├── CardDetailsPage.cs │ ├── CardsPage.cs │ ├── CopyDeckPage.cs │ ├── DeckBuilderPage.cs │ ├── HeaderNav.cs │ ├── PageBase.cs │ └── Pages.cs └── Royale.csproj ├── StatsRoyale.sln ├── _drivers └── chromedriver └── framework-config.json /.gitignore: -------------------------------------------------------------------------------- 1 | # The following command works for downloading when using Git for Windows: 2 | # curl -LOf http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 3 | # 4 | # Download this file using PowerShell v3 under Windows with the following comand: 5 | # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore 6 | # 7 | # or wget: 8 | # wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.sln.docstates 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | x64/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | # build folder is nowadays used for build scripts and should not be ignored 22 | #build/ 23 | 24 | # NuGet Packages 25 | *.nupkg 26 | # The packages folder can be ignored because of Package Restore 27 | **/packages/* 28 | # except build/, which is used as an MSBuild target. 29 | !**/packages/build/ 30 | # Uncomment if necessary however generally it will be regenerated when needed 31 | #!**/packages/repositories.config 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | *_i.c 38 | *_p.c 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.log 59 | *.scc 60 | 61 | # OS generated files # 62 | .DS_Store* 63 | Icon? 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # Guidance Automation Toolkit 79 | *.gpState 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper*/ 83 | *.[Rr]e[Ss]harper 84 | 85 | # TeamCity is a build add-in 86 | _TeamCity* 87 | 88 | # DotCover is a Code Coverage Tool 89 | *.dotCover 90 | 91 | # NCrunch 92 | *.ncrunch* 93 | .*crunch*.local.xml 94 | 95 | # Installshield output folder 96 | [Ee]xpress/ 97 | 98 | # DocProject is a documentation generator add-in 99 | DocProject/buildhelp/ 100 | DocProject/Help/*.HxT 101 | DocProject/Help/*.HxC 102 | DocProject/Help/*.hhc 103 | DocProject/Help/*.hhk 104 | DocProject/Help/*.hhp 105 | DocProject/Help/Html2 106 | DocProject/Help/html 107 | 108 | # Click-Once directory 109 | publish/ 110 | 111 | # Publish Web Output 112 | *.Publish.xml 113 | 114 | # Windows Azure Build Output 115 | csx 116 | *.build.csdef 117 | 118 | # Windows Store app package directory 119 | AppPackages/ 120 | 121 | # Others 122 | *.Cache 123 | ClientBin/ 124 | [Ss]tyle[Cc]op.* 125 | ~$* 126 | *~ 127 | *.dbmdl 128 | *.[Pp]ublish.xml 129 | *.pfx 130 | *.publishsettings 131 | modulesbin/ 132 | tempbin/ 133 | 134 | # EPiServer Site file (VPP) 135 | AppData/ 136 | 137 | # RIA/Silverlight projects 138 | Generated_Code/ 139 | 140 | # Backup & report files from converting an old project file to a newer 141 | # Visual Studio version. Backup files are not needed, because we have git ;-) 142 | _UpgradeReport_Files/ 143 | Backup*/ 144 | UpgradeLog*.XML 145 | UpgradeLog*.htm 146 | 147 | # vim 148 | *.txt~ 149 | *.swp 150 | *.swo 151 | 152 | # Temp files when opening LibreOffice on ubuntu 153 | .~lock.* 154 | 155 | # svn 156 | .svn 157 | 158 | # CVS - Source Control 159 | **/CVS/ 160 | 161 | # Remainings from resolving conflicts in Source Control 162 | *.orig 163 | 164 | # SQL Server files 165 | **/App_Data/*.mdf 166 | **/App_Data/*.ldf 167 | **/App_Data/*.sdf 168 | 169 | 170 | #LightSwitch generated files 171 | GeneratedArtifacts/ 172 | _Pvt_Extensions/ 173 | ModelManifest.xml 174 | 175 | # ========================= 176 | # Windows detritus 177 | # ========================= 178 | 179 | # Windows image file caches 180 | Thumbs.db 181 | ehthumbs.db 182 | 183 | # Folder config file 184 | Desktop.ini 185 | 186 | # Recycle Bin used on file shares 187 | $RECYCLE.BIN/ 188 | 189 | # Mac desktop service store files 190 | .DS_Store 191 | 192 | # SASS Compiler cache 193 | .sass-cache 194 | 195 | # Visual Studio 2014 CTP 196 | **/*.sln.ide 197 | 198 | # Visual Studio temp something 199 | .vs/ 200 | 201 | # dotnet stuff 202 | project.lock.json 203 | 204 | # VS 2015+ 205 | *.vc.vc.opendb 206 | *.vc.db 207 | 208 | # Rider 209 | .idea/ 210 | 211 | # Visual Studio Code 212 | .vscode/ 213 | 214 | # Output folder used by Webpack or other FE stuff 215 | **/node_modules/* 216 | **/wwwroot/* 217 | 218 | # SpecFlow specific 219 | *.feature.cs 220 | *.feature.xlsx.* 221 | *.Specs_*.html 222 | 223 | ##### 224 | # End of core ignore list, below put you custom 'per project' settings (patterns or path) 225 | ##### 226 | -------------------------------------------------------------------------------- /Framework/FW.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Framework.Logging; 4 | using Newtonsoft.Json; 5 | using NUnit.Framework; 6 | 7 | namespace Framework 8 | { 9 | public class FW 10 | { 11 | public static string WORKSPACE_DIRECTORY = Path.GetFullPath(@"../../../../"); 12 | 13 | public static Logger Log => _logger ?? throw new NullReferenceException("_logger is null. SetLogger() first."); 14 | 15 | public static FwConfig Config => _configuration ?? throw new NullReferenceException("Config is null. Call FW.SetConfig() first."); 16 | 17 | [ThreadStatic] 18 | public static DirectoryInfo CurrentTestDirectory; 19 | 20 | [ThreadStatic] 21 | private static Logger _logger; 22 | 23 | private static FwConfig _configuration; 24 | 25 | public static DirectoryInfo CreateTestResultsDirectory() 26 | { 27 | var testDirectory = WORKSPACE_DIRECTORY + "TestResults"; 28 | 29 | if (Directory.Exists(testDirectory)) 30 | { 31 | Directory.Delete(testDirectory, recursive: true); 32 | } 33 | 34 | return Directory.CreateDirectory(testDirectory); 35 | } 36 | 37 | public static void SetConfig() 38 | { 39 | if (_configuration == null) 40 | { 41 | var jsonStr = File.ReadAllText(WORKSPACE_DIRECTORY + "/framework-config.json"); 42 | _configuration = JsonConvert.DeserializeObject(jsonStr); 43 | } 44 | } 45 | 46 | public static void SetLogger() 47 | { 48 | lock (_setLoggerLock) 49 | { 50 | var testResultsDir = WORKSPACE_DIRECTORY + "TestResults"; 51 | var testName = TestContext.CurrentContext.Test.Name; 52 | var fullPath = $"{testResultsDir}/{testName}"; 53 | 54 | if (Directory.Exists(fullPath)) 55 | { 56 | CurrentTestDirectory = Directory.CreateDirectory(fullPath + TestContext.CurrentContext.Test.ID); 57 | } 58 | else 59 | { 60 | CurrentTestDirectory = Directory.CreateDirectory(fullPath); 61 | } 62 | 63 | _logger = new Logger(testName, CurrentTestDirectory.FullName + "/log.txt"); 64 | } 65 | } 66 | 67 | private static object _setLoggerLock = new object(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Framework/Framework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Framework/FwConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Framework 2 | { 3 | public class FwConfig 4 | { 5 | public DriverSettings Driver { get; set; } 6 | 7 | public TestSettings Test { get; set; } 8 | } 9 | 10 | public class DriverSettings 11 | { 12 | public string Browser { get; set; } 13 | 14 | public int WaitSeconds { get; set; } 15 | } 16 | 17 | public class TestSettings 18 | { 19 | public string Url { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Framework/Logging/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Framework.Logging 5 | { 6 | public class Logger 7 | { 8 | private readonly string _filepath; 9 | 10 | public Logger(string testName, string filepath) 11 | { 12 | _filepath = filepath; 13 | 14 | using (var log = File.CreateText(_filepath)) 15 | { 16 | log.WriteLine($"Starting timestamp: {DateTime.Now.ToLocalTime()}"); 17 | log.WriteLine($"Test: {testName}"); 18 | } 19 | } 20 | 21 | public void Info(string message) 22 | { 23 | WriteLine($"[INFO]: {message}"); 24 | } 25 | 26 | public void Step(string message) 27 | { 28 | WriteLine($" [STEP]: {message}"); 29 | } 30 | 31 | public void Warning(string message) 32 | { 33 | WriteLine($"[WARNING]: {message}"); 34 | } 35 | 36 | public void Error(string message) 37 | { 38 | WriteLine($"[ERROR]: {message}"); 39 | } 40 | 41 | public void Fatal(string message) 42 | { 43 | WriteLine($"[FATAL]: {message}"); 44 | } 45 | 46 | private void WriteLine(string text) 47 | { 48 | using (var log = File.AppendText(_filepath)) 49 | { 50 | log.WriteLine(text); 51 | } 52 | } 53 | 54 | private void Write(string text) 55 | { 56 | using (var log = File.AppendText(_filepath)) 57 | { 58 | log.Write(text); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Framework/Models/Card.cs: -------------------------------------------------------------------------------- 1 | namespace Framework.Models 2 | { 3 | public class Card 4 | { 5 | public virtual string Id { get; set; } 6 | 7 | public virtual string Name { get; set; } 8 | 9 | public virtual string Icon { get; set; } 10 | 11 | public virtual int Cost { get; set; } 12 | 13 | public virtual string Rarity { get; set; } 14 | 15 | public virtual string Type { get; set; } 16 | 17 | public virtual int Arena { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Framework/Models/IceSpiritCard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Framework.Models 4 | { 5 | public class IceSpiritCard : Card 6 | { 7 | public override string Name { get; set; } = "Ice Spirit"; 8 | 9 | public override int Cost { get; set; } = 1; 10 | 11 | public override string Rarity { get; set; } = "Common"; 12 | 13 | public override string Type { get; set; } = "Troop"; 14 | 15 | public override int Arena { get; set; } = 8; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Framework/Models/MirrorCard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Framework.Models 4 | { 5 | public class MirrorCard : Card 6 | { 7 | public override string Name { get; set; } = "Mirror"; 8 | 9 | public override int Cost { get; set; } = 1; 10 | 11 | public override string Rarity { get; set; } = "Epic"; 12 | 13 | public override string Type { get; set; } = "Spell"; 14 | 15 | public override int Arena { get; set; } = 12; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Framework/Selenium/Driver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using OpenQA.Selenium; 4 | using OpenQA.Selenium.Chrome; 5 | 6 | namespace Framework.Selenium 7 | { 8 | public static class Driver 9 | { 10 | [ThreadStatic] 11 | private static IWebDriver _driver; 12 | 13 | [ThreadStatic] 14 | public static Wait Wait; 15 | 16 | [ThreadStatic] 17 | public static Window Window; 18 | 19 | public static void Init() 20 | { 21 | _driver = DriverFactory.Build(FW.Config.Driver.Browser); 22 | Wait = new Wait(FW.Config.Driver.WaitSeconds); 23 | Window = new Window(); 24 | Window.Maximize(); 25 | } 26 | 27 | public static IWebDriver Current => _driver ?? throw new NullReferenceException("_driver is null."); 28 | 29 | public static string Title => Current.Title; 30 | 31 | public static void Goto(string url) 32 | { 33 | if (!url.StartsWith("http")) 34 | { 35 | url = $"http://{url}"; 36 | } 37 | 38 | FW.Log.Info(url); 39 | Current.Navigate().GoToUrl(url); 40 | } 41 | 42 | public static Element FindElement(By by, string elementName) 43 | { 44 | var element = Wait.Until(drvr => drvr.FindElement(by)); 45 | return new Element(element, elementName) 46 | { 47 | FoundBy = by 48 | }; 49 | } 50 | 51 | public static Elements FindElements(By by) 52 | { 53 | return new Elements(Current.FindElements(by)) 54 | { 55 | FoundBy = by 56 | }; 57 | } 58 | 59 | public static void TakeScreenshot(string imageName) 60 | { 61 | var ss = ((ITakesScreenshot)Current).GetScreenshot(); 62 | var ssFileName = Path.Combine(FW.CurrentTestDirectory.FullName, imageName); 63 | ss.SaveAsFile($"{ssFileName}.png", ScreenshotImageFormat.Png); 64 | } 65 | 66 | public static void Quit() 67 | { 68 | FW.Log.Info("Close Browser"); 69 | Current.Quit(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Framework/Selenium/DriverFactory.cs: -------------------------------------------------------------------------------- 1 | using OpenQA.Selenium; 2 | using OpenQA.Selenium.Chrome; 3 | using OpenQA.Selenium.Firefox; 4 | 5 | namespace Framework.Selenium 6 | { 7 | public static class DriverFactory 8 | { 9 | public static IWebDriver Build(string browserName) 10 | { 11 | FW.Log.Info($"Browser: {browserName}"); 12 | 13 | switch (browserName) 14 | { 15 | case "chrome": 16 | return new ChromeDriver(FW.WORKSPACE_DIRECTORY + "_drivers"); 17 | 18 | case "firefox": 19 | return new FirefoxDriver(); 20 | 21 | default: 22 | throw new System.ArgumentException($"{browserName} not supported."); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Framework/Selenium/Element.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Drawing; 3 | using OpenQA.Selenium; 4 | using OpenQA.Selenium.Interactions; 5 | 6 | namespace Framework.Selenium 7 | { 8 | public class Element : IWebElement 9 | { 10 | private readonly IWebElement _element; 11 | 12 | public readonly string Name; 13 | 14 | public By FoundBy { get; set; } 15 | 16 | public Element(IWebElement element, string name) 17 | { 18 | _element = element; 19 | Name = name; 20 | } 21 | 22 | public IWebElement Current => _element ?? throw new System.NullReferenceException("_element is null."); 23 | 24 | public string TagName => Current.TagName; 25 | 26 | public string Text => Current.Text; 27 | 28 | public bool Enabled => Current.Enabled; 29 | 30 | public bool Selected => Current.Selected; 31 | 32 | public Point Location => Current.Location; 33 | 34 | public Size Size => Current.Size; 35 | 36 | public bool Displayed => Current.Displayed; 37 | 38 | public void Clear() 39 | { 40 | Current.Clear(); 41 | } 42 | 43 | public void Click() 44 | { 45 | FW.Log.Step($"Click {Name}"); 46 | Current.Click(); 47 | } 48 | 49 | public IWebElement FindElement(By by) 50 | { 51 | return Current.FindElement(by); 52 | } 53 | 54 | public ReadOnlyCollection FindElements(By by) 55 | { 56 | return Current.FindElements(by); 57 | } 58 | 59 | public string GetAttribute(string attributeName) 60 | { 61 | return Current.GetAttribute(attributeName); 62 | } 63 | 64 | public string GetCssValue(string propertyName) 65 | { 66 | return Current.GetCssValue(propertyName); 67 | } 68 | 69 | public string GetProperty(string propertyName) 70 | { 71 | return Current.GetProperty(propertyName); 72 | } 73 | 74 | public void Hover() 75 | { 76 | var actions = new Actions(Driver.Current); 77 | actions.MoveToElement(Current).Perform(); 78 | } 79 | 80 | public void SendKeys(string text) 81 | { 82 | Current.SendKeys(text); 83 | } 84 | 85 | public void Submit() 86 | { 87 | Current.Submit(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Framework/Selenium/Elements.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using OpenQA.Selenium; 4 | 5 | namespace Framework.Selenium 6 | { 7 | public class Elements : ReadOnlyCollection 8 | { 9 | private readonly IList _elements; 10 | 11 | public Elements(IList list) : base(list) 12 | { 13 | _elements = list; 14 | } 15 | 16 | public By FoundBy { get; set; } 17 | 18 | public bool IsEmpty => Count == 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Framework/Selenium/Wait.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenQA.Selenium; 3 | using OpenQA.Selenium.Support.UI; 4 | 5 | namespace Framework.Selenium 6 | { 7 | public class Wait 8 | { 9 | private readonly WebDriverWait _wait; 10 | 11 | public Wait(int waitSeconds) 12 | { 13 | _wait = new WebDriverWait(Driver.Current, TimeSpan.FromSeconds(waitSeconds)) 14 | { 15 | PollingInterval = TimeSpan.FromMilliseconds(500) 16 | }; 17 | 18 | _wait.IgnoreExceptionTypes( 19 | typeof(NoSuchElementException), 20 | typeof(ElementNotVisibleException), 21 | typeof(StaleElementReferenceException) 22 | ); 23 | } 24 | 25 | public bool Until(Func condition) 26 | { 27 | return _wait.Until(condition); 28 | } 29 | 30 | public IWebElement Until(Func condition) 31 | { 32 | return _wait.Until(condition); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Framework/Selenium/WaitConditions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenQA.Selenium; 3 | 4 | namespace Framework.Selenium 5 | { 6 | public sealed class WaitConditions 7 | { 8 | public static Func ElementDisplayed(IWebElement element) 9 | { 10 | bool condition(IWebDriver driver) 11 | { 12 | return element.Displayed; 13 | } 14 | 15 | return condition; 16 | } 17 | 18 | public static Func ElementIsDisplayed(IWebElement element) 19 | { 20 | IWebElement condition(IWebDriver driver) 21 | { 22 | try 23 | { 24 | return element.Displayed ? element : null; 25 | } 26 | catch (NoSuchElementException) 27 | { 28 | return null; 29 | } 30 | catch (ElementNotVisibleException) 31 | { 32 | return null; 33 | } 34 | } 35 | 36 | return condition; 37 | } 38 | 39 | public static Func ElementNotDisplayed(IWebElement element) 40 | { 41 | bool condition(IWebDriver driver) 42 | { 43 | try 44 | { 45 | return !element.Displayed; 46 | } 47 | catch (StaleElementReferenceException) 48 | { 49 | return true; 50 | } 51 | } 52 | 53 | return condition; 54 | } 55 | 56 | public static Func ElementsNotEmpty(Elements elements) 57 | { 58 | Elements condition(IWebDriver driver) 59 | { 60 | Elements _elements = Driver.FindElements(elements.FoundBy); 61 | return _elements.IsEmpty ? null : _elements; 62 | } 63 | 64 | return condition; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Framework/Selenium/Window.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Drawing; 4 | using OpenQA.Selenium; 5 | 6 | namespace Framework.Selenium 7 | { 8 | public class Window 9 | { 10 | public ReadOnlyCollection CurrentWindows => Driver.Current.WindowHandles; 11 | 12 | public void SwitchTo(int windowIndex) 13 | { 14 | Driver.Current.SwitchTo().Window(CurrentWindows[windowIndex]); 15 | } 16 | 17 | public Size ScreenSize 18 | { 19 | get 20 | { 21 | var js = "return [window.screen.availWidth, window.screen.availHeight];"; 22 | var jse = (IJavaScriptExecutor)Driver.Current; 23 | 24 | dynamic dimensions = jse.ExecuteScript(js, null); 25 | var x = Convert.ToInt32(dimensions[0]); 26 | var y = Convert.ToInt32(dimensions[1]); 27 | 28 | return new Size(x, y); 29 | } 30 | } 31 | 32 | public void Maximize() 33 | { 34 | Driver.Current.Manage().Window.Position = new Point(0, 0); 35 | Driver.Current.Manage().Window.Size = ScreenSize; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Framework/Services/ApiCardService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Framework.Models; 4 | using Newtonsoft.Json; 5 | using RestSharp; 6 | 7 | namespace Framework.Services 8 | { 9 | public class ApiCardService : ICardService 10 | { 11 | public const string CARDS_API = "https://statsroyale.com/api/cards"; 12 | 13 | public IList GetAllCards() 14 | { 15 | var client = new RestClient(CARDS_API); 16 | var request = new RestRequest 17 | { 18 | Method = Method.GET, 19 | RequestFormat = DataFormat.Json 20 | }; 21 | 22 | var response = client.Execute(request); 23 | 24 | if (response.StatusCode != System.Net.HttpStatusCode.OK) 25 | { 26 | throw new System.Exception("/cards endpoint failed with " + response.StatusCode); 27 | } 28 | 29 | return JsonConvert.DeserializeObject>(response.Content); 30 | } 31 | 32 | public Card GetCardByName(string cardName) 33 | { 34 | var cards = GetAllCards(); 35 | return cards.FirstOrDefault(card => card.Name == cardName); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Framework/Services/ICardService.cs: -------------------------------------------------------------------------------- 1 | using Framework.Models; 2 | 3 | namespace Framework.Services 4 | { 5 | public interface ICardService 6 | { 7 | Card GetCardByName(string cardName); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Framework/Services/InMemoryCardService.cs: -------------------------------------------------------------------------------- 1 | using Framework.Models; 2 | 3 | namespace Framework.Services 4 | { 5 | public class InMemoryCardService : ICardService 6 | { 7 | public Card GetCardByName(string cardName) 8 | { 9 | switch(cardName) 10 | { 11 | case "Ice Spirit": 12 | return new IceSpiritCard(); 13 | 14 | case "Mirror": 15 | return new MirrorCard(); 16 | 17 | default: 18 | throw new System.ArgumentException("Card is not available: " + cardName); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # From Scripting to Framework course on TestAutomationU.com 2 | 3 | ## HOW TO USE THIS REPO 4 | 5 | This repository is meant to help you as you are watching and learning through the course. 6 | 7 | By default, when you land on this page, the current branch will be `master` which will have all of the latest code. As you follow along, make sure to switch to the branch or commit that matches your current chapter. 8 | 9 | For example, if you want to see the code used for Chapter 5, but don't want to see the solutions to the challenge(s): 10 | 11 | 1. Click `Commits` 12 | - This should take you to something like https://github.com/ElSnoMan/from-scripting-to-framework/commits/master 13 | 14 | 2. Click on the Commit you want to view 15 | - In this case, click `Chapter 5 - Customizing WebDriver` 16 | 17 | If you want to see the solutions to Chapter 5's challenges: 18 | 19 | 1. Click `Commits` 20 | 21 | 2. Click on the `Challenge` or `(solution)` Commit 22 | - In this case, click `Chapter 5 - Challenge` 23 | 24 | This will show you the code that I used to solve the challenge, but note that there are usually multiple solutions that are valid. 25 | 26 | ## Known Issues 27 | 28 | > Can't build or "Driver" does not contain a definition for FindElement 29 | 30 | In chapter 5, you will have a `Royale.Pages` namespace. dotnet core will raise problems because the namespace shares the same name as the `Pages.cs` file and class! PLEASE CHANGE THE NAME OF `Pages.cs` and its class to something else like `PageWrapper.cs`! This will resolve build issues 31 | 32 | 33 | ## CHAPTERS 34 | 35 | Each chapter in this README is an overview with highlights of the chapter. These are helpful since it's easier to keep this up-to-date than videos. 36 | 37 | If you get stuck, take a look at the chapter's overview for some extra guidance. 38 | 39 | If you are still stuck, feel free to create an issue or ping me on the TAU Slack Channel. 40 | 41 | ## Chapter 1 - Machine Setup 42 | 43 | 1. Requirements 44 | - .NET Core version 2.2 (latest at time of recording) or greater 45 | - VS Code 46 | 47 | 2. Make a new project called `scripting-to-framework` and open it in VS Code 48 | 49 | 3. Install Extensions in VS Code 50 | - C# by Microsoft 51 | - PackSharp by Carlos Kidman 52 | 53 | 4. Open the Command Palette for each of these commands 54 | - `PackSharp: Create New Project` > select `Class Library` > call the Project "Framework" 55 | - `PackSharp: Create New Project` > select `Class Library` > call the Project "Royale" 56 | - `PackSharp: Create New Project` > select `NUnit 3 Test Project` > call the Project "Royale.Tests" 57 | - `PackSharp: Create New Project` > select `Solution File` > call the Project "StatsRoyale" 58 | 59 | > NOTE: The Solution (.sln) file will manage the Project (.csproj) files while the Project files handle their own packages and dependencies. As you add things, this is all handled for you! Very cool. 60 | 61 | 5. Add the Projects to the Solution. Run these commands in the Terminal: 62 | - `$ dotnet sln add Framework` 63 | - `$ dotnet sln add Royale` 64 | - `$ dotnet sln add Royale.Tests` 65 | 66 | 6. Build the Solution so we know everything is working 67 | - `$ dotnet build` 68 | 69 | 7. Open the `UnitTest1.cs` file in the Royale.Tests project 70 | - C# will "spin up" so you can start coding in C# and get helpful completions, hints, etc. 71 | - VS Code will ask if you want "Required assets to build and debug". Add them by clicking the "Yes" button. 72 | - If you do not get this immediately, try closing VS Code and reopening the project. 73 | - This will add a `.vscode` folder to your solution, but this is required to run and debug the tests. 74 | 75 | 8. Run the Tests 76 | - `$ dotnet test` 77 | - This will run all the tests, but you only have one right now. It should pass. 78 | 79 | 80 | ## Chapter 2 - Script Some Tests 81 | 82 | > NOTE: Our application under test (website) is https://statsroyale.com 83 | 84 | 1. Change the name of the Test Class and Test Method so they make more sense 85 | - Test Class from `Tests` to `CardTests` 86 | - File name from `Tests.cs` to `CardTests.cs` 87 | - Test Method from `Test1()` to `Ice_Spirit_is_on_Cards_Page()` 88 | 89 | 2. Install Selenium NuGet (package) with PackSharp 90 | - Open Command Palette > select `PackSharp: Bootstrap Selenium` > add to `Royale.Tests`, our Test Project 91 | - If you open the `Royale.Tests.csproj` file, you will see that Selenium packages have been added 92 | - You will also see a `_drivers` directory is added at the Workspace root 93 | 94 | > NOTE: This command installs the **latest** versions of chromedriver and the Selenium packages. 95 | 96 | 3. Use Selenium in our CardTests.cs file 97 | - Within the CardTests class, add the `IWebDriver driver;` field to the top 98 | - Resolve the error by hovering the red line and click on the lightbulb 99 | - The first option will want you to add the `using OpenQA.Selenium;` statement. Select that one 100 | - The error will go away and you will see that using statement is added automatically 101 | 102 | 4. SetUp and TearDown methods 103 | - The [SetUp] method is run *before each* test. Change the method name from `Setup()` to `BeforeEach()` 104 | - Add a [TearDown] method which runs *after each* test. Call this method `AfterEach()` 105 | 106 | 5. Within AfterEach(), add: 107 | - `driver.Quit();` 108 | - This will close the driver after each test is finished 109 | 110 | 6. Within BeforeEach(), add: 111 | - `driver = new ChromeDriver();` 112 | - This will open a new Chrome browser for every test 113 | - "Point" the ChromeDriver() to wherever you store your `chromedriver` or `chromedriver.exe`. 114 | 115 | > NOTE: Everyone manages their drivers (like `chromedriver`, `geckodriver`, etc.) differently. Use your preferred method. 116 | 117 | 7. Write the first test. The steps are: 118 | 1. Go to https://statsroyale.com 119 | 2. Click on Cards link 120 | 3. Assert Ice Spirit is displayed 121 | 122 | 8. For the second test, the steps are: 123 | 1. Go to https://statsroyale.com 124 | 2. Click on Cards link 125 | 3. Click on Ice Spirit card 126 | 4. Assert the basic headers are correct. These headers are: 127 | - Name ("Ice Spirit") 128 | - Type ("Troop") 129 | - Arena ("Arena 8") 130 | - Rarity ("Common") 131 | 132 | 9. There's a lot of code in this one, so make sure to pause and replay as much as you need :) 133 | 134 | 135 | ## Chapter 3 - Page Object Model 136 | 137 | Follow the video to for an explanation on the `Page Object Model` and `Page Map Pattern`. 138 | 139 | 1. Within the Royale project, create a `Pages` directory. This is where all of our Page objects will live. 140 | 141 | 2. Move the `Class1.cs` file into `Pages` and rename it to `HeaderNav.cs` 142 | 143 | 3. Within the file, rename `Class1` to `HeaderNav` and then make another class called `HeaderNavMap` 144 | 145 | 4. Use PackSharp to restructure our packages and dependencies so we leverage Framework and Royale projects 146 | 1. Move Selenium to the `Framework` project 147 | - Open Command Palette > `PackSharp: Bootstrap Selenium` > select `Framework` 148 | 2. Remove Selenium from `Royale.Tests` project 149 | - Open Command Palette > `PackSharp: Remove Package` > select `Royale.Tests` > select `Selenium.Support` 150 | - Also remove `Selenium.WebDriver` 151 | 152 | 5. Framework is our base, so we want the projects to reference each other in a linear way. 153 | 154 | Framework -> Royale -> Royale.Tests 155 | 156 | `Royale.Tests` will reference `Royale` which references `Framework` 157 | 158 | - Open Command Palette > `PackSharp: Add Project Reference` > select `Royale.Tests` > select `Royale` 159 | - Open Command Palette > `PackSharp: Add Project Reference` > select `Royale` > select `Framework` 160 | 161 | 6. Now we can bring in `using OpenQA.Selenium` in our `HeaderNav.cs` file 162 | 163 | > NOTE: The rest of this video is very "code-heavy", so make sure to follow along there 164 | 165 | 7. The naming convention for Pages and Page Maps is very simple. If you have a Home page, then you would do this: 166 | - Page => `HomePage` 167 | - Map => `HomePageMap` 168 | 169 | 8. In the video, there is a "jump" from `11:22` to `11:25` where I am able to use the `Map.Card()` immediately. At this point, you will have an error. All you need to do is: 170 | - Add the `public readonly CardPageMap Map;` field at the top of the `CardsPage` class and the Constructor as well 171 | - You will see the needed code at `11:43`. Sorry about that! 172 | 173 | 9. Card Details Page and Map 174 | - Take a moment to pause the video and copy the code to move forward 175 | 176 | 10. At the end of the video, run your second test. It should fail! Your challenge is to solve this error so the test passes. 177 | 178 | 179 | ## Chapter 4 - Models and Services 180 | 181 | 1. Copy and Paste the second test to make a third test. However, this test will be for the "Mirror" card, so change the values in the test accordingly. 182 | 183 | 2. In the Framework project, make a new folder called `Models` and move the `Class1.cs` file into it. 184 | 185 | 3. Rename the file to `MirrorCard.cs` and change the class name too 186 | 187 | 4. We'll be adding the card properties that we care about and give them default values. 188 | 189 | > NOTE: Some portions are sped up, so just pause the video when it gets back to regular speed and copy as needed. 190 | 191 | 5. Because `MirrorCard` and `IceSpiritCard` share the same properties, we can create a "base" `Card` with these properties and have our cards inherit it. This is very similar to how our `PageBase` class works. Every page on the website has access to the `HeaderNav`, right? Instead of repeating the header navigation bar on every page, we can create it once and then share it to every page! This helps us avoid repeating ourselves as well as simplifying our code. 192 | 193 | - Copy and Paste the `MirrorCard` into a new file called `IceSpiritCard.cs` in Models. 194 | - Change the default values to the Ice Spirit values 195 | 196 | 6. Now we can bring this all together by creating a `GetBaseCard()` method in our `CardDetailsPage`. Pause the video as needed. 197 | 198 | 7. We can use this method in the Mirror test by getting two cards: 199 | - `var card = cardDetails.GetBaseCard();` 200 | - This card is the one we get off of the page using Selenium and our Page objects 201 | - `var mirror = new MirrorCard();` 202 | - This card has the actual values that we expect our Mirror Card to have 203 | 204 | 8. Assert that the expected values from our `var mirror` card match the actual values we received from `var card`. 205 | 206 | 9. Our first Card Services 207 | 208 | These "services" are abstractions of how we end up getting cards from a data store. 209 | 210 | The "In Memory" card service will help with getting cards from local, hard-coded values we've specified in our Models directory, but ultimately we'd like to get these values from actual data stores like a database. We will be doing that in a future chapter :) 211 | 212 | 10. Now that the Services are complete and being used in the test, our 2nd and 3rd tests are almost identical. The only difference are the names! 213 | 214 | - We can now leverage the `[TestCase]` and `[TestSource]` attributes from the `NUnit Test Framework` 215 | - These will "feed" values into the test 216 | - This turns a single Test Method into multiple Test Cases! 217 | 218 | 11. We can use the `[Parallelizable]` attribute to make these Test Cases run in parallel! 219 | - `[Parallelizable(ParallelScope.Children)]` 220 | 221 | 12. Run the 3rd test. It will spin up two browsers at the same time, but most likely both with fail. What happened? 222 | - You will also notice that one of the two browsers doesn't close even after failing. Can you guess why? 223 | - The answer is that we are instantiating a single WebDriver for both tests, so they are fighting each other over the driver! 224 | - We will solve this in the next chapter 225 | 226 | 13. Last thing we'll do is add the `[Category]` attribute to our test 227 | - `[Category("cards")]` 228 | - We can use Categories when running our tests. To run only the tests with the Category of "cards", we would do: 229 | - `$ dotnet test --filter testcategory=cards` 230 | 231 | > NOTE: After the recording, they changed the way some of their pages loaded. This includes the Cards page, so we will need to add a wait. 232 | 233 | ```c# 234 | // initialize a WebDriverWait 235 | // make sure to bring in appropriate "using" statement 236 | var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10)); 237 | 238 | // you can use it in GetCardByName() 239 | wait.Until(drvr => Map.Card(cardName).Displayed); 240 | 241 | // or maybe in the CardsPageMap's Card(string name) property 242 | wait.Until(drvr => drvr.FindElement(By.CssSelector($"a[href*='{name}']"))) 243 | 244 | ``` 245 | 246 | 247 | ## Chapter 5 - Customizing WebDriver 248 | 249 | 1. Start by creating a `Selenium` folder in our `Framework` project 250 | 251 | 2. Create a `Driver.cs` file in `Selenium`. This is where our wrapper of WebDriver will exist. 252 | 253 | 3. The key to achieving the simplicity of a `static` even though it's not a singleton is with the `[ThreadStatic]` attribute. 254 | ```c# 255 | [ThreadStatic] 256 | private static IWebDriver _driver; 257 | 258 | public static void Init() 259 | { 260 | _driver = new ChromeDriver(); 261 | } 262 | 263 | public static IWebDriver Current => _driver ?? throw new System.ArgumentException("_driver is null."); 264 | ``` 265 | 266 | - This is the bread and butter of this approach. `_driver` is the instance of WebDriver on a thread. 267 | - `Current` is how you access the current instance of WebDriver for the test you're on 268 | 269 | 4. In the `BeforeEach()` method of CardTests, you can now use `Driver.Init();` and remove the "global" `IWebDriver driver` at the top. 270 | 271 | 5. Then create a "Pages Wrapper" 272 | 1. Create `Pages.cs` file in `Royale.Pages` directory 273 | 2. Create a field for each page we have 274 | ```c# 275 | [ThreadStatic] 276 | public static CardsPage Cards; 277 | 278 | [ThreadStatic] 279 | public static CardDetailsPage CardDetails; 280 | 281 | public static void Init() 282 | { 283 | Cards = new CardsPage(Driver.Current); 284 | CardDetails = new CardDetailsPage(Driver.Current); 285 | } 286 | ``` 287 | 288 | 6. In the tests, we can get rid of any lines that say `new Page()` because our Pages Wrapper handles that for us 289 | - Replace: `var cardsPage = new CardsPage(Driver.Current);` 290 | - With: `Pages.Cards` 291 | 292 | 7. In our `Driver` class, let's add a way to navigate to a URL so we don't have to say `Driver.Current.Url` 293 | ```c# 294 | public static void Goto(string url) 295 | { 296 | if (!url.StartsWith("http")) 297 | { 298 | url = $"http://{url}"; 299 | } 300 | 301 | Debug.WriteLine(url); 302 | Current.Navigate().GoToUrl(url); 303 | } 304 | ``` 305 | 306 | 8. Your `BeforeEach()` method should now look like this: 307 | ```c# 308 | [SetUp] 309 | public void BeforeEach() 310 | { 311 | Driver.Init(); 312 | Pages.Init(); 313 | Driver.Goto("https://statsroyale.com"); 314 | } 315 | ``` 316 | 317 | 9. Now add a `FindElement()` and `FindElements()` method to Driver. 318 | 319 | 10. Go to each of the Page Objects and replace `_driver` with `Driver`. 320 | - We no longer need the `IWebDriver _driver` field in the Page Maps 321 | - We no longer need the constructors in the Page Maps 322 | - We no longer need to pass in `IWebDriver driver` in Page Object constructors! 323 | 324 | 11. Yes, a lot of code is deleted, but that's a good thing! 325 | 326 | 12. The challenge is to add a `Quit()` method to our Driver class to get rid of `Driver.Current.Quit();` in the `AfterEach()`. 327 | 328 | 329 | ## Chapter 6 - Test Data from an API 330 | 331 | 1. From Postman, we saw the the /cards endpoint returned all of the cards that exist in the game. However, they had a different shape than our Card class. Update our Card class to include these new properties: 332 | 333 | ```c# 334 | public class Card 335 | { 336 | public virtual string Id { get; set; } 337 | 338 | public virtual string Name { get; set; } 339 | 340 | public virtual string Icon { get; set; } 341 | 342 | public virtual int Cost { get; set; } 343 | 344 | public virtual string Rarity { get; set; } 345 | 346 | public virtual string Type { get; set; } 347 | 348 | public virtual string Arena { get; set; } 349 | } 350 | ``` 351 | 352 | 2. We want to leverage that endpoint by creating an ApiCardService 353 | - Create `ApiCardService.cs` within `Framework.Services` 354 | - Implement the `ICardService` interface 355 | 356 | 3. Install the `Newtonsoft.Json` package to the `Framework` project 357 | - Open Command Palette > `PackSharp: Add Package` > search `Newtonsoft` > select `Newtonsoft.Json` 358 | 359 | 4. Install the `RestSharp` package to the `Framework` project 360 | - Open Command Palette > `PackSharp: Add Package` > search `RestSharp` > select `RestSharp` 361 | 362 | 5. Within the `ApiCardService` class, we will use RestSharp to make the call just like PostMan. We'll call this method `GetAllCards()` 363 | 364 | ```c# 365 | public const string CARDS_API = "https://statsroyale.com/api/cards"; 366 | 367 | public IList GetAllCards() 368 | { 369 | var client = new RestClient(CARDS_API); 370 | var request = new RestRequest 371 | { 372 | Method = Method.GET, 373 | RequestFormat = DataFormat.Json 374 | }; 375 | 376 | var response = client.Execute(request); 377 | 378 | if (response.StatusCode != System.Net.HttpStatusCode.OK) 379 | { 380 | throw new System.Exception("/cards endpoint failed with " + response.StatusCode); 381 | } 382 | 383 | return JsonConvert.DeserializeObject>(response.Content); 384 | } 385 | ``` 386 | 387 | 6. Now let's implement the `GetCardByName(string cardName)` method 388 | 389 | ```c# 390 | public Card GetCardByName(string cardName) 391 | { 392 | var cards = GetAllCards(); 393 | return cards.FirstOrDefault(card => card.Name == cardName); 394 | } 395 | ``` 396 | 397 | 7. Now we can use the list of cards from the API rather than our hard-coded list of two cards. In CardTests, put this above our first test: 398 | ```c# 399 | static IList apiCards = new ApiCardService().GetAllCards(); 400 | ``` 401 | 402 | 8. Our tests can now leverage this list of cards using the `[TestCaseSource]` attribute. Our first test should then look like: 403 | 404 | ```c# 405 | [Test, Category("cards")] 406 | [TestCaseSource("apiCards")] 407 | [Parallelizable(ParallelScope.Children)] 408 | public void Card_is_on_Cards_Page(Card card) 409 | { 410 | var cardOnPage = Pages.Cards.Goto().GetCardByName(card.Name); 411 | Assert.That(cardOnPage.Displayed); 412 | } 413 | ``` 414 | - We have added the "cards" Category to the test 415 | - The TestCaseSource is now coming from "apiCards" 416 | - We've added the same [Parallelizable] attribute to this test 417 | - Changed the test name to better reflect that it tests all cards and not just Ice Spirit anymore 418 | - The object we are passing into the test is not a `string`, but a `Card` object. 419 | 420 | 9. Update the second test to also use `apiCards` 421 | 422 | 10. Run the tests but use the `NUnit.NumberOfTestWorkers=2` argument so you don't overload your machine 423 | ```bash 424 | $ dotnet test --filter testcategory=cards -- NUnit.NumberOfTestWorkers=2 425 | ``` 426 | - This will only run two tests at a time which is probably best for your machine 427 | - You can press `CTRL + C` in the terminal to cancel the test execution 428 | 429 | 11. CHALLENGE 1: After some test failures, you will see some interesting errors. The biggest one is that "Troop" is not equal to "tid_card_type_character" and "Spell" is not equal to "tid_card_type_spell". 430 | 431 | The challenge is to solve this error. 432 | 433 | > HINT: `tid_card_type_spell` already has the word "spell" in it. Could we use that somehow? 434 | > HINT: `Troop` and `character` are the same thing in the context of the game. We should treat characters as troops and vice-versa 435 | 436 | 12. CHALLENGE 2: Similar to Challenge 1, tests will be failing because "Arena 8" is not equal to `8` 437 | 438 | The challenge is to solve this error. 439 | 440 | > HINT: A string of `"8"` is different than an integer of `8` 441 | > HINT: Be aware that some cards have an Arena of `0` because they are playable in the "Training Camp". This is why we need to understand the data we're working with. Otherwise, you wouldn't be testing for those values! Check out the `Baby Dragon` card. 442 | 443 | Since the recording, they have slightly changed the way their pages load. Because of this, you may experience "flakiness" because we are lacking any waits. We will be discussing waits in Chapter 7 and 12. 444 | 445 | 446 | ## Chapter 7 - New Test Suite 447 | 448 | #### 7a 449 | 450 | 1. Make a new suite of tests by creating a new test file called `CopyDeckTests.cs` 451 | 452 | 2. Within this file, we'll copy and paste our SetUp and TearDown from the `CardTests.cs` 453 | 454 | 3. Then we can write a new test that checks whether a user can copy a deck 455 | 456 | ```c# 457 | [Test] 458 | public void User_can_copy_the_deck() 459 | { 460 | // 2. go to Deck Builder page 461 | Driver.FindElement(By.CssSelector("[href='/deckbuilder']")).Click(); 462 | // 3. Click "add cards manually" 463 | Driver.FindElement(By.XPath("//a[text()='add cards manually']")).Click(); 464 | // 4. Click Copy Deck icon 465 | Driver.FindElement(By.CssSelector(".copyButton")).Click(); 466 | // 5. Click Yes 467 | Driver.FindElement(By.Id("button-open")).Click(); 468 | // 6. Assert the "if click Yes..." message is displayed 469 | var copyMessage = Driver.FindElement(By.CssSelector(".notes.active")); 470 | Assert.That(copyMessage.Displayed); 471 | } 472 | ``` 473 | 474 | 4. With the test written, we can now refactor this into our Page Map Pattern. 475 | - Create a `DeckBuilderPage.cs` file within `Royale.Pages` 476 | - Create the Page Map 477 | ```c# 478 | public class DeckBuilderPageMap 479 | { 480 | public IWebElement AddCardsManuallyLink => Driver.FindElement(By.CssSelector("")); 481 | 482 | public IWebElement CopyDeckIcon => Driver.FindElement(By.XPath("//a[text()='add cards manually']")); 483 | } 484 | ``` 485 | 486 | - Then create the Page 487 | ```c# 488 | public class DeckBuilderPage : PageBase 489 | { 490 | public readonly DeckBuilderPageMap Map; 491 | 492 | public DeckBuilderPage() 493 | { 494 | Map = new DeckBuilderPageMap(); 495 | } 496 | 497 | public DeckBuilderPage Goto() 498 | { 499 | HeaderNav.Map.DeckBuilderLink.Click(); 500 | return this; 501 | } 502 | 503 | public void AddCardsManually() 504 | { 505 | Map.AddCardsManuallyLink.Click(); 506 | } 507 | 508 | public void CopySuggestedDeck() 509 | { 510 | Map.CopyDeckIcon.Click(); 511 | } 512 | } 513 | ``` 514 | 515 | 5. Create the `CopyDeckPage.cs` file 516 | 517 | 6. Add these pages to the Pages Wrapper class in `Pages.cs`. Your Pages class should now look like this 518 | 519 | ```c# 520 | public class Pages 521 | { 522 | [ThreadStatic] 523 | public static CardsPage Cards; 524 | 525 | [ThreadStatic] 526 | public static CardDetailsPage CardDetails; 527 | 528 | [ThreadStatic] 529 | public static DeckBuilderPage DeckBuilder; 530 | 531 | [ThreadStatic] 532 | public static CopyDeckPage CopyDeck; 533 | 534 | public static void Init() 535 | { 536 | Cards = new CardsPage(); 537 | CardDetails = new CardDetailsPage(); 538 | DeckBuilder = new DeckBuilderPage(); 539 | CopyDeck = new CopyDeckPage(); 540 | } 541 | } 542 | ``` 543 | 544 | 7. Use these pages in the test 545 | 546 | ```c# 547 | [Test] 548 | public void User_can_copy_the_deck() 549 | { 550 | Pages.DeckBuilder.Goto(); 551 | Pages.DeckBuilder.AddCardsManually(); 552 | Pages.DeckBuilder.CopySuggestedDeck(); 553 | 554 | Pages.CopyDeck.Yes(); 555 | 556 | Assert.That(Pages.CopyDeck.Map.CopiedMessage.Displayed); 557 | } 558 | ``` 559 | 560 | 8. If you run the test, it fails pretty quickly! That's because the test in the code is running faster than the page is moving. We need to use `WebDriverWait`. Add this to the top of our test. 561 | ```c# 562 | var wait = new WebDriverWait(Driver.Current, TimeSpan.FromSeconds(10)); 563 | ``` 564 | 565 | 9. After `Pages.DeckBuilder.AddCardsManually` add 566 | ```c# 567 | wait.Until(drvr => Pages.DeckBuilder.Map.CopyDeckIcon.Displayed); 568 | ``` 569 | 570 | 10. Add another wait after `Pages.CopyDeck.Yes` 571 | ```c# 572 | wait.Until(drvr => Pages.CopyDeck.Map.CopiedMessage.Displayed); 573 | ``` 574 | 575 | 11. Just like how we created our own Driver class to "extend" WebDriver, we will do the same for WebDriverWait. 576 | - Create a `Wait.cs` file in `Framework.Selenium` 577 | ```c# 578 | public class Wait 579 | { 580 | private readonly WebDriverWait _wait; 581 | 582 | public Wait(int waitSeconds) 583 | { 584 | _wait = new WebDriverWait(Driver.Current, TimeSpan.FromSeconds(waitSeconds)) 585 | { 586 | PollingInterval = TimeSpan.FromMilliseconds(500) 587 | }; 588 | 589 | _wait.IgnoreExceptionTypes( 590 | typeof(NoSuchElementException), 591 | typeof(ElementNotVisibleException), 592 | typeof(StaleElementReferenceException) 593 | ); 594 | } 595 | 596 | public bool Until(Func condition) 597 | { 598 | return _wait.Until(condition); 599 | } 600 | } 601 | ``` 602 | 603 | 12. Include our new Wait to the Driver class 604 | 605 | ```c# 606 | [ThreadStatic] 607 | private static IWebDriver _driver; 608 | 609 | [ThreadStatic] 610 | public static Wait Wait; 611 | 612 | public static void Init() 613 | { 614 | _driver = new ChromeDriver(Path.GetFullPath(@"../../../../" + "_drivers")); 615 | Wait = new Wait(10); 616 | } 617 | ``` 618 | 619 | 13. In our test, replace `wait.Until()` with our new Wait. For example: 620 | ```c# 621 | Driver.Wait.Until(drvr => Pages.DeckBuilder.Map.CopyDeckIcon.Displayed); 622 | ``` 623 | 624 | 14. CHALLENGE 7a: On the `DeckBuilderPage`, the Page Map has two elements that may have incorrect locators. You probably didn't spot this! 625 | 626 | Part 1 of the challenge is to validate that they work in the DevTools Console and fix them if needed. 627 | 628 | Also, you may need to add another wait for the `AddCardsManually()` step. 629 | 630 | Part 2 of the challenge is to add this missing wait to the test. 631 | 632 | 633 | #### 7b 634 | 635 | 1. Refactor the Waits from our `User_can_copy_deck()` test into their respective actions. For example, the `AddCardsManually()` method should now look like: 636 | 637 | ```c# 638 | public void AddCardsManually() 639 | { 640 | Map.AddCardsManuallyLink.Click(); 641 | Driver.Wait.Until(drvr => Map.CopyDeckIcon.Displayed); 642 | } 643 | ``` 644 | 645 | 2. Write the next two tests with how we want them to eventuall look like. This is kind of a TDD approach: 646 | 647 | ```C# 648 | [Test] 649 | public void User_opens_app_store() 650 | { 651 | Pages.DeckBuilder.Goto().AddCardsManually(); 652 | Pages.DeckBuilder.CopySuggestedDeck(); 653 | Pages.CopyDeck.No().OpenAppStore(); 654 | Assert.That(Driver.Title, Is.EqualTo("Clash Royale on the App Store")); 655 | } 656 | [Test] 657 | public void User_opens_google_play() 658 | { 659 | Pages.DeckBuilder.Goto().AddCardsManually(); 660 | Pages.DeckBuilder.CopySuggestedDeck(); 661 | Pages.CopyDeck.No().OpenGooglePlay(); 662 | Assert.AreEqual("Clash Royale - Apps on Google Play", Driver.Title); 663 | } 664 | ``` 665 | 666 | - Change the return type of our `DeckBuilder.Goto()` method so we can "chain" `AddCardsManually()` 667 | - From `void` to `DeckBuilderPage` and adding `return this;` 668 | 669 | ```c# 670 | public DeckBuilder Goto() 671 | { 672 | HeaderNav.Map.DeckBuilderLink.Click(); 673 | Driver.Wait.Until(drvr => Map.AddCardsManuallyLink.Displayed); 674 | return this; 675 | } 676 | ``` 677 | 678 | 3. You will have some errors like a red squiggly under `No()` because we haven't implemented it yet. Let's do that! In the `CopyDeckPage`, add a No() method: 679 | 680 | ```c# 681 | public CopyDeckPage No() 682 | { 683 | Map.NoButton.Click(); 684 | return this; 685 | } 686 | ``` 687 | 688 | - `return this;` returns the current instance of itself - the CopyDeckPage 689 | - Notice how the return type is a `CopyDeckPage` 690 | - This allows us to "chain" commands and actions like we did in Step 2! 691 | 692 | 4. Add the `OpenAppStore()` and `OpenGooglePlay()` methods as well 693 | 694 | ```c# 695 | public void OpenAppStore() 696 | { 697 | Map.AppStoreButton.Click(); 698 | } 699 | 700 | public void OpenGooglePlay() 701 | { 702 | Map.GooglePlayButton.Click(); 703 | } 704 | ``` 705 | 706 | - Find the elements and add them to the `CopyDeckPageMap` so you can access them in the above methods 707 | 708 | 5. Fix the last error which is `Driver.Title` in the test. Like the error suggests, our `Driver` class doesn't have a `Title` property. Let's add it! 709 | 710 | ```c# 711 | public static string Title => Current.Title; 712 | ``` 713 | 714 | 6. The Accept Cookies banner at the bottom of the Copy Deck page will overlap our App Store buttons. We need to accept this to remove the banner. Add this method: 715 | 716 | ```c# 717 | public void AcceptCookies() 718 | { 719 | Map.AcceptCookiesButton.Click(); 720 | Driver.Wait.Until(drvr => !Map.AcceptCookiesButton.Displayed); 721 | } 722 | ``` 723 | 724 | - Find the element and add it to the Map 725 | - The `AcceptCookies()` method will click the button and then wait for it to disappear (aka NOT BE displayed) 726 | 727 | 7. Add the AcceptCookies() to our No() 728 | 729 | ```c# 730 | public CopyDeckPage No() 731 | { 732 | Map.NoButton.Click(); 733 | AcceptCookies(); 734 | Driver.Wait.Until(drvr => Map.OtherStoresButton.Displayed); 735 | return this; 736 | } 737 | ``` 738 | 739 | - We also added a Wait to make sure the `OtherStoresButton` is displayed first before proceeding 740 | 741 | 8. CHALLENGE 7b: At the end of the video, I show you that our `User_opens_app_store()` fails because of a string comparison issue: 742 | 743 | `"Clash Royale on the App Store"` 744 | 745 | is not equal to 746 | 747 | `"Clash Royale on the App Store"` 748 | 749 | They look identical, but there is a big difference. The string we get back from `Driver.Title` has a *unicode* character at the beginning! 750 | 751 | - The challenge is to solve this error 752 | 753 | > NOTE: There are many ways to approach this, but remember that this is String Comparison 754 | 755 | 756 | ## Chapter 8 - Logging 757 | 758 | 1. Create a `FW.cs` in the Framework project 759 | - This is going to hold objects that are used throughout our Framework and projects 760 | - We'll start with the the workspace directory and test results directory 761 | 762 | ```c# 763 | public static string WORKSPACE_DIRECTORY = Path.GetFullPath(@"../../../../"); 764 | 765 | public static DirectoryInfo CreateTestResultsDirectory() 766 | { 767 | var testDirectory = WORKSPACE_DIRECTORY + "TestResults"; 768 | 769 | if (Directory.Exists(testDirectory)) 770 | { 771 | Directory.Delete(testDirectory, recursive: true); 772 | } 773 | 774 | return Directory.CreateDirectory(testDirectory); 775 | } 776 | ``` 777 | 778 | 2. Create a `Logging` directory with the `Framework` project and create a file called `Logger.cs` 779 | 780 | 3. The Logger class will handle the `log.txt` and writing to them 781 | 782 | ```c# 783 | public class Logger 784 | { 785 | private readonly string _filepath; 786 | 787 | public Logger(string testName, string filepath) 788 | { 789 | _filepath = filepath; 790 | 791 | using (var log = File.CreateText(_filepath)) 792 | { 793 | log.WriteLine($"Starting timestamp: {DateTime.Now.ToLocalTime()}"); 794 | log.WriteLine($"Test: {testName}"); 795 | } 796 | } 797 | 798 | public void Info(string message) 799 | { 800 | WriteLine($"[INFO]: {message}"); 801 | } 802 | 803 | public void Step(string message) 804 | { 805 | WriteLine($" [STEP]: {message}"); 806 | } 807 | 808 | public void Warning(string message) 809 | { 810 | WriteLine($"[WARNING]: {message}"); 811 | } 812 | 813 | public void Error(string message) 814 | { 815 | WriteLine($"[ERROR]: {message}"); 816 | } 817 | 818 | public void Fatal(string message) 819 | { 820 | WriteLine($"[FATAL]: {message}"); 821 | } 822 | 823 | private void WriteLine(string text) 824 | { 825 | using (var log = File.AppendText(_filepath)) 826 | { 827 | log.WriteLine(text); 828 | } 829 | } 830 | 831 | private void Write(string text) 832 | { 833 | using (var log = File.AppendText(_filepath)) 834 | { 835 | log.Write(text); 836 | } 837 | } 838 | } 839 | ``` 840 | 841 | - `WriteLine()` and `Write()` will make writing to the correct log.txt file a piece of cake 842 | - Different "log types" are used depending on the type of message we want to display 843 | - Info 844 | - Step 845 | - Warning 846 | - Error 847 | - Fatal 848 | - Whenever we make a new instance of Logger(), it will create a new log file using the `testName` and `filepath` that are passed. 849 | 850 | 4. Our `Framework` project needs to use NUnit, so we will use PackSharp 851 | - Open Command Palette > `PackSharp: Add Package` > select `Framework` > search "NUnit" > select `NUnit` 852 | - Open Command Palette > `PackSharp: Remove Package` > select `Royale.Tests` > select `NUnit` 853 | - Build solution to make sure things are still structured ok: 854 | 855 | ```bash 856 | $ dotnet clean 857 | $ dotnet restore 858 | $ dotnet build 859 | ``` 860 | 861 | 5. Add a `SetLogger()` method to our `FW` class to create the Logger per test. We'll also need some fields and properties to hold these values. We will also use a `lock` to solve any "race conditions": 862 | 863 | ```c# 864 | public static Logger Log => _logger ?? throw new NullReferenceException("_logger is null. SetLogger() first."); 865 | 866 | [ThreadStatic] 867 | public static DirectoryInfo CurrentTestDirectory; 868 | 869 | [ThreadStatic] 870 | private static Logger _logger; 871 | 872 | public static void SetLogger() 873 | { 874 | lock (_setLoggerLock) 875 | { 876 | var testResultsDir = WORKSPACE_DIRECTORY + "TestResults"; 877 | var testName = TestContext.CurrentContext.Test.Name; 878 | var fullPath = $"{testResultsDir}/{testName}"; 879 | 880 | if (Directory.Exists(fullPath)) 881 | { 882 | CurrentTestDirectory = Directory.CreateDirectory(fullPath + TestContext.CurrentContext.Test.ID); 883 | } 884 | else 885 | { 886 | CurrentTestDirectory = Directory.CreateDirectory(fullPath); 887 | } 888 | 889 | _logger = new Logger(testName, CurrentTestDirectory.FullName + "/log.txt"); 890 | } 891 | } 892 | 893 | private static object _setLoggerLock = new object(); 894 | ``` 895 | 896 | 6. In the Test Suites, `CopyDeckTests` and `CardTests`, add a `[OneTimeSetup]` method and update the `[SetUp]` method: 897 | 898 | ```c# 899 | [OneTimeSetUp] 900 | public void BeforeAll() 901 | { 902 | FW.CreateTestResultsDirectory(); 903 | } 904 | 905 | [SetUp] 906 | public void BeforeEach() 907 | { 908 | FW.SetLogger(); 909 | Driver.Init(); 910 | Pages.Init(); 911 | Driver.Goto("https://statsroyale.com"); 912 | } 913 | ``` 914 | 915 | - `[OneTimeSetUp]` is run before any tests. This will create the `TestResults` directory for the test run. 916 | - `FW.SetLogger()` will create an instance of Logger, which creates a log.txt file, for each test. 917 | 918 | 7. Run the tests 919 | 920 | 8. Open the new `TestResults` directory. You will see that there are directories created for each test and each test has its own `log.txt` file! 921 | 922 | 923 | ## Chapter 9 - Element and Elements 924 | 925 | 1. Create an `Element.cs` under `Framework.Selenium` 926 | 927 | ```c# 928 | public class Element : IWebElement 929 | { 930 | private readonly IWebElement _element; 931 | 932 | public readonly string Name; 933 | 934 | public By FoundBy { get; set; } 935 | 936 | public Element(IWebElement element, string name) 937 | { 938 | _element = element; 939 | Name = name; 940 | } 941 | 942 | public IWebElement Current => _element ?? throw new System.NullReferenceException("_element is null."); 943 | 944 | public string TagName => Current.TagName; 945 | 946 | public string Text => Current.Text; 947 | 948 | public bool Enabled => Current.Enabled; 949 | 950 | public bool Selected => Current.Selected; 951 | 952 | public Point Location => Current.Location; 953 | 954 | public Size Size => Current.Size; 955 | 956 | public bool Displayed => Current.Displayed; 957 | 958 | public void Clear() 959 | { 960 | Current.Clear(); 961 | } 962 | 963 | public void Click() 964 | { 965 | FW.Log.Step($"Click {Name}"); 966 | Current.Click(); 967 | } 968 | 969 | public IWebElement FindElement(By by) 970 | { 971 | return Current.FindElement(by); 972 | } 973 | 974 | public ReadOnlyCollection FindElements(By by) 975 | { 976 | return Current.FindElements(by); 977 | } 978 | 979 | public string GetAttribute(string attributeName) 980 | { 981 | return Current.GetAttribute(attributeName); 982 | } 983 | 984 | public string GetCssValue(string propertyName) 985 | { 986 | return Current.GetCssValue(propertyName); 987 | } 988 | 989 | public string GetProperty(string propertyName) 990 | { 991 | return Current.GetProperty(propertyName); 992 | } 993 | 994 | public void SendKeys(string text) 995 | { 996 | Current.SendKeys(text); 997 | } 998 | 999 | public void Submit() 1000 | { 1001 | Current.Submit(); 1002 | } 1003 | } 1004 | ``` 1005 | 1006 | - Our class `Element` will implement the `IWebElement` interface 1007 | - Just like our Driver, `Current` will represent the current instance of the IWebElement we're extending 1008 | - Add logging to our `Click()` method 1009 | 1010 | 2. Our Driver should now return `Element` instead of `IWebElement` 1011 | 1012 | ```c# 1013 | public static Element FindElement(By by, string elementName) 1014 | { 1015 | return new Element(Current.FindElement(by), elementName) 1016 | { 1017 | FoundBy = by 1018 | }; 1019 | } 1020 | ``` 1021 | 1022 | 3. Go to each of our Pages and change our Maps to use `Element` instead of `IWebElement` and give each element a name. For example, in `HeaderNav.cs`: 1023 | 1024 | ```c# 1025 | public class HeaderNavMap 1026 | { 1027 | public Element CardsTabLink => Driver.FindElement(By.CssSelector("a[href='/cards']"), "Cards Link"); 1028 | 1029 | public Element DeckBuilderLink => Driver.FindElement(By.CssSelector("a[href='/deckbuilder']"), "Deck Builder Link"); 1030 | } 1031 | ``` 1032 | 1033 | > NOTE: The `elementName` we pass in, like "Cards Link", is used for logging purposes 1034 | 1035 | 4. Now create `Elements.cs` under `Framework.Selenium` 1036 | 1037 | ```c# 1038 | public class Elements : ReadOnlyCollection 1039 | { 1040 | private readonly IList _elements; 1041 | 1042 | public Elements(IList list) : base(list) 1043 | { 1044 | _elements = list; 1045 | } 1046 | 1047 | public By FoundBy { get; set; } 1048 | 1049 | public bool IsEmpty => Count == 0; 1050 | } 1051 | ``` 1052 | 1053 | - Our class already has access to things like `Count` because it's inheriting from `ReadOnlyCollection` 1054 | - We already have the functionality of a list, but now we can add our own! `FoundBy` and `IsEmpty` are examples of this 1055 | 1056 | 5. Our Driver should return `Elements` instead of `IList` 1057 | 1058 | ```c# 1059 | public static Elements FindElements(By by) 1060 | { 1061 | return new Elements(Current.FindElements(by)) 1062 | { 1063 | FoundBy = by 1064 | }; 1065 | } 1066 | ``` 1067 | 1068 | 6. CHALLENGE: Our Element class can now have any functionality we want! This is a very powerful way for you to control what you can and cannot do with your elements. 1069 | 1070 | - The challenge is to add a `Hover()` method so that each element simply call .Hover() 1071 | 1072 | > HINT: The Actions class is in `OpenQA.Selenium.Interactions` 1073 | 1074 | 1075 | ## Chapter 10 - Configuration 1076 | 1077 | 1. Create `DriverFactory.cs` under `Framework.Selenium` 1078 | 1079 | ```c# 1080 | public static class DriverFactory 1081 | { 1082 | public static IWebDriver Build(string browserName) 1083 | { 1084 | FW.Log.Info($"Browser: {browserName}"); 1085 | 1086 | switch (browserName) 1087 | { 1088 | case "chrome": 1089 | return new ChromeDriver(FW.WORKSPACE_DIRECTORY + "_drives"); 1090 | 1091 | case "firefox": 1092 | return new FirefoxDriver(); 1093 | 1094 | default: 1095 | throw new System.ArgumentException($"{browserName} not supported."); 1096 | } 1097 | } 1098 | } 1099 | ``` 1100 | 1101 | - This handles creating our ChromeDriver, but also allows you to define how you want your different drivers to be created 1102 | 1103 | 2. Use this is our Driver 1104 | 1105 | ```c# 1106 | public static void Init(string browserName) 1107 | { 1108 | _driver = DriverFactory.Build(browserName); 1109 | Wait = new Wait(10); 1110 | } 1111 | ``` 1112 | 1113 | 3. Update our `[SetUp]` methods since we now require a browserName in `Driver.Init()` 1114 | 1115 | ```c# 1116 | [SetUp] 1117 | public void BeforeEach() 1118 | { 1119 | FW.SetLogger(); 1120 | Driver.Init("chrome"); 1121 | Pages.Init(); 1122 | Driver.Goto("https://statsroyale.com"); 1123 | } 1124 | ``` 1125 | 1126 | 4. Create a `framework-config.json` at the workspace root 1127 | 1128 | ```javascript 1129 | { 1130 | "driver": { 1131 | "browser": "chrome" 1132 | }, 1133 | 1134 | "test": { 1135 | "url": "staging.statsroyale.com" 1136 | } 1137 | } 1138 | ``` 1139 | 1140 | 5. Create `FwConfig.cs` under `Framework` to represent the json in code: 1141 | 1142 | ```c# 1143 | public class FwConfig 1144 | { 1145 | public DriverSettings Driver { get; set; } 1146 | 1147 | public TestSettings Test { get; set; } 1148 | } 1149 | 1150 | public class DriverSettings 1151 | { 1152 | public string Browser { get; set; } 1153 | } 1154 | 1155 | public class TestSettings 1156 | { 1157 | public string Url { get; set; } 1158 | } 1159 | ``` 1160 | 1161 | - `DriverSettings` represents the "driver" object in the json 1162 | - `TestSettings` represents the "test" object in the json 1163 | - `FwConfig` represents the entire config file 1164 | 1165 | 6. Add a "singleton" representation of our config in the `FW` class 1166 | 1167 | ```c# 1168 | public static FwConfig Config => _configuration ?? throw new NullReferenceException("Config is null. Call FW.SeConfig() first."); 1169 | 1170 | private static FwConfig _configuration; 1171 | ``` 1172 | 1173 | 7. Now add the `SetConfig()` method 1174 | 1175 | ```c# 1176 | public static void SetConfig() 1177 | { 1178 | if (_configuration == null) 1179 | { 1180 | var jsonStr = File.ReadAllText(WORKSPACE_DIRECTORY + "/framework-config.json"); 1181 | _configuration = JsonConvert.DeserializeObject(jsonStr); 1182 | } 1183 | } 1184 | ``` 1185 | 1186 | - We check if the _configuration is null 1187 | - if null, then set it 1188 | - if not null, then it has already been set 1189 | - We are getting the .json file and turning it into a json string 1190 | - Then we deserialize it into the shape of our `FwConfig` so it can be used in code 1191 | 1192 | 8. Go back to our Driver class and use the config 1193 | 1194 | ```c# 1195 | public static void Init() 1196 | { 1197 | _driver = DriverFactory.Build(FW.Config.Driver.Browser); 1198 | Wait = new Wait(10); 1199 | } 1200 | ``` 1201 | 1202 | 9. Update our test files to use the config too 1203 | 1204 | ```c# 1205 | [OneTimeSetUp] 1206 | public void BeforeAll() 1207 | { 1208 | FW.SetConfig(); 1209 | FW.CreateTestResultsDirectory(); 1210 | } 1211 | 1212 | [SetUp] 1213 | public void BeforeEach() 1214 | { 1215 | FW.SetLogger(); 1216 | Driver.Init(); 1217 | Pages.Init(); 1218 | Driver.Goto(FW.Config.Test.Url); 1219 | } 1220 | ``` 1221 | 1222 | - `FW.SetConfig();` in the [OneTimeSetUp] 1223 | - `Driver.Goto(FW.Config.Test.Url);` in the [SetUp] 1224 | 1225 | 10. Run the `copydeck` tests 1226 | 1227 | 11. They failed! Take a look at the log files to see what happened 1228 | 1229 | - Our test logged this: `[INFO]: http://staging.statsroyale.com` 1230 | - It tried navigating to a "staging" version of the site, but we don't have access 1231 | 1232 | 12. Update the `test.url` value in our `framework-config.json` 1233 | 1234 | ```javascript 1235 | { 1236 | "driver": { 1237 | "browser": "chrome" 1238 | }, 1239 | 1240 | "test": { 1241 | "url": "statsroyale.com" 1242 | } 1243 | } 1244 | ``` 1245 | 1246 | 13. CHALLENGE: I bet you already have some ideas for things we could put into our framework-config.json. For this challenge, add a "wait" property to hold the default number of seconds that we want to use when instantiating our Wait class. 1247 | 1248 | > HINT: We're instantiating Wait in Driver.Init() 1249 | 1250 | 1251 | ## Chapter 11 - TestBase and Outcomes 1252 | 1253 | 1. Create a `Base` folder within `Royale.Tests` 1254 | 1255 | 2. Create `TestBase.cs` within that `Base` folder 1256 | 1257 | 3. In one of our test suites, copy the `[OneTimeSetUp]`, `[SetUp]` and `[TearDown]` methods and paste them into our TestBase class 1258 | 1259 | ```c# 1260 | public abstract class TestBase 1261 | { 1262 | [OneTimeSetUp] 1263 | public virtual void BeforeAll() 1264 | { 1265 | FW.SetConfig(); 1266 | FW.CreateTestResultsDirectory(); 1267 | } 1268 | 1269 | [SetUp] 1270 | public virtual void BeforeEach() 1271 | { 1272 | FW.SetLogger(); 1273 | Driver.Init(); 1274 | Pages.Init(); 1275 | Driver.Goto(FW.Config.Test.Url); 1276 | } 1277 | 1278 | [TearDown] 1279 | public virtual void AfterEach() 1280 | { 1281 | Driver.Quit(); 1282 | } 1283 | } 1284 | ``` 1285 | 1286 | - TestBase is an abstract class 1287 | 1288 | 4. In our test suites, delete the `[OneTimeSetUp]`, `[SetUp]` and `[TearDown]` methods 1289 | 1290 | 5. Our test classes should now inherit TestBase 1291 | 1292 | ```c# 1293 | public class CopyDeckTests : TestBase 1294 | ``` 1295 | 1296 | 6. Run a test and you'll see nothing has changed! 1297 | 1298 | 7. In our `[TearDown]` method, now in TestBase, we'll handle the outcome of the test 1299 | 1300 | ```c# 1301 | [TearDown] 1302 | public virtual void AfterEach() 1303 | { 1304 | var outcome = TestContext.CurrentContext.Result.Outcome.Status; 1305 | 1306 | if (outcome == TestStatus.Passed) 1307 | { 1308 | FW.Log.Info("Outcome: Passed"); 1309 | } 1310 | else if (outcome == TestStatus.Failed) 1311 | { 1312 | FW.Log.Info("Outcome: Failed"); 1313 | } 1314 | else 1315 | { 1316 | FW.Log.Warning("Outcome: " + outcome); 1317 | } 1318 | 1319 | Driver.Quit(); 1320 | } 1321 | ``` 1322 | 1323 | 8. We want to take a screenshot if our test fails. Let's add that functionality somewhere in our Driver class 1324 | 1325 | ```c# 1326 | public static void TakeScreenshot(string imageName) 1327 | { 1328 | var ss = ((ITakesScreenshot)Current).GetScreenshot(); 1329 | var ssFileName = Path.Combine(FW.CurrentTestDirectory.FullName, imageName); 1330 | ss.SaveAsFile($"{ssFileName}.png", ScreenshotImageFormat.Png); 1331 | } 1332 | ``` 1333 | 1334 | 9. Now add `TakeScreenshot()` when our test fails 1335 | 1336 | ```c# 1337 | else if (outcome == TestStatus.Failed) 1338 | { 1339 | Driver.TakeScreenshot("test_failed"); 1340 | FW.Log.Info("Outcome: Failed"); 1341 | } 1342 | ``` 1343 | 1344 | 10. Run a test that fails. You may need to force a failure with `Assert.Fail();` 1345 | 1346 | 11. Open the test's result directory 1347 | - There should be a `test_failed.png` along with the `log.txt` 1348 | 1349 | #### BONUS: Maximizing a Window 1350 | 1351 | We touched on this in Chapter 2. For Windows, it's pretty easy: 1352 | 1353 | ```c# 1354 | public IWebDriver BuildChrome() 1355 | { 1356 | var options = new ChromeOptions(); 1357 | options.AddArgument("--start-maximized"); 1358 | return new ChromeDriver(options); 1359 | } 1360 | ``` 1361 | 1362 | or 1363 | 1364 | ```c# 1365 | driver.Manage().Window.Maximize(); 1366 | ``` 1367 | 1368 | However, this may not work for Mac or Linux. Let's go over a method that works cross-platform. 1369 | 1370 | 1. Create `Window.cs` under `Framework.Selenium` 1371 | 1372 | 2. We'll add a few methods to start out 1373 | 1374 | ```c# 1375 | public ReadOnlyCollection CurrentWindows => Driver.Current.WindowHandles; 1376 | 1377 | public void SwitchTo(int windowIndex) 1378 | { 1379 | Driver.Current.SwitchTo().Window(CurrentWindows[windowIndex]); 1380 | } 1381 | ``` 1382 | 1383 | 3. Add a property to get the current ScreenSize so we know the amount of space we're working with 1384 | 1385 | > NOTE: This is different than what's in the video because of changes made to .NET Standard and Selenium 1386 | 1387 | ```c# 1388 | public Size ScreenSize 1389 | { 1390 | get 1391 | { 1392 | var js = "return [window.screen.availWidth, window.screen.availHeight];"; 1393 | var jse = (IJavaScriptExecutor)Driver.Current; 1394 | 1395 | dynamic dimensions = jse.ExecuteScript(js, null); 1396 | var x = Convert.ToInt32(dimensions[0]); 1397 | var y = Convert.ToInt32(dimensions[1]); 1398 | 1399 | return new Size(x, y); 1400 | } 1401 | } 1402 | ``` 1403 | 1404 | 4. Create a `Maximize()` method that uses the ScreenSize 1405 | 1406 | ```c# 1407 | public void Maximize() 1408 | { 1409 | Driver.Current.Manage().Window.Position = new Point(0, 0); 1410 | Driver.Current.Manage().Window.Size = ScreenSize; 1411 | } 1412 | ``` 1413 | 1414 | 5. Add the Window class to our Driver 1415 | 1416 | ```c# 1417 | [ThreadStatic] 1418 | public static Window Window; 1419 | 1420 | public static void Init() 1421 | { 1422 | _driver = DriverFactory.Build(FW.Config.Driver.Browser); 1423 | Wait = new Wait(FW.Config.Driver.WaitSeconds); 1424 | Window = new Window(); 1425 | Window.Maximize(); 1426 | } 1427 | ``` 1428 | 1429 | 6. Run a test. It should spin up a browser and then maximize! 1430 | 1431 | 1432 | ## Chapter 12 - Wait Conditions 1433 | 1434 | 1. Install the `Selenium Extras` package with PackSharp 1435 | 1436 | - Open Command Palette > `PackSharp: Add Package` > select `Framework` > search "Selenium Extras" > select `DotNetSeleniumExtras.WaitHelpers` 1437 | 1438 | 2. Use ExpectedConditions in our No() method 1439 | 1440 | > NOTE: Make sure to use it from the `SeleniumExtras.WaitHelpers` and NOT from `OpenQA.Selenium.Support.UI` 1441 | 1442 | ```c# 1443 | Driver.Wait.Until(ExpectedConditions.ElementIsVisible(Map.OtherStoresButton.FoundBy)); 1444 | ``` 1445 | 1446 | 3. We need to solve the error by adding an overload of Until() in our Wait class 1447 | 1448 | ```c# 1449 | public IWebElement Until(Func condition) 1450 | { 1451 | return _wait.Until(condition); 1452 | } 1453 | ``` 1454 | 1455 | 4. Our No() method should now look like this 1456 | 1457 | ```c# 1458 | public CopyDeckPage No() 1459 | { 1460 | Map.NoButton.Click(); 1461 | AcceptCookies(); 1462 | Driver.Wait.Until(ExpectedConditions.ElementIsVisible(Map.OtherStoresButton.FoundBy)); 1463 | return this; 1464 | } 1465 | ``` 1466 | 1467 | 5. Create `WaitConditions.cs` under `Framework.Selenium` 1468 | 1469 | ```c# 1470 | public sealed class WaitConditions 1471 | { 1472 | public static Func ElementDisplayed(IWebElement element) 1473 | { 1474 | bool condition(IWebDriver driver) 1475 | { 1476 | return element.Displayed; 1477 | } 1478 | 1479 | return condition; 1480 | } 1481 | } 1482 | ``` 1483 | 1484 | 6. Use our WaitConditions in the DeckBuilderPage's `AddCardsManually()` method 1485 | 1486 | ```c# 1487 | public void AddCardsManually() 1488 | { 1489 | Driver.Wait.Until(WaitConditions.ElementDisplayed(Map.AddCardsManuallyLink)); 1490 | Map.AddCardsManuallyLink.Click(); 1491 | Driver.Wait.Until(WaitConditions.ElementDisplayed(Map.CopyDeckIcon)); 1492 | } 1493 | ``` 1494 | 1495 | 7. We also need to handle elements that need to disappear. Let's add it to our WaitConditions class 1496 | 1497 | ```c# 1498 | public static Func ElementNotDisplayed(IWebElement element) 1499 | { 1500 | bool condition(IWebDriver driver) 1501 | { 1502 | try 1503 | { 1504 | return !element.Displayed; 1505 | } 1506 | catch (StaleElementReferenceException) 1507 | { 1508 | return true; 1509 | } 1510 | } 1511 | 1512 | return condition; 1513 | } 1514 | ``` 1515 | 1516 | 8. Now we'll use this in our `AcceptCookies()` method in the CopyDeckPage 1517 | 1518 | ```c# 1519 | public void AcceptCookies() 1520 | { 1521 | Map.AcceptCookiesButton.Click(); 1522 | Driver.Wait.Until(WaitConditions.ElementNotDisplayed(Map.AcceptCookiesButton)); 1523 | } 1524 | ``` 1525 | 1526 | 9. With our Waits, we can return more than just booleans. Let's create a WaitCondition that returns a list of Elements once at least one of them is found. Add this to our WaitConditions class: 1527 | 1528 | ```c# 1529 | public static Func ElementsNotEmpty(Elements elements) 1530 | { 1531 | Elements condition(IWebDriver driver) 1532 | { 1533 | Elements _elements = Driver.FindElements(elements.FoundBy); 1534 | return _elements.IsEmpty ? null : _elements; 1535 | } 1536 | 1537 | return condition; 1538 | } 1539 | ``` 1540 | 1541 | 10. Add another Wait that returns an element instead of just bool. This WaitCondition will solve us waiting for an element to be displayed on one line, and then clicking it in the next line. 1542 | 1543 | ```c# 1544 | public static Func ElementIsDisplayed(IWebElement element) 1545 | { 1546 | IWebElement condition(IWebDriver driver) 1547 | { 1548 | try 1549 | { 1550 | return element.Displayed ? element : null; 1551 | } 1552 | catch (NoSuchElementException) 1553 | { 1554 | return null; 1555 | } 1556 | catch (ElementNotVisibleException) 1557 | { 1558 | return null; 1559 | } 1560 | } 1561 | 1562 | return condition; 1563 | } 1564 | ``` 1565 | 1566 | 11. Now let's use it in the AddCardsManually() method 1567 | 1568 | ```c# 1569 | public void AddCardsManually() 1570 | { 1571 | Driver.Wait.Until( 1572 | WaitConditions.ElementIsDisplayed(Map.AddCardsManuallyLink)) 1573 | .Click(); 1574 | Driver.Wait.Until(WaitConditions.ElementDisplayed(Map.CopyDeckIcon)); 1575 | } 1576 | ``` 1577 | 1578 | 12. CHALLENGE: I didn't run the test just so we could do this challenge :) 1579 | 1580 | When working with web elements, we usually only work with elements that we expect to be present on the DOM. At least, that's what our users do. Because of this, it makes sense for us to "wait" for elements to at least be present in the DOM before returning it. 1581 | 1582 | In this challenge, you need to add a "Wait" to our `Driver.FindElement()` method so every time we "find" an element, we wait until it exists before returning it. 1583 | 1584 | > HINT: Give it a try, but you can look at the last commit to see how this is done. 1585 | > HINT: Your tests will most likely fail on the `AddCardsManually()` step because when we call `Map.AddCardsManuallyButton`, we are having the Driver find the element at that time instead of within a Wait. Solving this challenge will also solve that issue. 1586 | 1587 | That's it! You are now done with the course :) 1588 | -------------------------------------------------------------------------------- /Royale.Tests/Base/TestBase.cs: -------------------------------------------------------------------------------- 1 | using Framework; 2 | using Framework.Selenium; 3 | using NUnit.Framework; 4 | using NUnit.Framework.Interfaces; 5 | using Royale.Pages; 6 | 7 | namespace Tests.Base 8 | { 9 | public abstract class TestBase 10 | { 11 | [OneTimeSetUp] 12 | public virtual void BeforeAll() 13 | { 14 | FW.SetConfig(); 15 | FW.CreateTestResultsDirectory(); 16 | } 17 | 18 | [SetUp] 19 | public virtual void BeforeEach() 20 | { 21 | FW.SetLogger(); 22 | Driver.Init(); 23 | Pages.Init(); 24 | Driver.Goto(FW.Config.Test.Url); 25 | } 26 | 27 | [TearDown] 28 | public virtual void AfterEach() 29 | { 30 | var outcome = TestContext.CurrentContext.Result.Outcome.Status; 31 | 32 | if (outcome == TestStatus.Passed) 33 | { 34 | FW.Log.Info("Outcome: Passed"); 35 | } 36 | else if (outcome == TestStatus.Failed) 37 | { 38 | Driver.TakeScreenshot("test_failed"); 39 | FW.Log.Info("Outcome: Failed"); 40 | } 41 | else 42 | { 43 | FW.Log.Warning("Outcome: " + outcome); 44 | } 45 | 46 | Driver.Quit(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Royale.Tests/CardTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Framework; 5 | using Framework.Models; 6 | using Framework.Selenium; 7 | using Framework.Services; 8 | using NUnit.Framework; 9 | using OpenQA.Selenium; 10 | using OpenQA.Selenium.Chrome; 11 | using Royale.Pages; 12 | using Tests.Base; 13 | 14 | namespace Tests 15 | { 16 | public class CardTests : TestBase 17 | { 18 | static IList apiCards = new ApiCardService().GetAllCards(); 19 | 20 | [Test, Category("cards")] 21 | [TestCaseSource("apiCards")] 22 | [Parallelizable(ParallelScope.Children)] 23 | public void Card_is_on_Cards_Page(Card card) 24 | { 25 | var cardOnPage = Pages.Cards.Goto().GetCardByName(card.Name); 26 | Assert.That(cardOnPage.Displayed); 27 | } 28 | 29 | [Test, Category("cards")] 30 | [TestCaseSource("apiCards")] 31 | [Parallelizable(ParallelScope.Children)] 32 | public void Card_headers_are_correct_on_Card_Details_Page(Card card) 33 | { 34 | Pages.Cards.Goto().GetCardByName(card.Name).Click(); 35 | 36 | var cardOnPage = Pages.CardDetails.GetBaseCard(); 37 | 38 | if (cardOnPage.Type == "troop") 39 | cardOnPage.Type = "character"; 40 | 41 | Assert.AreEqual(card.Name, cardOnPage.Name); 42 | Assert.AreEqual(card.Arena, cardOnPage.Arena); 43 | Assert.AreEqual(card.Rarity, cardOnPage.Rarity); 44 | Assert.That(card.Type.Contains(cardOnPage.Type)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Royale.Tests/CopyDeckTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Framework; 4 | using Framework.Selenium; 5 | using NUnit.Framework; 6 | using OpenQA.Selenium; 7 | using OpenQA.Selenium.Support.UI; 8 | using Royale.Pages; 9 | using Tests.Base; 10 | 11 | namespace Tests 12 | { 13 | public class CopyDeckTests : TestBase 14 | { 15 | [Test, Category("copydeck")] 16 | public void User_can_copy_the_deck() 17 | { 18 | Pages.DeckBuilder.Goto().AddCardsManually(); 19 | Pages.DeckBuilder.CopySuggestedDeck(); 20 | Pages.CopyDeck.Yes(); 21 | Assert.That(Pages.CopyDeck.Map.CopiedMessage.Displayed); 22 | } 23 | 24 | [Test, Category("copydeck")] 25 | public void User_opens_app_store() 26 | { 27 | Pages.DeckBuilder.Goto().AddCardsManually(); 28 | Pages.DeckBuilder.CopySuggestedDeck(); 29 | Pages.CopyDeck.No().OpenAppStore(); 30 | 31 | // A solution to Challenge 7b 32 | // Remove the Unicode character `\u0200e` by "replacing" it with empty 33 | var title = Regex.Replace(Driver.Title, @"\u0200e", string.Empty); 34 | 35 | Assert.That(title, Is.EqualTo("‎Clash Royale on the App Store")); 36 | } 37 | 38 | [Test, Category("copydeck")] 39 | public void User_opens_google_play() 40 | { 41 | Pages.DeckBuilder.Goto().AddCardsManually(); 42 | Pages.DeckBuilder.CopySuggestedDeck(); 43 | Pages.CopyDeck.No().OpenGooglePlay(); 44 | Assert.AreEqual("Clash Royale - Apps on Google Play", Driver.Title); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Royale.Tests/Royale.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Royale/Pages/CardDetailsPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Framework.Models; 4 | using Framework.Selenium; 5 | using OpenQA.Selenium; 6 | 7 | namespace Royale.Pages 8 | { 9 | public class CardDetailsPage : PageBase 10 | { 11 | public readonly CardDetailsPageMap Map; 12 | 13 | public CardDetailsPage() 14 | { 15 | Map = new CardDetailsPageMap(); 16 | } 17 | 18 | public (string Type, int Arena) GetCardCategory() 19 | { 20 | var categories = Map.CardCategory.Text.Split(','); // "Troop, Arena 8" => ["Troop", " Arena 8"] 21 | var type = categories[0].ToLower(); // "Troop" => "troop" 22 | var arena = categories[1].Trim().Split(' ').Last(); // " Arena 8" => "8" 23 | 24 | int intArena; 25 | if (int.TryParse(arena, out intArena)) 26 | { 27 | return (type, intArena); 28 | } 29 | else 30 | { 31 | // The Arena was "Training Camp" and should return 0 32 | return (type, 0); 33 | } 34 | } 35 | 36 | public Card GetBaseCard() 37 | { 38 | var (type, arena) = GetCardCategory(); 39 | 40 | return new Card 41 | { 42 | Name = Map.CardName.Text, 43 | Rarity = Map.CardRarity.Text.Split('\n').Last(), 44 | Type = type, 45 | Arena = arena 46 | }; 47 | } 48 | } 49 | 50 | public class CardDetailsPageMap 51 | { 52 | public Element CardName => Driver.FindElement(By.CssSelector("div[class*='cardName']"), "Card Name"); 53 | 54 | public Element CardCategory => Driver.FindElement(By.CssSelector("div[class*='card__rarity']"), "Card Category"); 55 | 56 | public Element CardRarity => Driver.FindElement(By.CssSelector("div[class*='rarityCaption']"), "Card Rarity"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Royale/Pages/CardsPage.cs: -------------------------------------------------------------------------------- 1 | using Framework.Selenium; 2 | using OpenQA.Selenium; 3 | 4 | namespace Royale.Pages 5 | { 6 | public class CardsPage : PageBase 7 | { 8 | public readonly CardsPageMap Map; 9 | 10 | public CardsPage() 11 | { 12 | Map = new CardsPageMap(); 13 | } 14 | 15 | public CardsPage Goto() 16 | { 17 | HeaderNav.GotoCardsPage(); 18 | return this; 19 | } 20 | 21 | public Element GetCardByName(string cardName) 22 | { 23 | if (cardName.Contains(" ")) 24 | { 25 | cardName = cardName.Replace(" ", "+"); 26 | } 27 | 28 | return Map.Card(cardName); 29 | } 30 | } 31 | 32 | public class CardsPageMap 33 | { 34 | public Element Card(string name) => Driver.FindElement(By.CssSelector($"a[href*='{name}']"), $"Card: {name}"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Royale/Pages/CopyDeckPage.cs: -------------------------------------------------------------------------------- 1 | using Framework.Selenium; 2 | using OpenQA.Selenium; 3 | using SeleniumExtras.WaitHelpers; 4 | 5 | namespace Royale.Pages 6 | { 7 | public class CopyDeckPage 8 | { 9 | public readonly CopyDeckPageMap Map; 10 | 11 | public CopyDeckPage() 12 | { 13 | Map = new CopyDeckPageMap(); 14 | } 15 | 16 | public CopyDeckPage Yes() 17 | { 18 | Map.YesButton.Click(); 19 | Driver.Wait.Until(drvr => Map.CopiedMessage.Displayed); 20 | return this; 21 | } 22 | 23 | public CopyDeckPage No() 24 | { 25 | Map.NoButton.Click(); 26 | AcceptCookies(); 27 | Driver.Wait.Until(ExpectedConditions.ElementIsVisible(Map.OtherStoresButton.FoundBy)); 28 | return this; 29 | } 30 | 31 | public void AcceptCookies() 32 | { 33 | Map.AcceptCookiesButton.Click(); 34 | Driver.Wait.Until(WaitConditions.ElementNotDisplayed(Map.AcceptCookiesButton)); 35 | } 36 | 37 | public void OpenAppStore() 38 | { 39 | Map.AppStoreButton.Click(); 40 | } 41 | 42 | public void OpenGooglePlay() 43 | { 44 | Map.GooglePlayButton.Click(); 45 | } 46 | } 47 | 48 | public class CopyDeckPageMap 49 | { 50 | public Element YesButton => Driver.FindElement(By.Id("button-open"), "Yes Button"); 51 | 52 | public Element CopiedMessage => Driver.FindElement(By.CssSelector(".notes.active"), "Copied Message"); 53 | 54 | public Element NoButton => Driver.FindElement(By.Id("not-installed"), "No Button"); 55 | 56 | public Element AppStoreButton => Driver.FindElement(By.XPath("//a[text()='App Store']"), "App Store Button"); 57 | 58 | public Element GooglePlayButton => Driver.FindElement(By.XPath("//a[text()='Google Play']"), "Google Play Button"); 59 | 60 | public Element AcceptCookiesButton => Driver.FindElement(By.CssSelector("a.cc-btn.cc-dismiss"), "Accept Cookies Button"); 61 | 62 | public Element OtherStoresButton => Driver.FindElement(By.Id("other-stores"), "Other Stores Button"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Royale/Pages/DeckBuilderPage.cs: -------------------------------------------------------------------------------- 1 | using Framework; 2 | using Framework.Selenium; 3 | using OpenQA.Selenium; 4 | 5 | namespace Royale.Pages 6 | { 7 | public class DeckBuilderPage : PageBase 8 | { 9 | public readonly DeckBuilderPageMap Map; 10 | 11 | public DeckBuilderPage() 12 | { 13 | Map = new DeckBuilderPageMap(); 14 | } 15 | 16 | public DeckBuilderPage Goto() 17 | { 18 | HeaderNav.Map.DeckBuilderLink.Click(); 19 | return this; 20 | } 21 | 22 | public void AddCardsManually() 23 | { 24 | Driver.Wait.Until( 25 | WaitConditions.ElementIsDisplayed(Map.AddCardsManuallyLink)) 26 | .Click(); 27 | Driver.Wait.Until(WaitConditions.ElementDisplayed(Map.CopyDeckIcon)); 28 | } 29 | 30 | public void CopySuggestedDeck() 31 | { 32 | Map.CopyDeckIcon.Click(); 33 | } 34 | } 35 | 36 | public class DeckBuilderPageMap 37 | { 38 | public Element AddCardsManuallyLink => Driver.FindElement(By.XPath("//a[text()='add cards manually']"), "Add Cards Manullay Link"); 39 | 40 | public Element CopyDeckIcon => Driver.FindElement(By.CssSelector(".copyButton"), "Copy Deck Button"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Royale/Pages/HeaderNav.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Framework.Selenium; 3 | using OpenQA.Selenium; 4 | 5 | namespace Royale.Pages 6 | { 7 | public class HeaderNav 8 | { 9 | public readonly HeaderNavMap Map; 10 | 11 | public HeaderNav() 12 | { 13 | Map = new HeaderNavMap(); 14 | } 15 | 16 | public void GotoCardsPage() 17 | { 18 | Map.CardsTabLink.Click(); 19 | } 20 | } 21 | 22 | public class HeaderNavMap 23 | { 24 | public Element CardsTabLink => Driver.FindElement(By.CssSelector("a[href='/cards']"), "Cards Link"); 25 | 26 | public Element DeckBuilderLink => Driver.FindElement(By.CssSelector("a[href='/deckbuilder']"), "Deck Builder Link"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Royale/Pages/PageBase.cs: -------------------------------------------------------------------------------- 1 | namespace Royale.Pages 2 | { 3 | public abstract class PageBase 4 | { 5 | public readonly HeaderNav HeaderNav; 6 | 7 | public PageBase() 8 | { 9 | HeaderNav = new HeaderNav(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Royale/Pages/Pages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Framework.Selenium; 3 | 4 | namespace Royale.Pages 5 | { 6 | public class Pages 7 | { 8 | [ThreadStatic] 9 | public static CardsPage Cards; 10 | 11 | [ThreadStatic] 12 | public static CardDetailsPage CardDetails; 13 | 14 | [ThreadStatic] 15 | public static DeckBuilderPage DeckBuilder; 16 | 17 | [ThreadStatic] 18 | public static CopyDeckPage CopyDeck; 19 | 20 | public static void Init() 21 | { 22 | Cards = new CardsPage(); 23 | CardDetails = new CardDetailsPage(); 24 | DeckBuilder = new DeckBuilderPage(); 25 | CopyDeck = new CopyDeckPage(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Royale/Royale.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | netstandard2.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /StatsRoyale.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Framework", "Framework\Framework.csproj", "{0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Royale", "Royale\Royale.csproj", "{A33248E0-85EE-4939-A533-403A2F7FC302}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Royale.Tests", "Royale.Tests\Royale.Tests.csproj", "{F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|x64.Build.0 = Debug|Any CPU 29 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Debug|x86.Build.0 = Debug|Any CPU 31 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|x64.ActiveCfg = Release|Any CPU 34 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|x64.Build.0 = Release|Any CPU 35 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|x86.ActiveCfg = Release|Any CPU 36 | {0E31A57A-1E7B-4B1D-834A-0DDAD0B7D847}.Release|x86.Build.0 = Release|Any CPU 37 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|x64.Build.0 = Debug|Any CPU 41 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Debug|x86.Build.0 = Debug|Any CPU 43 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|x64.ActiveCfg = Release|Any CPU 46 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|x64.Build.0 = Release|Any CPU 47 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|x86.ActiveCfg = Release|Any CPU 48 | {A33248E0-85EE-4939-A533-403A2F7FC302}.Release|x86.Build.0 = Release|Any CPU 49 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|x64.Build.0 = Debug|Any CPU 53 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Debug|x86.Build.0 = Debug|Any CPU 55 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|x64.ActiveCfg = Release|Any CPU 58 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|x64.Build.0 = Release|Any CPU 59 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|x86.ActiveCfg = Release|Any CPU 60 | {F61D93BA-F5C5-4D5B-B968-33ECBD6D95E5}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /_drivers/chromedriver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElSnoMan/from-scripting-to-framework/95588efdb896a9b2a68d0f125e3f16e0bbf4b199/_drivers/chromedriver -------------------------------------------------------------------------------- /framework-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "driver": { 3 | "browser": "chrome", 4 | "waitSeconds": 10 5 | }, 6 | 7 | "test": { 8 | "url": "statsroyale.com" 9 | } 10 | } 11 | --------------------------------------------------------------------------------