Skip to main content
Version: 10.2.8

Implement RPA with PageObject

Sample PageObject implementation walkthrough

The PageObject design, borrowed from AQA large projects, allows structuring the RPA code to reflect the application page decomposition clearly and isolates persistence and Secrets Vault operations. All of this makes the RPA implementation codebase cleaner and easier to read by new project contributors.

The example project structure is as follows:

In the example, InvoicesRPATask is a Java-based RPA Bot Task that appears in Control Tower later and can be included in a Business Process:

InvoicesRPATask.java RPA Bot Task
package com.workfusion.odf2.example.task;

import com.google.common.collect.ImmutableMap;
import com.workfusion.odf2.compiler.BotTask;
import com.workfusion.odf2.core.cdi.Requires;
import com.workfusion.odf2.core.task.processing.BusinessEntityProcessorTask;
import com.workfusion.odf2.core.task.rpa.RpaDriver;
import com.workfusion.odf2.core.task.rpa.RpaFactory;
import com.workfusion.odf2.core.task.rpa.RpaRunner;
import com.workfusion.odf2.core.webharvest.service.vault.SecretsVaultService;
import com.workfusion.odf2.example.module.RepositoryModule;
import com.workfusion.odf2.example.model.Invoice;
import com.workfusion.odf2.example.repository.InvoiceRepository;
import com.workfusion.odf2.example.task.processing.InvoicePlaneRobot;
import org.openqa.selenium.Proxy;
import org.slf4j.Logger;
import javax.inject.Inject;
import java.util.Collection;
import java.util.UUID;

@BotTask(requireRpa = true)
@Requires({RepositoryModule.class})
public class InvoicesRpaTask implements BusinessEntityProcessorTask<Invoice> {

private final RpaRunner rpaRunner;
private final SecretsVaultService secretsVault;
private final InvoiceRepository invoiceRepository;
private final Logger logger;

@Inject
public InvoicesRpaTask(RpaFactory rpaFactory, SecretsVaultService secretsVault, InvoiceRepository invoiceRepository, Logger logger) {
rpaRunner = rpaFactory.builder(RpaDriver.CHROME)
.closeOnCompletion(true)
.maximizeOnStartup(true)
.startInPrivate(true)
.build();
this.secretsVault = secretsVault;
this.invoiceRepository = invoiceRepository;
this.logger = logger;
}

@Override
public Collection<Invoice> findBusinessEntities(UUID transactionId) {
return invoiceRepository.findAll(transactionId);
}

@Override
public void processBusinessEntities(Collection<Invoice> invoices) {
rpaRunner.execute(driver -> {
InvoicePlaneRobot robot = new InvoicePlaneRobot(driver, secretsVault, logger);
for (Invoice invoice : invoices) {
robot.processInvoice(invoice);
}
});
}

@Override
public void saveBusinessEntities(Collection<Invoice> invoices) {
invoices.forEach(invoiceRepository::update);
}

}
  • SecretsVaultService is provided by ODF 2 and isolates operations with Secrets Vault.

  • RpaRunner is a ODF 2 factory responsible for configuring and executing RPA commands from Java.

  • logger is used for logging important messages to the Control Tower log; see the issues logged with the ERROR level in the Control Tower error popup.

  • InvoiceRepository is the project-level implementation of custom entity persistence into the AI Agent Data Stores:

ORMLite-based InvoiceRepository.java RPA Bot Task
package com.workfusion.odf2.example.repository;

import com.workfusion.odf2.example.model.Invoice;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import com.j256.ormlite.support.ConnectionSource;
import com.workfusion.odf2.core.OdfException;
import com.workfusion.odf2.core.orm.model.OdfTransactionalEntity;
import com.workfusion.odf2.core.orm.repository.TransactionalEntityRepository;
import javax.inject.Inject;

public class InvoiceRepository extends TransactionalEntityRepository<Invoice> {

private final ProductRepository productRepository;

@Inject
public InvoiceRepository(ConnectionSource connectionSource, ProductRepository productRepository) throws SQLException {
super(connectionSource, Invoice.class);
this.productRepository = productRepository;
}

public List<Invoice> findAll(UUID transactionId) {
try {
return dao.queryForEq(OdfTransactionalEntity.TRANSACTION_UUID_COLUMN, transactionId);
} catch (SQLException e) {
throw new OdfException(e);
}
}

@Override
public Invoice create(Invoice entity) {
Invoice invoice = super.create(entity);
invoice.getProducts().forEach(productRepository::createOrUpdate);
return invoice;
}
}
Invoice.java entity
package com.workfusion.odf2.example.model;

import java.util.Collection;
import java.util.Date;
import com.j256.ormlite.field.DataType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.ForeignCollectionField;
import com.j256.ormlite.table.DatabaseTable;
import com.workfusion.odf2.core.orm.model.BusinessEntity;

@DatabaseTable(tableName = "invoice")
public class Invoice extends BusinessEntity {

public static final String DATE_COLUMN = "date";
public static final String DUE_DATE_COLUMN = "due_date";
public static final String INVOICE_NUMBER_COLUMN = "invoice_number";
public static final String INVOICE_STATUS_COLUMN = "invoice_status";
public static final String PAYMENT_METHOD_COLUMN = "payment";
public static final String INVOICE_DOWNLOAD_URL = "download_url";

@DatabaseField(columnName = DATE_COLUMN, dataType = DataType.DATE)
private Date date;

@DatabaseField(columnName = DUE_DATE_COLUMN, dataType = DataType.DATE)
private Date dueDate;

@DatabaseField(columnName = INVOICE_NUMBER_COLUMN, dataType = DataType.STRING)
private String invoiceNumber;

@DatabaseField(columnName = INVOICE_STATUS_COLUMN, dataType = DataType.STRING)
private String invoiceStatus;

@DatabaseField(columnName = PAYMENT_METHOD_COLUMN, dataType = DataType.STRING)
private String paymentMethod;

@DatabaseField(columnName = INVOICE_DOWNLOAD_URL, dataType = DataType.STRING)
private String downloadUrl;

@ForeignCollectionField
private Collection<Product> products;

public Date getDate() {
return date;
}

public void setDate(Date date) {
this.date = date;
}

public Date getDueDate() {
return dueDate;
}

public void setDueDate(Date dueDate) {
this.dueDate = dueDate;
}

public String getInvoiceNumber() {
return invoiceNumber;
}

public void setInvoiceNumber(String invoiceNumber) {
this.invoiceNumber = invoiceNumber;
}

public String getInvoiceStatus() {
return invoiceStatus;
}

public void setInvoiceStatus(String invoiceStatus) {
this.invoiceStatus = invoiceStatus;
}

public String getPaymentMethod() {
return paymentMethod;
}

public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}

public String getDownloadUrl() {
return downloadUrl;
}

public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}

public Collection<Product> getProducts() {
return products;
}

public void setProducts(Collection<Product> products) {
this.products = products;
}
}

Now, the RPA orchestration is performed in InvoicePlaneRobot.

note

Mind that credentials are taken from the hardcoded Secrets Vault robotCredentials alias and the web application address hardcoded as https://train-invoiceplane.workfusion.com. In your implementation, move this into the configuration Data Store.

InvoicePlaneRobot.java RPA orchestrator
package com.workfusion.odf2.example.task.processing;

import com.workfusion.bot.service.SecureEntryDTO;
import com.workfusion.odf2.core.webharvest.service.vault.SecretsVaultService;
import com.workfusion.odf2.example.model.Invoice;
import com.workfusion.rpa.driver.Driver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
import org.slf4j.Logger;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public class InvoicePlaneRobot implements Consumer<Driver> {

public static final String INVOICEPLANE_ADDRESS = "https://train-invoiceplane.workfusion.com";
public static final String INVOICEPLANE_CREDENTIALS_ALIAS = "robotCredentials";
private final Driver driver;
private final SecretsVaultService secretsVault;
private final NavigationBar navigationBar;
private final LoginPage loginPage;
private final CreateInvoicePage createInvoicePage;
private final EditInvoicePage editInvoicePage;
private final Logger logger;

public InvoicePlaneRobot(Driver driver, SecretsVaultService secretsVault, Logger logger) {
this.driver = Objects.requireNonNull(driver);
this.secretsVault = Objects.requireNonNull(secretsVault);
this.logger = logger;
setOptions();

navigationBar = PageFactory.initElements(driver, NavigationBar.class);
loginPage = PageFactory.initElements(driver, LoginPage.class);
createInvoicePage = PageFactory.initElements(driver, CreateInvoicePage.class);
editInvoicePage = PageFactory.initElements(driver, EditInvoicePage.class);
}

public void processInvoice(Invoice invoice) {
driver.navigate().to(INVOICEPLANE_ADDRESS);
SecureEntryDTO secureEntry = secretsVault.getEntry(INVOICEPLANE_CREDENTIALS_ALIAS);
loginPage.login(secureEntry.getKey(), secureEntry.getValue());
navigationBar.toCreateInvoicePage();
createInvoicePage.createDraftInvoice();
editInvoicePage.fillAndSaveDraftInvoice(invoice);

try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("Process invoice interrupted");
}
navigationBar.logout();
}

@Override
public void accept(Driver driver) {

}

private void setOptions() {
driver.switchDriver("chrome");
WebDriver.Options options = driver.manage();
options.timeouts()
.implicitlyWait(10, TimeUnit.SECONDS)
.pageLoadTimeout(90, TimeUnit.SECONDS);
options.deleteAllCookies();
}

}

Finally, PageObjects include isolated RPA actions per application page:

PageObject for LoginPage.java login screen
package com.workfusion.odf2.example.task.processing;

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class LoginPage {

@FindBy(id = "email")
private WebElement emailField;

@FindBy(id = "password")
private WebElement passwordField;

@FindBy(xpath = "//input[@type='submit']")
private WebElement submit;

@FindBy(xpath = "//*[self::div[@id='main-area'] or self::div[@id='login']/div[contains(@class,'alert')]]")
private WebElement loginFailed;

@FindBy(xpath = "//div[@id='login']/div[contains(@class,'alert')]")
private WebElement loginFailedMessage;

public void login(String email, String password) {
sendKeys(emailField, email);
sendKeys(passwordField, password);
submit.click();
String actualId = loginFailed.getAttribute("id");
if (!"main-area".equalsIgnoreCase(actualId)) {
throw new IllegalStateException(String.format("%s%nUser id: %s", loginFailedMessage.getText(), email));
}
}

private static void sendKeys(WebElement element, String value) {
element.clear();
element.sendKeys(value);
}

}
PageObject for EditInvoicePage.java
package com.workfusion.odf2.example.task.processing;

import com.workfusion.odf2.example.model.Invoice;
import com.workfusion.odf2.example.model.Product;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.Select;

public class EditInvoicePage {

public static final String INVOICE_PLANE_DATE_PATTERN = "MM/dd/yyyy";

@FindBy(id = "invoice_number")
private WebElement invoiceNumber;

@FindBy(id = "invoice_date_created")
private WebElement invoiceDate;

@FindBy(id = "invoice_date_due")
private WebElement invoiceDueDate;

@FindBy(id = "payment_method")
private WebElement invoicePayment;

@FindBy(id = "invoice_status_id")
private WebElement invoiceStatus;

@FindBy(id = "btn_save_invoice")
private WebElement saveButton;

@FindBy(xpath = "//a[contains(@class, 'btn_add_row')]")
private WebElement addRowButton;

@FindBy(id = "item_table")
private WebElement itemTable;

public void fillAndSaveDraftInvoice(Invoice invoice) {
int size = itemTable.findElements(By.xpath("//table/tbody")).size();
int rowNo = 0;
for (Product product : invoice.getProducts()) {
if (rowNo > size - 3) {
addRowButton.click();
size++;
}
WebElement row = itemTable.findElement(By.xpath("//table/tbody[" + (rowNo + 2) + "]"));
fillProductRow(row, product);
rowNo++;
}
invoice.setInvoiceNumber(invoiceNumber.getAttribute("value"));
invoice.setDate(DateTime.parse(invoiceDate.getAttribute("value"), DateTimeFormat.forPattern(INVOICE_PLANE_DATE_PATTERN)).toDate());
invoice.setDueDate(DateTime.parse(invoiceDueDate.getAttribute("value"), DateTimeFormat.forPattern(INVOICE_PLANE_DATE_PATTERN)).toDate());
Select selectPayment = new Select(invoicePayment);
selectPayment.selectByIndex(1);
String payment = selectPayment.getFirstSelectedOption().getText();
invoice.setPaymentMethod(payment);
Select selectStatus = new Select(invoiceStatus);
String status = selectStatus.getFirstSelectedOption().getText();
invoice.setInvoiceStatus(status);
String invoiceId = itemTable.findElement(By.name("invoice_id")).getAttribute("value");
invoice.setDownloadUrl(InvoicePlaneRobot.INVOICEPLANE_ADDRESS + "/index.php/invoices/generate_pdf/" + invoiceId);
saveButton.click();
}

private void fillProductRow(WebElement row, Product product) {
WebElement name = row.findElement(By.name("item_name"));
name.sendKeys(product.getName());
WebElement quantity = row.findElement(By.name("item_quantity"));
quantity.sendKeys("1");
WebElement price = row.findElement(By.name("item_price"));
price.sendKeys(product.getPrice().toString());
WebElement description = row.findElement(By.name("item_description"));
description.sendKeys(product.getDescription());
}

}