An opinionated browser test framework for Selenium WebDriver. There are many like this one. The goal here was to build something that was easy to implement while still providing flexibility without over abstracting.
dotnet add package BasinFramework
Create a JSON file at the root of your project
{
"Environment": {
"Site": "integration",
"Browser": "Local Chrome",
"Login": "lebronjaymes"
},
"Sites": [
{
"Id": "staging",
"Url": "https://staging.coolapp.com"
},
{
"Id": "integration",
"Url": "https://integration.coolapp.com"
},
{
"Id": "preprod",
"Url": "http://preprod.coolapp.com"
}
],
"Logins": [
{
"Role": "Customer",
"Username": "JordanTheGOAT",
"Password": "givemetheball"
},
{
"Role": "Admin",
"Username": "lebronjaymes",
"Password": "foulsmakemecry"
}
],
"Browsers": [
{
"Id": "Local Chrome",
"Kind": "chrome",
"Timeout": 10
}
]
}
Initialize the config file:
BasinEnv.SetConfig("path/to/json/config/file.json");
Then you access config data with BasinEnv
BasinEnv.Site.Id; // returns "integration"
BasinEnv.Site.Url; // returns "https://integration.coolapp.com"
BasinEnv.Browser.Kind; // returns "chrome"
BasinEnv.Login.Username; // returns "lebronjaymes"
Or switch to an different configuration
BasinEnv.UseSite("Some Other Site Id");
BasinEnv.UseBrowser("Some Other Browser Id");
Or use a config object
BasinEnv.UseSite(new SiteConfig() {
Id = "My New Site",
Url = "http://dopesites.example.com"
});
Note: If you use these methods before calling BasinEnv.SetConfig()
, they will be overwritten with settings defined in the JSON config file.
By default, all the drivers are configured with a clean slate. That just means there are no browser flags or custom binary paths being set initially. Starting up a browser and going to a url is 2 lines of code.
// Uses the value defined in `Environment.Browser` by default to load a listed browser config by its `Id`
BrowserSession.Init();
// Navigates to a given url
BrowserSession.Goto("http://someurl");
For the most part, nothing else is needed. But if you need to access the IWebDriver
instance, just call BrowserSession.Current
.
Maybe I already have code written to manage driver instances. Just pass in an instance of IWebDriver.
BrowserSession.Init(new ChromeDriver());
BrowserSession.Goto("http://someurl");
Basin provides classes and interfaces to ease the pain of building page object frameworks. Below is an example login page.
using Basin.Selenium;
using Basin.Pages;
using OpenQA.Selenium;
namespace Example
{
public class LoginPage : Page
{
// Page elements
public Element UsernameField => TextInputTag.WithId("username");
public Element PasswordField => InputTag.WithAttr("type", "password").WithId("password");
public Element Submit => ButtonTag.WithAttr("name", "submitLogin");
// Page behavior
public void Login(string username, string password)
{
UsernameField.SendKeys(username);
PasswordField.SendKeys(password);
Submit.Click();
}
}
}
Page object classes can grow very large over time. I've found it incredibly difficult to retain readability and clarity with a single class. So instead of keeping everything in a single massive class, I break it down into to 2 classes. A page class for behaviors and a page map class for storing the element locators. Below its how its accomplished:
using Basin.Selenium;
using Basin.Pages;
using OpenQA.Selenium;
namespace Example
{
// This class no longer includes locator methods.
// The goal is to only put behavior methods in this class.
public class LoginPage : Page<LoginPageMap>
{
public void LoginWith(string username, string password)
{
Map.UsernameField.SendKeys(username);
Map.PasswordField.SendKeys(password);
Map.Submit.Click();
}
}
// PageMap provides the locator methods.
// The goal is to only put Element definitions in this class.
public class LoginPageMap : PageMap
{
public Element UsernameField => TextInputTag.WithId("username");
public Element PasswordField => InputTag.WithAttr("type", "password").WithId("password");
public Element Submit => ButtonTag.WithAttr("name", "submitLogin");
}
}
Now I have clean separate APIs for calling page elements and behaviors. Page map classes are portable and can be used in other classes that need the same element locators.
How page objects should be organized is quite subjective, but the goal of these interfaces and abstracts classes is to provide some basic structure without getting in the way.
- Fork the repo
- Create a branch
- Create a PR based on your fork branch
Thanks to ElSnoMan and Test Automation University from Applitools for this Course
Basin is based on and heavily inspired by the source code from this repo