How to inherit from WebElement in WebdriverIO
Asked Answered
C

2

11

I looking for a way to inherit from WebElement object that webdriverio returns, without monkey-patching and with TS types support (autocompletion is a must). Is there a way to do something like this?

class Checkbox extends WebdriverIOWebElement {
    constructor() {
       super($('div'))
    }
    // overriding base method
    isDisplayed(): boolean {
        // blabla some new logic here
    }

    check() {
        if(!this.isChecked()) {
            this.click()
        }
    }

    uncheck() {
        if(this.isChecked()) {
            this.click()
        }
    }
}
Cyclometer answered 12/1, 2019 at 17:10 Comment(8)
I think this is a case were composition would be better than inheritance. Your class can contain a reference to the WebElement instead of inheriting from it, and you can provide a function that exposes the WebElement when needed.Galvin
I recommend to create a Page Object. There is no way to create a class inheritance as you suggested.Hoofed
@Hoofed I do use PageObject. I wanted to make typified WebElements like Checkbox, Select, Input or my own blocks like LoginForm, or even typified collections of elements like - Messages<Message> . I did something like this for ProtractorJS - github.com/Xotabu4/protractor-element-extend/blob/master/…Cyclometer
@Cyclometer just create a PO that provides all necessary functions to interact with it. No need to inherit the full element prototype.Hoofed
Where is located WebdriverIOWebElement class ? can't find it in webdriverio/webdriverio on GitHubPlast
Can you provide an example of a possible usage of Checkbox class, to demonstrate up to which level you need types support?Soll
@MosèRaguzzini thats just pseudocodeCyclometer
@MarinosAn const checkbox = new Checkbox($('input[type="checkbox"]')) checkbox.check()Cyclometer
V
3

Lets take an example , When we have a New Tag (my-app) in HTML and we have to build a case to Login using webdriverIO ,

Assume this is the HTML :

What we would do is using the component object pattern , component object pattern attempts to reduce that repetition and move the component's api into an object of its own. We know that in order to interact with an element's shadow DOM, we first need the host element. Using a base class for your component objects makes this pretty straightforward.

Here's a bare-bones component base class that takes the host element in its constructor and unrolls that element's queries up to the browser object, so it can be reused in many page objects (or other component objects), without having to know anything about the page itself.

    class Component {

      constructor(host) {
        const selectors = [];
        // Crawl back to the browser object, and cache all selectors
        while (host.elementId && host.parent) {
          selectors.push(host.selector);
          host = host.parent;
        }
        selectors.reverse();
        this.selectors_ = selectors;
      }

      get host() {
        // Beginning with the browser object, reselect each element
        return this.selectors_.reduce((element, selector) => element.$(selector), browser);
      }
    }

    module.exports = Component;

then what we would do is , We will write a subclass for our app-login component:

const Component = require('./component');

class Login extends Component {

  get usernameInput() {
    return this.host.shadow$('input #username');
  }

  get passwordInput() {
    return this.host.shadow$('input[type=password]');
  }

  get submitButton() {
    return this.login.shadow$('button[type=submit]');
  }

  login(username, password) {
    this.usernameInput.setValue(username);
    this.passwordInput.setValue(password);
    this.submitButton.click();
  }
}

module.exports = Login;

Finally, we can use the component object inside our login page object:

const Login = require('./components/login');

class LoginPage {

  open() {
    browser.url('/login');
  }

  get app() {
    return browser.$('my-app');
  }

  get loginComponent() {
    // return a new instance of our login component object
    return new Login(this.app.$('app-login'));
  }

}

Now this component object can now be used in tests for any page or section of your app that uses an app-login web component, without having to know about how that component is structured. If you later decide to change the internal structure of the web component, you only need to update the component object.

Now we apply the same approach with the Check Box Component by using Shadow Dom Support :

public class CheckBox extends Component {
  public CheckBox(element) {
    this.element = element;
  }
  get checkBoxSelector() {
    return this.host.shadow$(element);
  }
  get void toggle() {
    checkBoxSelector().click();
  }
  get void check() {
    if (!isChecked()) {
      toggle();
    }
  }
  get void uncheck() {
    if (isChecked()) {
      toggle();
    }
  }
  get boolean isChecked() {
    return checkBoxSelector().isSelected();
  }
}

Then We can write a Check Box Controller component that can get the instance of check box using id and verify what every is necessary.

const CheckBox= require('./components/CheckBox');
class CheckBoxController{
  open() {
    browser.url('/login');
  }
  get checkboxComponent() {

    // Using this we can verify whether the Specific Check Box has been Selected or Not
    let element = browser.$('[id="lpagecheckbox"]');
    return new CheckBox(element);
  }
}

Note :

Please bear this is not the actual code , This is just a part of the template which can help us to move towards the solution of the Problem .

Source Contends :

https://webdriver.io/docs/api/element/isSelected.html

https://webdriver.io/blog/2019/02/22/shadow-dom-support.html

https://webdriver.io/blog/2019/04/03/react-selectors.html

https://webdriver.io/docs/pageobjects.html

Moreover if we are using Selenium Webdriver , This can help us to Achieve it

Here we have an interface which actually combines all the webdriver interfaces , then we create a Specific implementation by inheriting the Element Class , finally Lets assume of any component you need we should be inheriting and using it with its own implementation , In this case lets assume the Check box that should be inherited from then Element Implementation Class and finally a Cranky way of using it by instantiating the object. CheckBox cb = new CheckBox(element);cb.uncheck();

Step 1:

Create an Interface that combines all of the WebDriver interfaces:

public interface Element extends WebElement, WrapsElement, Locatable {}

Step 2:

Element Implementation Inheriting the element class:

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...

}

Step 3: Consider any component you use , Lets assume Check Box in this case

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();
    }
}

Way of Using It :

CheckBox cb = new CheckBox(element);
cb.uncheck();

If you want More Clear way of Implementing Something Like this : refer the third Link

public class Part2ExampleTest {
    private final WebDriver driver;

    @FindBy(id = "checkbox")
    CheckBox checkBox;

    protected Part2ExampleTest(WebDriver driver) {
        this.driver = driver;
    }

    protected static Part2ExampleTest initialize(WebDriver driver) {
        return ElementFactory.initElements(driver, Part2ExampleTest.class);
    }

    @Test
    public void simple() {
        WebDriver driver = new FirefoxDriver();
        Part2ExampleTest page = initialize(driver);

        PageLoader.get(driver, "forms.html");

        Assert.assertFalse(page.checkBox.isChecked());
        page.checkBox.check();
        Assert.assertTrue(page.checkBox.isChecked());

        driver.close();
    }
}

Sources :

Extend Selenium WebDriver WebElement?

http://elisarver.com/2012/12/09/wrapping-webelement-1/

http://elisarver.com/2012/12/10/wrapping-webelement-2

Verine answered 22/11, 2019 at 14:40 Comment(1)
Thanks for your answer! Unfortunately this is not quite what i expected, since Component will not be instance of WebDriverIO element. I think second Java example is more relevant - i hoped i won't be need to implement interface...Cyclometer
S
-1

IWebElement is an interface that you can just implement inside your driver class.

Subatomic answered 22/11, 2019 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.