Skip to content

Commit

Permalink
Merge pull request #266 from q2ebanking/web-example
Browse files Browse the repository at this point in the history
Rewrote Web UI example test for Wikipedia
  • Loading branch information
AutomationPanda authored May 29, 2023
2 parents fdad706 + 00d4256 commit a681ae1
Show file tree
Hide file tree
Showing 20 changed files with 306 additions and 300 deletions.
2 changes: 1 addition & 1 deletion Boa.Constrictor.Example/Boa.Constrictor.Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>3.0.4</Version>
<Version>3.1.0</Version>
<Authors>Pandy Knight and the PrecisionLender SETs</Authors>
<Company>Q2</Company>
<Title>Boa.Constrictor.Example</Title>
Expand Down
7 changes: 7 additions & 0 deletions Boa.Constrictor.Example/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Versioning is performed purely for tracking changes.
(none)


## [3.1.0] - 2023-05-28

### Changed

- Replaced DuckDuckGo search test with Wikipedia search test since DuckDuckGo changes their home page


## [3.0.4] - 2022-12-13

### Changed
Expand Down
22 changes: 0 additions & 22 deletions Boa.Constrictor.Example/Interactions/SearchDuckDuckGo.cs

This file was deleted.

22 changes: 22 additions & 0 deletions Boa.Constrictor.Example/Interactions/SearchWikipedia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Boa.Constrictor.Screenplay;
using Boa.Constrictor.Selenium;

namespace Boa.Constrictor.Example
{
public class SearchWikipedia : ITask
{
public string Phrase { get; }

private SearchWikipedia(string phrase) =>
Phrase = phrase;

public static SearchWikipedia For(string phrase) =>
new SearchWikipedia(phrase);

public void PerformAs(IActor actor)
{
actor.AttemptsTo(SendKeys.To(MainPage.SearchInput, Phrase));
actor.AttemptsTo(Click.On(MainPage.SearchButton));
}
}
}
13 changes: 13 additions & 0 deletions Boa.Constrictor.Example/Pages/ArticlePage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Boa.Constrictor.Selenium;
using OpenQA.Selenium;
using static Boa.Constrictor.Selenium.WebLocator;

namespace Boa.Constrictor.Example
{
public static class ArticlePage
{
public static IWebLocator Title => L(
"Title Span",
By.CssSelector("[id='firstHeading'] span"));
}
}
19 changes: 19 additions & 0 deletions Boa.Constrictor.Example/Pages/MainPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Boa.Constrictor.Selenium;
using OpenQA.Selenium;
using static Boa.Constrictor.Selenium.WebLocator;

namespace Boa.Constrictor.Example
{
public static class MainPage
{
public const string Url = "https://en.wikipedia.org/wiki/Main_Page";

public static IWebLocator SearchButton => L(
"Wikipedia Search Button",
By.XPath("//button[text()='Search']"));

public static IWebLocator SearchInput => L(
"Wikipedia Search Input",
By.Name("search"));
}
}
13 changes: 0 additions & 13 deletions Boa.Constrictor.Example/Pages/ResultPage.cs

This file was deleted.

19 changes: 0 additions & 19 deletions Boa.Constrictor.Example/Pages/SearchPage.cs

This file was deleted.

13 changes: 7 additions & 6 deletions Boa.Constrictor.Example/Tests/ScreenplayWebUiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public class ScreenplayWebUiTest
public void InitializeScreenplay()
{
ChromeOptions options = new ChromeOptions();
options.AddArgument("headless"); // Remove this line to "see" the browser run
options.AddArgument("headless"); // Remove this line to "see" the browser run
options.AddArgument("window-size=1920,1080"); // Use this option with headless mode
ChromeDriver driver = new ChromeDriver(options);

Actor = new Actor(name: "Andy", logger: new ConsoleLogger());
Expand All @@ -28,12 +29,12 @@ public void QuitBrowser()
}

[Test]
public void TestDuckDuckGoWebSearch()
public void TestWikipediaSearch()
{
Actor.AttemptsTo(Navigate.ToUrl(SearchPage.Url));
Actor.AskingFor(ValueAttribute.Of(SearchPage.SearchInput)).Should().BeEmpty();
Actor.AttemptsTo(SearchDuckDuckGo.For("panda"));
Actor.WaitsUntil(Appearance.Of(ResultPage.ResultLinks), IsEqualTo.True());
Actor.AttemptsTo(Navigate.ToUrl(MainPage.Url));
Actor.AskingFor(ValueAttribute.Of(MainPage.SearchInput)).Should().BeEmpty();
Actor.AttemptsTo(SearchWikipedia.For("Giant panda"));
Actor.WaitsUntil(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda"));
}
}
}
94 changes: 47 additions & 47 deletions docs/pages/main-docs/getting-started/page-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ It reveals pain points at each step to show why the Screenplay Pattern is ultima

## Phase 1: Raw WebDriver Calls

Let's say you want to automate a test that performs a [DuckDuckGo](https://duckduckgo.com/) web search.
Let's say you want to automate a test that searches for an article on [Wikipedia](https://en.wikipedia.org/wiki/Main_Page).
You could simply write raw [Selenium WebDriver](https://www.selenium.dev/documentation/en/webdriver/) code like this:

```csharp
// Initialize the WebDriver
IWebDriver driver = new ChromeDriver();

// Open the search engine
driver.Navigate().GoToUrl("https://duckduckgo.com/");
driver.Navigate().GoToUrl("https://en.wikipedia.org/wiki/Main_Page");

// Search for a phrase
driver.FindElement(By.Id("search_form_input_homepage")).SendKeys("panda");
driver.FindElement(By.Id("search_button_homepage")).Click();
driver.FindElement(By.Name("search")).SendKeys("Giant panda");
driver.FindElement(By.XPath("//button[text()='Search']")).Click();

// Verify results appear
driver.Title.ToLower().Should().Contain("panda");
driver.FindElements(By.CssSelector("a.result__a")).Should().BeGreaterThan(0);
driver.FindElement(By.CssSelector("[id='firstHeading'] span")).Text.Should().Be("Giant panda");

// Quit the WebDriver
driver.Quit();
Expand Down Expand Up @@ -65,24 +65,24 @@ IWebDriver driver = new ChromeDriver();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(30));

// Open the search engine
driver.Navigate().GoToUrl("https://duckduckgo.com/");
driver.Navigate().GoToUrl("https://en.wikipedia.org/wiki/Main_Page");

// Search for a phrase
wait.Until(d => d.FindElements(By.Id("search_form_input_homepage")).Count > 0);
driver.FindElement(By.Id("search_form_input_homepage")).SendKeys("panda");
driver.FindElement(By.Id("search_button_homepage")).Click();
wait.Until(d => d.FindElements(By.Name("search")).Count > 0);
driver.FindElement(By.Name("search")).SendKeys("Giant panda");
driver.FindElement(By.XPath("//button[text()='Search']")).Click();

// Verify results appear
wait.Until(d => d.Title.ToLower().Contains("panda"));
wait.Until(d => d.FindElements(By.CssSelector("a.result__a"))).Count > 0);
wait.Until(d => d.FindElements(By.CssSelector("[id='firstHeading'] span")).Text == "Giant Panda");

// Quit the WebDriver
driver.Quit();
```

These waits are necessary to make the code correct, but they cause new problems.
First, they cause duplicate code because Web element locators are used multiple times.
Notice how the locator `By.Id("search_form_input_homepage")` is written twice.
Notice how the locator `By.Name("search")` is written twice.
Second, raw calls with explicit waits make code more cryptic and less intuitive.
It is difficult to understand what this code does at a glance.

Expand All @@ -94,15 +94,15 @@ In the Page Object Model (or "Page Object Pattern"), each page is modeled as a c
So, a page object for the search page could look like this:

```csharp
public class SearchPage
public class MainPage
{
public const string Url = "https://duckduckgo.com/";
public static By SearchInput => By.Id("search_form_input_homepage");
public static By SearchButton => By.Id("search_button_homepage");
public const string Url = "https://en.wikipedia.org/wiki/Main_Page";
public static By SearchInput => By.Name("search");
public static By SearchButton => By.XPath("//button[text()='Search']");

public IWebDriver Driver { get; private set; }

public SearchPage(IWebDriver driver) => Driver = driver;
public MainPage(IWebDriver driver) => Driver = driver;

public void Load() => driver.Navigate().GoToUrl(Url);

Expand All @@ -117,25 +117,25 @@ public class SearchPage
```

This page object class has a decent structure and a mild separation of concerns.
The `SearchPage` class has locators (`SearchInput` and `SearchButton`) and interaction methods (`Load` and `Search`).
The `MainPage` class has locators (`SearchInput` and `SearchButton`) and interaction methods (`Load` and `Search`).
The `Search` method uses an explicit wait before attempting to interact with elements.
It also uses the locator properties so that locator queries are not duplicated.
Locators and interaction methods have meaningful names.
Page objects require a few more lines of code that raw calls at first, but their parts can be called easily.

The original test steps can be rewritten using this new `SearchPage` class, as well as a hypothetical `ResultPage` class.
The original test steps can be rewritten using this new `MainPage` class, as well as a hypothetical `ArticlePage` class.
This new code looks much cleaner:

```csharp
IWebDriver driver = new ChromeDriver();

SearchPage searchPage = new SearchPage(driver);
searchPage.Load();
searchPage.Search("panda");
MainPage mainPage = new MainPage(driver);
mainPage.Load();
mainPage.Search("Giant panda");

ResultPage resultPage = new ResultPage(driver);
resultPage.WaitForTitle("panda");
resultPage.WaitForResultLinks();
ArticlePage articlePage = new ArticlePage(driver);
articlePage.WaitForTitle("panda");
articlePage.WaitForArticleTitle();

driver.Quit();
```
Expand Down Expand Up @@ -184,7 +184,7 @@ public abstract class BasePage
public IWebDriver Driver { get; private set; }
public WebDriverWait Wait { get; private set; }

public SearchPage(IWebDriver driver)
public BasePage(IWebDriver driver)
{
Driver = driver;
Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(30));
Expand Down Expand Up @@ -272,53 +272,53 @@ The test could be rewritten using Boa Constrictor's Screenplay calls like this:
```csharp
IActor actor = new Actor(logger: new ConsoleLogger());
actor.Can(BrowseTheWeb.With(new ChromeDriver()));
actor.AttemptsTo(Navigate.ToUrl(SearchPage.Url));
actor.AttemptsTo(Navigate.ToUrl(MainPage.Url));
string title = actor.AsksFor(Title.OfPage());
actor.AttemptsTo(SearchDuckDuckGo.For("panda"));
actor.WaitsUntil(Appearance.Of(ResultPage.ResultLinks), IsEqualTo.True());
actor.AttemptsTo(SearchWikipedia.For("Giant panda"));
actor.WaitsUntil(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda"));
```

The page classes would provide locators:

```csharp
public static class SearchPage
public static class MainPage
{
public const string Url = "https://www.duckduckgo.com/";

public static IWebLocator SearchInput => L(
"DuckDuckGo Search Input",
By.Id("search_form_input_homepage"));
public const string Url = "https://en.wikipedia.org/wiki/Main_Page";

public static IWebLocator SearchButton => L(
"DuckDuckGo Search Button",
By.Id("search_button_homepage"));
"Wikipedia Search Button",
By.XPath("//button[text()='Search']"));

public static IWebLocator SearchInput => L(
"Wikipedia Search Input",
By.Name("search"));
}

public static class ResultPage
public static class ArticlePage
{
public static IWebLocator ResultLinks => L(
"DuckDuckGo Result Page Links",
By.ClassName("result__a"));
public static IWebLocator Title => L(
"Title Span",
By.CssSelector("[id='firstHeading'] span"));
}
```

Performing the DuckDuckGo search could use a custom interaction like this:
Performing the Wikipedia search could use a custom interaction like this:

```csharp
public class SearchDuckDuckGo : ITask
public class SearchWikipedia : ITask
{
public string Phrase { get; }

private SearchDuckDuckGo(string phrase) =>
private SearchWikipedia(string phrase) =>
Phrase = phrase;

public static SearchDuckDuckGo For(string phrase) =>
new SearchDuckDuckGo(phrase);
public static SearchWikipedia For(string phrase) =>
new SearchWikipedia(phrase);

public void PerformAs(IActor actor)
{
actor.AttemptsTo(SendKeys.To(SearchPage.SearchInput, Phrase));
actor.AttemptsTo(Click.On(SearchPage.SearchButton));
actor.AttemptsTo(SendKeys.To(MainPage.SearchInput, Phrase));
actor.AttemptsTo(Click.On(MainPage.SearchButton));
}
}
```
Expand Down
Loading

0 comments on commit a681ae1

Please sign in to comment.