Overview:
We had already covered 2 articles on the script-less page object design. If you have not read them already, I would suggest you to read them first in the below order.
- Part 1 – Creating JSON based page objects which contains elements name – value pairs
- Part 2 – An Engine which parses the json file and enters the accordignly
In the Part 1, when we created the JSON file, we had hard-coded the test data. Now in this part, we are going to make this data-driven by referring to a CSV sheet for test data.
Sample CSV Data:
Lets assume I have test data as shown here. I would like to access the page and enter the data on the page for each row of the CSV file to make this data-driven.
Lets also assume that first row in the CSV is the column header.
Page Object JSON:
In order to do that, We should NOT want our JSON Page Object to be created in the below format as it hard codes the data.
{
"q71_patientGender":"Male",
"q45_patientName[first]":"test",
"q45_patientName[last]":"automation",
"q46_patientBirth[month]":"January",
"q46_patientBirth[day]":"6",
"q46_patientBirth[year]":"1960"
...
}
Instead, we need to refer to the column name of the CSV as shown here.
{
"q71_patientGender":"${gender}",
"q45_patientName[first]":"${firstname}",
"q45_patientName[last]":"${lastname}",
"q46_patientBirth[month]":"${dob-month}",
"q46_patientBirth[day]":"${dob-day}",
"q46_patientBirth[year]":"${dob-year}"
...
}
You could directly update the JSON yourself with the appropriate column name or you could inject below script in the Chrome console.
//inject JQuery
var jq = document.createElement('script');
jq.src = "//code.jquery.com/jquery-3.2.1.min.js";
document.getElementsByTagName('head')[0].appendChild(jq);
//create alias
var $j = jQuery.noConflict();
//inject function for page object model
var eles = {};
var pageObjectModel = function(root){
$j(root).find('input, select, textarea').filter(':enabled:visible:not([readonly])').each(function(){
let eleName = this.name;
eles[eleName] = "${" + eleName + '}';
});
console.log(JSON.stringify(eles, null, 4));
}
//invoke the function
pageObjectModel(document)
It would create the Page Object JSON in the below format assuming your CSV file has those columns.
Note: If you do not want to refer to CSV for all the fields, you can use the ${…} expression only for those elements which need to be retrieved from the CSV file
For example, below JSON is perfectly valid. In this case, we want only the Gender to be retrieved from the CSV. All other fields would use the hard coded data.
{
"q71_patientGender":"${gender}",
"q45_patientName[first]":"test",
"q45_patientName[last]":"automation",
"q46_patientBirth[month]":"January",
"q46_patientBirth[day]":"6",
"q46_patientBirth[year]":"1960"
...
}
Page Object Parser:
We already have seen this in Part 2 of this article. We have Page Object Parser as shown here which reads the Page Object json file and gives the map of the element names and value to be entered.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class PageObjectParser {
public static Map<String, String> parse(String filename) {
Map<String, String> result = null;
try {
result = new ObjectMapper().readValue(new File(filename), LinkedHashMap.class);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
CSV Reader:
We need to read the given CSV file and get all the records. So I create below script using apache-commons csv lib.
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import java.io.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class CSVReader {
public static Stream<CSVRecord> getRecords(final String filename) throws IOException {
Reader in = new InputStreamReader(new FileInputStream(new File(filename)));
Iterable<CSVRecord> records = CSVFormat.RFC4180.withFirstRecordAsHeader().parse(in);
Stream<CSVRecord> stream = StreamSupport.stream(records.spliterator(), false);
return stream;
}
}
Data Map:
Our Page Object parse gives a map as shown here.
[
"q71_patientGender"="${gender}",
"q45_patientName[first]"="${firstname}",
"q45_patientName[last]"="${lastname}",
]
Our CSV Reader can give us a map for each row as shown here.
[
"gender"="Male",
"firstname"="Michael",
"lastname"="jackson"
]
But what we really need is a map as shown here
[
"q71_patientGender"="Male",
"q45_patientName[first]"="Michael",
"q45_patientName[last]"="Jackson",
]
So, I am creating below class which is responsible for replacing the ${csvcolmn} in the JSON Page Object with the actual data.
public class DataMap {
private static final String TEMPLATE_EXPRESSION = "\\$\\{([^}]+)\\}";
private static final Pattern TEMPLATE_EXPRESSION_PATTERN = Pattern.compile(TEMPLATE_EXPRESSION);
//json element names and values with ${..}
final Map<String, String> elementsNameValueMap;
//csv test data for a row
final Map<String, String> csvRecord;
public DataMap(Map<String, String> elementsNameValueMap, Map<String, String> csvRecord){
this.elementsNameValueMap = elementsNameValueMap;
this.csvRecord = csvRecord;
//this replaces ${..} with corresponding value from the CSV
this.updateMapWithValues(elementsNameValueMap);
}
//this map contains elements names and actual values to be entered
public Map<String, String> getElementsNameValueMap(){
return this.elementsNameValueMap;
}
private void updateMapWithValues(final Map<String, String> variablesMapping){
variablesMapping
.entrySet()
.stream()
.forEach(e -> e.setValue(updateTemplateWithValues(e.getValue())));
}
private String updateTemplateWithValues(String templateString){
Matcher matcher = TEMPLATE_EXPRESSION_PATTERN.matcher(templateString);
while(matcher.find()){
templateString = this.csvRecord.get(matcher.group(1));
}
return templateString;
}
}
Now you could call CSVreader to get the list of csv records
//get csv data
Stream<CSVRecord> records = CSVReader.getRecords(TEST_DATA_CSV_PATH);
//csv records
records
.map(CSVRecord::toMap) //convert each row to a map of key value pair - contains column name and row value
.map(csvMap -> new DataMap(PageObjectParser.parse(PAGE_OBJECT_JSON_PATH), csvMap)) //feed page object map and csv row map to get the page object element name and actual value
.map(DataMap::getElementsNameValueMap) // get the map which contains elements name and actual test data from csv
.forEach(ScriptlessFramework::accessPageEnterData); //this method is responsible for accessing the page and calling elements handler
Below is the method which is responsible for accessing the page and entering the data for each Map it receives.
private static void accessPageEnterData(Map<String, String> map){
//start webdriver
WebDriver driver = DriverFactory.getDriver(CHROME);
driver.get("https://form.jotform.com/81665408084158");
//enter data
map.entrySet()
.stream()
.forEach(entry -> {
List<WebElement> elements = driver.findElements(By.name(entry.getKey()));
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", elements.get(0));
ElementsHandler.handle(elements, entry.getValue());
});
//quit
driver.quit();
}
I tried to run with 2 records in a CSV file which works just as we expected.
GitHub:
I have uploaded the project here for your reference.
Summary:
By injecting JQuery on the chrome console, We are able to quickly create page objects within few seconds. We modified only those field names in the JSON for which we would be referring the data from the CSV file. Now the ScriptlessFramework engine parses the given CSV file and Page object json ans enters the data for each row of the CSV file. You could further enhance this approach to create a workflow / include page validation etc.
Happy Testing & Subscribe 🙂
good information, Better way to implement it for scriptless automation
Great new way of automating forms!
What about not saving the JSON and parsing the page object model runtime with a driver.executeScript(… return pageObject) part?
It depends on the requirement. This article is just an idea. Saving the json file gives the ability to map the input to a corresponding element on the page. It will also help if the some of expected elements are missing.
Another great article series!
Did you put this to use in one of your company frameworks? If so, did you run it via TestNG and how does it show the failure points in test reports generated?