Je suis le modèle d'objet de page suggéré par Selenium, mais comment pourrais-je créer un WebElement plus spécialisé pour une page. Plus précisément, nous avons des tableaux sur nos pages et j'ai écrit quelques fonctions d'assistance pour obtenir des lignes spécifiques d'un tableau, renvoyer le contenu d'un tableau, etc.
Actuellement, voici un extrait d'un objet de page créé avec une table:
public class PermissionsPage {
@FindBy(id = "studyPermissionsTable")
private WebElement permissionTable;
@FindBy(id = "studyPermissionAddPermission")
private WebElement addPermissionButton;
...
}
Donc, ce que je voudrais faire, c'est que permissionsTable soit un WebElement plus personnalisé qui utilise certaines des méthodes que j'ai mentionnées précédemment.
Par exemple:
public class TableWebElement extends WebElement {
WebElement table;
// a WebDriver needs to come into play here too I think
public List<Map<String, String>> getTableData() {
// code to do this
}
public int getTableSize() {
// code to do this
}
public WebElement getElementFromTable(String id) {
// code to do this
}
}
J'espère que cela a du sens, ce que j'essaie d'expliquer. Je suppose que ce que je recherche, c’est un moyen d’avoir ce WebElement personnalisé pour effectuer des tâches supplémentaires spécifiques à un tableau. Ajoutez cet élément personnalisé à une page et tirez parti de la manière dont Selenium relie les éléments Web à la page en fonction des annotations.
C'est possible? Et si oui, est-ce que quelqu'un sait comment cela peut être fait?
J'ai créé une interface qui combine toutes les interfaces WebDriver:
public interface Element extends WebElement, WrapsElement, Locatable {}
C'est juste là pour résumer tout ce que WebElements peut faire pour envelopper un élément.
Puis une implémentation:
public class ElementImpl implements Element {
private final WebElement element;
public ElementImpl(final WebElement element) {
this.element = element;
}
@Override
public void click() {
element.click();
}
@Override
public void sendKeys(CharSequence... keysToSend) {
element.sendKeys(keysToSend);
}
// And so on, delegates all the way down...
}
Ensuite, par exemple une case à cocher:
public class CheckBox extends ElementImpl {
public CheckBox(WebElement element) {
super(element);
}
public void toggle() {
getWrappedElement().click();
}
public void check() {
if (!isChecked()) {
toggle();
}
}
public void uncheck() {
if (isChecked()) {
toggle();
}
}
public boolean isChecked() {
return getWrappedElement().isSelected();
}
}
En l'utilisant dans mon script:
CheckBox cb = new CheckBox(element);
cb.uncheck();
J'ai également trouvé un moyen d'encapsuler les classes Element
. Vous devez créer quelques usines pour remplacer la PageFactory
intégrée, mais c'est faisable et cela donne beaucoup de flexibilité.
J'ai documenté ce processus sur mon site:
J'ai aussi un projet appelé selophane qui s'inspire de cette question et d'autres: selophane
Vous pouvez créer des WebElements personnalisés à l'aide de WebDriver Extensions framework qui fournit une classe WebComponent qui implémente l'interface WebElement.
Créez votre élément Web personnalisé
public class Table extends WebComponent {
@FindBy(tagName = "tr")
List<Row> rows;
public Row getRow(int row) {
return rows.get(row - 1);
}
public int getTableSize() {
return rows.size();
}
public static class Row extends WebComponent {
@FindBy(tagName = "td")
List<WebElement> columns;
public WebElement getCell(int column) {
return columns.get(column - 1);
}
}
}
... puis ajoutez-le à votre PageObject avec une annotation @FindBy et utilisez WebDriverExtensionFieldDecorator lors de l'appel de la méthode PageFactory.initElements
public class PermissionPage {
public PermissionPage(WebDriver driver) {
PageFactory.initElements(new WebDriverExtensionFieldDecorator(driver), this);
}
@FindBy(id = "studyPermissionsTable")
public Table permissionTable;
@FindBy(id = "studyPermissionAddPermission")
public WebElement addPermissionButton;
}
... puis utilisez-le dans votre test
public class PermissionPageTest {
@Test
public void exampleTest() {
WebDriver driver = new FirefoxDriver();
PermissionPage permissionPage = new PermissionPage(driver);
driver.get("http://www.url-to-permission-page.com");
assertEquals(25, permissionPage.permissionTable.getTableSize());
assertEquals("READ", permissionPage.permissionTable.getRow(2).getCell(1).getText());
assertEquals("WRITE", permissionPage.permissionTable.getRow(2).getCell(2).getText());
assertEquals("EXECUTE", permissionPage.permissionTable.getRow(2).getCell(3).getText());
}
}
Ou mieux, utilisez l’implémentation WebDriver Extensions PageObject
public class PermissionPage extends WebPage {
@FindBy(id = "studyPermissionsTable")
public Table permissionTable;
@FindBy(id = "studyPermissionAddPermission")
public WebElement addPermissionButton;
@Override
public void open(Object... arguments) {
open("http://www.url-to-permission-page.com");
assertIsOpen();
}
@Override
public void assertIsOpen(Object... arguments) throws AssertionError {
assertIsDisabled(permissionTable);
assertIsDisabled(addPermissionButton);
}
}
et JUnitRunner avec les méthodes d'assertion statique pour WebElements
import static com.github.webdriverextensions.Bot.*;
@RunWith(WebDriverRunner.class)
public class PermissionPageTest {
PermissionPage permissionPage;
@Test
@Firefox
public void exampleTest() {
open(permissionPage);
assertSizeEquals(25, permissionPage.permissionTable.rows);
assertTextEquals("READ", permissionPage.permissionTable.getRow(2).getCell(1));
assertTextEquals("WRITE", permissionPage.permissionTable.getRow(2).getCell(2));
assertTextEquals("EXECUTE", permissionPage.permissionTable.getRow(2).getCell(3));
}
}
J'ai également créé une implémentation WebElement personnalisée en étendant DefaultFieldDecorator et cela fonctionne très bien avec le modèle PageFactory. Cependant, l'utilisation de PageFactory elle-même me préoccupe. Cela semble très bien fonctionner uniquement avec des applications «statiques» (où l'interaction de l'utilisateur ne modifie pas la disposition des composants de l'application).
Mais chaque fois que vous devez travailler avec quelque chose de légèrement plus complexe, comme si vous avez une page avec un tableau, qui contient la liste des utilisateurs et le bouton de suppression à côté de chaque utilisateur, utiliser PageFactory devient problématique.
Voici un exemple pour illustrer ce dont je parle:
public class UserList
{
private UserListPage page;
UserList(WebDriver driver)
{
page = new UserListPage(driver);
}
public void deleteFirstTwoUsers()
{
if (page.userList.size() <2) throw new RuntimeException("Terrible bug!");
page.deleteUserButtons.get(0).click();
page.deleteUserButtons.get(0).click();
}
class UserListPage {
@FindBy(xpath = "//span[@class='All_Users']")
List<BaseElement> userList;
@FindBy(xpath = "//span[@class='All_Users_Delete_Buttons']")
List<BaseElement> deleteUserButtons;
UserListPage(WebDriver driver)
{
PageFactory.initElements(new ExtendedFieldDecorator(driver), this);
}
}
Ainsi, dans le scénario ci-dessus, l'appel de la méthode deleteFirstTwoUsers () échouera si vous tentez de supprimer le deuxième utilisateur avec "StaleElementReferenceException: référence de l'élément périmé: l'élément n'est pas attaché au document de la page". Cela est dû au fait que "page" n'a été instancié qu'une seule fois dans le constructeur et que PageFactory n'a "aucun moyen de le savoir" :), le nombre d'utilisateurs a diminué.
Les solutions de contournement que j'ai trouvées jusqu'à présent sont l'éther:
1) N'utilisez pas complètement Page Factory pour des méthodes comme celle-ci, et utilisez simplement WebDriver directement avec une sorte de boucle while: driver.findElementsBy()...
.
2) ou faire quelque chose comme ça:
public void deleteFirstTwoUsers()
{
if (page.userList.size() <2) throw new RuntimeException("Terrible bug!");
new UserListPage(driver).deleteUserButtons.get(0).click();
new UserListPage(driver).deleteUserButtons.get(1).click();
}
3) ou ceci:
public class UserList {
private WebDriver driver;
UserList(WebDriver driver)
{
this.driver = driver;
}
UserListPage getPage { return new UserListPage(driver);})
public void deleteFirstTwoUsers()
{
if (getPage.userList.size() <2) throw new RuntimeException("Terrible bug!");
getPage.deleteUserButtons.get(0).click();
getPage.deleteUserButtons.get(0).click();
}
Mais dans les premiers exemples, vous ne pouvez pas utiliser votre BaseElement encapsulé. Et en deuxième et troisième position - vous créez de nouvelles instances de Page Factory pour chaque action que vous effectuez sur la page. Je suppose donc que tout se résume à deux questions:
1) Pensez-vous qu’il vaut vraiment la peine d’utiliser PageFactory pour quoi que ce soit, vraiment? Il serait bien "moins cher" de créer chaque élément à la volée de la manière suivante:
class UserListPage
{
private WebDriver driver;
UserListPage(WebDriver driver)
{
this.driver = driver;
}
List<BaseElement> getUserList() {
return driver.findElements(By.xpath("//span[@class='All_Users']"));
}
2) Est-il possible de la méthode @Override driver.findElements pour renvoyer une instance de mon objet BaseElement encapsulé et non de WebElement? Si non, y a-t-il des alternatives?
En guise de suivi, voici ce que j’ai finalement fait (au cas où quelqu'un aurait le même problème). Ci-dessous, un extrait de la classe que j'ai créée en tant qu'emballage pour un WebElement:
public class CustomTable {
private WebDriver driver;
private WebElement tableWebElement;
public CustomTable(WebElement table, WebDriver driver) {
this.driver = driver;
tableWebElement = table;
}
public WebElement getTableWebElement() {
return tableWebElement;
}
public List<WebElement> getTableRows() {
String id = tableWebElement.getAttribute("id");
return driver.findElements(By.xpath("//*[@id='" + id + "']/tbody/tr"));
}
public List<WebElement> getTableHeader() {
String id = tableWebElement.getAttribute("id");
return tableWebElement.findElements(By.xpath("//*[@id='" + id + "']/thead/tr/th"));
}
.... more utility functions here
}
Et ensuite, je l'utilise dans toutes les pages qui créent en le référant comme suit:
public class TestPage {
@FindBy(id = "testTable")
private WebElement myTestTable;
/**
* @return the myTestTable
*/
public CustomTable getBrowserTable() {
return new CustomTable(myTestTable, getDriver());
}
La seule chose que je n'aime pas, c'est que lorsque la page veut obtenir le tableau, elle crée un "nouveau" tableau. J'ai fait cela pour éviter StaleReferenceExeptions, ce qui se produit lorsque les données de la table sont mises à jour (c'est-à-dire, cellule mise à jour, ligne ajoutée, ligne supprimée). Si quelqu'un a des suggestions sur la façon d'éviter de créer une nouvelle instance chaque fois que cela est demandé, renvoie plutôt le WebElement mis à jour, ce qui serait formidable!
Note latérale: WebElement
n'est pas une classe, son interface , ce qui signifie que votre classe ressemblerait davantage à ceci:
public class TableWebElement implements WebElement {
Mais dans ce cas vous devez implémentez all les méthodes qui sont dans WebDriver. Et c'est un peu exagéré.
Voici comment je fais cela - je me suis complètement débarrassé des PageObjects proposés par Selenium et j'ai "réinventé la roue" en ayant mes propres cours. J'ai une classe pour toute l'application:
public class WebUI{
private WebDriver driver;
private WebElement permissionTable;
public WebUI(){
driver = new firefoxDriver();
}
public WebDriver getDriver(){
return driver;
}
public WebElement getPermissionTable(){
return permissionTable;
}
public TableWebElement getTable(){
permissionTable = driver.findElement(By.id("studyPermissionsTable"));
return new TableWebElement(this);
}
}
Et puis j'ai mes cours d'assistance
public class TableWebElement{
private WebUI webUI;
public TableWebElement(WebUI wUI){
this.webUI = wUI;
}
public int getTableSize() {
// because I dont know exactly what are you trying to achieve just few hints
// this is how you get to the WebDriver:
WebElement element = webUI.getDriver().findElement(By.id("foo"));
//this is how you get to already found table:
WebElement myTable = webUI.getPermissionTable();
}
}
Échantillon de test:
@Test
public void testTableSize(){
WebUI web = new WebUI();
TableWebElement myTable = web.getTable();
Assert.assertEquals(myTable.getSize(), 25);
}
Jetez un coup d'oeil à htmlelements framework, cela ressemble exactement à ce dont vous avez besoin. Il a des éléments communs pré-implémentés, tels que case à cocher, radiobox, table, formulaire, etc., vous pouvez facilement créer vos propres éléments et en insérer un dans les autres en créant une arborescence claire.
Pourquoi ne pas créer un wrapper d'élément Web? comme ça?
class WEBElment
{
public IWebElement Element;
public WEBElement(/*send whatever you decide, a webelement, a by element, a locator whatever*/)
{
Element = /*make the element from what you sent your self*/
}
public bool IsDisplayed()
{
return Element.Displayed;
}
} // end of class here
mais vous pouvez rendre votre classe plus compliquée que cela. juste un concept
Je créerais ce que j'appelle un "SubPageObject" qui représente un PageObject sur un PageObject ...
L'avantage est que vous pouvez créer des méthodes personnalisées pour celui-ci et que vous ne pouvez initialiser que les WebElements dont vous avez réellement besoin dans ce SubPageObject.
Étape 1) Créez une classe de base appelée Element
qui étend l'interface IWrapsElement
public class Element : IWrapsElement
{
public IWebElement WrappedElement { get; set; }
public Element(IWebElement element)
{
this.WrappedElement = element;
}
}
Étape 2) Créez une classe pour chaque élément personnalisé et héritez de la classe Element
public class Checkbox : Element
{
public Checkbox(IWebElement element) : base(element) { }
public void Check(bool expected)
{
if (this.IsChecked()!= expected)
{
this.WrappedElement.Click();
}
}
public bool IsChecked()
{
return this.WrappedElement.GetAttribute("checked") == "true";
}
}
Usage:
une)
Checkbox x = new Checkbox(driver.FindElement(By.Id("xyz")));
x.Check(true)
b)
private IWebElement _chkMale{ get; set; }
[FindsBy(How = How.Name, Using = "chkMale")]
private Checkbox ChkMale
{
get { return new Checkbox (_chkMale); }
}
ChkMale.Check(true);
Pourquoi se préoccuper d'étendre WebElement? Essayez-vous de développer réellement le paquet Selenium? Dans ce cas, il ne serait pas judicieux d’étendre cette méthode à moins que vos ajouts ne soient applicables à tous les éléments Web que vous utilisez, et pas seulement à ceux qui sont spécifiques à votre table.
Pourquoi ne pas simplement faire quelque chose comme:
public class PermissionsPage extends TableWebElement{
...permissions stuff...
}
import WebElement
public class TableWebElement{
...custom web elements pertaining to my page...
}
Je ne sais pas si cela répond à votre question, mais il me semblait que c’était plus qu’une question d’architecture de classe.