In English you would read this as Simple Se-lenium Page Objects.
We decided to sound a bit desi as well. So in Hindi you would just read this name as Simple sey page objects.
Obviously we wanted to associated with Selenium which explains the suffix Se.
This library is called as SimpleSe because it attempts at providing a simple way to work with page objects.
Page objects as a concept has been built by different people in different ways. This library attempts at building page objects keeping in mind the following things:
-
Simple to use (try staying away from complex approaches)
-
De-couple the java code from the locators so that a locator change would merely warrant a change in the externalised data source (JSON file in our case)
-
Support localization i.e., being able to define locators for different countries and still have a unified way of working with the html elements.
-
Support a simple way of expressing explicit waits in Selenium. E.g., would include
- Wait for
30 seconds
for a particular element to show up before interacting with it. - Wait for an additional
x seconds
for a particular element to become clickable.
- Wait for
Simple Se Page Objects requires that you use :
- JDK 8.
- Selenium 4.4.0 or higher.
Simple Se Page Objects is a Maven artifact. In order to consume it, you merely need to add the following as a dependency in your pom file.
<dependency>
<groupId>com.rationaleemotions</groupId>
<artifactId>simple-se-pageobjects</artifactId>
<version>1.0.8</version>
</dependency>
At the heart of this project is the JSON file that contains all your locators information.
The intent is to define one JSON
file per page. So if your UI automation flow has 10 pages, then effectively you
would define 10 pages respectively.
Here's how a typical JSON file that represents the locators could look like :
{
"name": "FooPage",
"defaultLocale": "en_US",
"elements": [
{
"name": "bar",
"type": "button",
"list": true,
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
],
"wait": {
"until": "visible",
"for": 45
}
}
]
}
name
- (Mandatory) This is a user friendly name that you would be giving to your page. Names have to be unique so that there's no clash.defaultLocale
- (Mandatory) You would provide a fall back locale here. The expectation is that there would be atleast one locale definition withinelements
that matches the value ofdefaultLocale
. Typical values could been_US
(which suggests that for a particular element if there are no country specific locators defined, thenen_US
is to be used as the fall back locale anden_US
specific locators could be used.)elements
- (Mandatory) All your individual html elements that make up your page would go here. Every element would have the following attributes.name
- (Mandatory) This is a user friendly name that you would be giving to your html element. Here again names have to be unique so that there's no clash. Typical e.g., could includeloginBtn
(or)userNameTxtField
.locale
- (Mandatory) Represents the list of countries and their corresponding locators. Its attributes are described as below.name
- (Mandatory) The name/key of the country for which the locator is being defined.type
- (Optional). Defaults togeneric
Valid values are(button|checkbox|form|generic|image|label|link|radio|select|text)
. Helps in identifying the type of an element. Mostly useful for support tools such as code generators.list
- (Optional). Defaults tofalse
. When specified and set totrue
indicates that a particular entry represents a list of elements. Mostly useful for support tools such as code generators.locator
- (Mandatory) The actual locator which can be an xPath (or) css locator (or) id/name. Here's some conventions associated with this :- xpath format : Can be of the form
xpath=./input[@name='first_name']
(or)xpath=//input[@name='first_name']
(or)xpath=/input[@name='first_name']
(or)xpath=(//input[@class='btn'])[1]
(or)./input[@name='first_name']
(or)//input[@name='first_name']
(or)/input[@name='first_name']
- css format : Can be of the form
css=input[name='first_name']
- id (or) name format : Can be of the form
first_name
{ herefirst_name
can either be theid
of an element (or) thename
of an element. } - class format : Can be of the form
class=btn
- linkText format : Can be of the form
linkText=Checkboxes
- partialLinkText format : Can be of the form
parialLinkText=Check
- tagName format : Can be of the form
tagName=input
- xpath format : Can be of the form
wait
- (Optional) If you feel that a particular element is either a slow loading element (or) you would need to have some extra waits defined, then those go here. It can have the following attributes.until
- We currently support only one of the following values:Available
- (case in-sensitive) Waits for the element to be available in the Dom. Its functionally equivalent toExpectedConditions#presenceOfElementLocated
(or)ExpectedConditions#presenceOfAllElementsLocatedBy
.Visible
- (case in-sensitive) Waits for the element to be available in the Dom and also to be visible. Its functionally equivalent toExpectedConditions#visibilityOfElementLocated
(or)ExpectedConditions#visibilityOfAllElementsLocatedBy
.Clickable
- (case in-sensitive) Waits for the element to be available in the Dom and also to be clickable. Its functionally equivalent toExpectedConditions#elementToBeClickable
.
for
- An integer that represents the wait time in seconds. If no value is specified (or) if this attribute is omitted butuntil
is included, the system falls back to45 seconds.
- This value can be altered
globally via the JVM argument
simplese.default.waittime
(for e.g.,-Dsimplese.default.waittime=30
). - If for any reason wait is to be completely ignored, then the wait node can be excluded from the json file and instruct SimpleSe to ignore using a default wait mechanism by setting the JVM argument
-Dsimplese.default.waitstrategy=false
- This value can be altered
globally via the JVM argument
There are basically 3 types of scenarios with waits.
- Scenario #1 (You want a global implicit wait to be applied across all page objects without you explicitly mentioning it in your json file)
The json could look like below:
{
"name": "FooPage",
"defaultLocale": "en_US",
"elements": [
{
"name": "bar",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
]
}
]
}
In this case, SimpleSe
will default to a wait fo 45 seconds for the element to be present in the DOM before timing out. This default value can be changed using the JVM argument -Dsimplese.default.waittime=30
(we changed the default wait time from 45 seconds to 30 seconds)
- Scenario #2 (You want to globally disable implicit wait by not having it in your json files)
In this case, for all elements wherein the wait
attribute is missing in your json file, then wait will be skipped from being applied only for that element. Make sure you turn on this behavior via the JVM argument -Dsimplese.default.waitstrategy=false
.
The json could look like below:
{
"name": "FooPage",
"defaultLocale": "en_US",
"elements": [
{
"name": "bar",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
]
}
]
}
- Scenario #3 (You want to turn off implicit waits only for a few elements
In this case, for some elements, you want to use the implicit wait strategy, for some elements, you want to use an explicit wait strategy and for some elements you want to disable the wait strategy completely.
Here's how the json would look like:
{
"name": "FooPage",
"defaultLocale": "en_US",
"elements": [
{
"name": "bar",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
]
},
{
"name": "foo",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
],
"wait": {
"until": "clickable",
"for": 20
}
},
{
"name": "foobar",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1"
},
{
"name": "en_FR",
"locator": "xpath=//h1"
}
],
"wait": {
"until": "clickable",
"for": -1
}
}
]
}
Here:
- The
bar
element uses the implicit wait strategy (wait for 45 seconds for the element to be available in DOM) - The
foo
element uses the explicit wait strategy (wait for 20 seconds for the element to be clickable) - The
foobar
element disables the wait strategy by mentioning the wait time as-1
Lets say we have the below json sample which is located in src/test/resources/HomePage.json
.
{
"name": "HomePage",
"defaultLocale": "en_US",
"elements": [
{
"name": "heading",
"locale": [
{
"name": "en_US",
"locator": "xpath=//h1[@class='heading']"
}
],
"wait": {
"until": "visible",
"for": 45
}
},
{
"name": "heading1",
"locale": [
{
"name": "en_US",
"locator": "//h1[@class='heading1']"
}
],
"wait": {
"until": "visible",
"for": 45
}
},
{
"name": "checkboxesLink",
"locale": [
{
"name": "en_US",
"locator": "xpath=//a[text()='Checkboxes']"
}
],
"wait": {
"until": "clickable",
"for": 45
}
}
]
}
A sample code that would work with the above mentioned would look like below :
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
public class HerokkuAppTest {
private RemoteWebDriver driver;
@BeforeClass
public void setup() {
driver = new ChromeDriver();
}
@Test
public void testMethod() {
driver.get("http://the-internet.herokuapp.com/");
PageObject homePage = new PageObject(driver, "src/test/resources/HomePage.json");
Label heading = homePage.getLabel("heading");
Assert.assertEquals(heading.getText(), "Welcome to the Internet");
Link checkbox = homePage.getLink("checkboxesLink");
Assert.assertEquals(checkbox.isDisplayed(), true);
}
@AfterClass
public void cleanup() {
if (driver != null) {
driver.quit();
}
}
}
Lets say we have a json that looks like below located at src/test/resources/CheckboxPage.json
{
"name": "CheckboxPage",
"defaultLocale": "en_US",
"elements": [
{
"name": "checkbox",
"locale": [
{
"name": "en_US",
"locator": "xpath=//input[@type='checkbox']"
}
],
"wait": {
"until": "visible",
"for": 45
}
}
]
}
A sample code that would work with the above mentioned would look like below (working with list of elements) :
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
public class HerokkuAppTest {
private RemoteWebDriver driver;
@BeforeClass
public void setup() {
driver = new ChromeDriver();
}
@Test
public void testMethod() {
driver.get("http://the-internet.herokuapp.com/checkboxes");
PageObject checkboxPage = new PageObject(driver, "src/test/resources/CheckboxPage.json");
List<Checkbox> checkboxList = checkboxPage.getCheckboxes("checkbox");
Assert.assertEquals(checkboxList.size(), 2);
int uncheckedCount = 0;
int checkedCount = 0;
for (Checkbox checkbox : checkboxList) {
if (checkbox.isChecked()) {
checkedCount += 1;
} else {
uncheckedCount += 1;
}
}
Assert.assertEquals(checkedCount, 1);
Assert.assertEquals(uncheckedCount, 1);
}
@AfterClass
public void cleanup() {
if (driver != null) {
driver.quit();
}
}
}
Here's a sample that shows how to create a PageObject
for the locale en_FR
.
public class HerokkuAppTest {
@Test
public void testMethod() {
//Here we are creating a page object instance for the locale "en_FR"
PageObject homePage = new PageObject(driver, "src/test/resources/HomePage.json").forLocale("en_FR");
//More source code goes here.
// ..
}
}
Many times we may have to work with generic elements (such as date picker or a scroll bar etc) for which Simple Se
may not have anything to offer as a ready made object. But you can still regard them as GenericElement
and work
with them. Here's a sample:
public class HerokkuAppTest {
@Test
public void testMethod() {
PageObject homePage = new PageObject(driver, "src/test/resources/HomePage.json").forLocale("en_FR");
GenericElement datePicker = homePage.getGenericElement("datePickerControl");
}
}
In order to support mobile app automation with accessibility id locator strategy using appium, please refer to the below JSON file that have a button with name as loginButton
with accessibility id as loginButton
and the corresponding Page Object JSON mapping at, src/test/resources/login-page.json
looks like as below,
{
"name": "Home",
"defaultLocale": "en_US",
"elements": [
{
"name": "loginButton",
"locale": [
{
"name": "en_US",
"locator": "#loginButton"
}
]
}
]
}
With the above page object locators using accessibility id appended with #
at elements[0].locale[0].locator
, the same can be used as a reference with Java class to perform different user behaviour operations like click, input text, clear text, etc,
The below java class depicts the click operation on login button as,
import com.rationaleemotions.page.Button;
import com.rationaleemotions.page.PageObject;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import org.openqa.selenium.Platform;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class AppLauncherTest {
private AndroidDriver driver;
private static final String APP_PACKAGE_NAME = "";
private static final String APP_ACTIVITY_NAME = "";
@BeforeMethod
public void init() throws MalformedURLException {
UiAutomator2Options options = new UiAutomator2Options()
.setPlatformName(Platform.ANDROID.name())
.setAppPackage(APP_PACKAGE_NAME)
.setAppActivity(APP_ACTIVITY_NAME)
.eventTimings();
driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), options);
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
}
/**
* The same operation as below is the step definition for click operation on login button as,
* driver.findElement(AppiumBy.accessibilityId("loginButton")).click();
* */
@Test
public void launchApp() {
PageObject loginPage = new PageObject(driver, "src/test/resources/login-page.json");
Button loginButton = loginPage.getButton("loginButton");
loginButton.click();
}
@AfterMethod
public void quit() {
driver.quit();
}
}