Friday, 22 March 2013

Element Object - ExtJs Combobox

The topic is becoming more and more hot nowadays. Element object pretends to be evolved into the separate layer of any automation framework. And it highly depends on a UI framework chosen.
Frameworks like ExtJS have their internal way of generating ids, classes, etc ... and naturally to let webdriver operate with it you need to implement user conductible API
We've implemented it for some of extjs standard components like table, alert and combobox, which I'd like to share here

public class ExtJsComboBox {

  private static final String COMBO_EXT_JS_OBJECT = "var id = arguments[0].id.replace('-inputEl', '')" +
          ".replace('-triggerWrap','');var el = Ext.getCmp(id);";
  private static final By BOUND_LIST_LOCATOR = By.cssSelector("li.x-boundlist-item");
  private WebElement element;
  private WebElement input;
  private String listDynId = null;
  private static final By TEXT_INPUT_LOCATOR = By.cssSelector("input.x-form-field.x-form-text");
  private final WebDriver driver;
  private WebDriverWait wait;

  protected String getListDynId() {
    return listDynId;
  }

  /**
   * sets id of generated list with combobox options
   */
  protected void setListDynId() {
    listDynId = (String) ((JavascriptExecutor) driver).executeScript(
            COMBO_EXT_JS_OBJECT + "el.expand(); return el.listKeyNav.boundList.id;"
            , getTextInput());
  }

  /**
   * @param elementContainer - locator of either parent element which wraps text input and drop down button or text input
   */
  public ExtJsComboBox(WebDriver driver, WebElement elementContainer) {
    this.driver = driver;
    wait = new WebDriverWait(driver, 5);
    setElement(elementContainer);
  }

  private void setElement(WebElement el) {
    element = el;
  }

  /**
   * sends arrow key to text box
   */
  public void retrieveOptions() {
    sendKeys(Keys.ARROW_DOWN);
  }

  /**
   * @param optionToChoose - option to choose and additional keys to send
   */
  public void chooseOption(CharSequence... optionToChoose) {
    chooseOption(optionToChoose[0].toString());
    for (int i = 1; i < optionToChoose.length; i++) {
      getTextInput().sendKeys(optionToChoose[i]);
    }
  }
Main magic is probably here, at chooseOption() method
it iterates among option list looking for a partial match avoiding staleness of element which is often the case
  /**
   * chooses option if one is present
   * @param optionToChoose - partial text to find among options
   */
  public void chooseOption(final String optionToChoose) {
    clear();
    if (getListDynId() == null) {
      setListDynId();
    }
    List<WebElement> optionList = getOptionElements();
    if (optionList.size() == 0) {
      sendKeys(optionToChoose);
      new WebDriverWait(driver, 2, 200).until(
              new ExpectedCondition<Boolean>() {
                @Override
                public Boolean apply(final WebDriver webDriver) {
                  return isDirty();
                }
              }
      );
    }

    wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#" + getListDynId() + " li.x-boundlist-item")));

    for (int i = 0; i < optionList.size(); i++) {
      String actualOption;
      try {
        actualOption = optionList.get(i).getText().toLowerCase();
      } catch (StaleElementReferenceException e) {
        optionList = getOptionElements();
        i--;
        continue;
      }
      if (actualOption.contains(optionToChoose.toLowerCase())) {
        optionList.get(i).click();
        collapseDropDown();
        wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id(getListDynId())));
        break;
      }
    }
  }

  private Boolean isDirty() {
    return (Boolean) ((JavascriptExecutor) driver).executeScript(
            COMBO_EXT_JS_OBJECT + " return el.isDirty();"
            , getTextInput());
  }

  private void collapseDropDown() {
    ((JavascriptExecutor) driver).executeScript(
            COMBO_EXT_JS_OBJECT + " el.collapse();"
            , getTextInput());
  }

  /**
   * @return list of available options
   */
  public List<String> getOptions() {
    retrieveOptions();
    List<String> optionStrList = new ArrayList<String>();
    List<WebElement> optionList = getOptionElements();
    for (WebElement option : optionList) {
      optionStrList.add(option.getText().trim().toLowerCase());
    }
    retrieveOptions();
    return optionStrList;
  }

  /**
   * @return list of available option elements
   */
  private List<WebElement> getOptionElements() {
    return getListContainer().findElements(BOUND_LIST_LOCATOR);
  }

  /**
   * @return web element by dynamic id of reloaded list
   */
  private WebElement getListContainer() {
    if (getListDynId() == null) {
      setListDynId();
    }
    return driver.findElement(By.id(getListDynId()));
  }

  private WebElement getTextInput() {
    if (input == null) {
      if (!element.getTagName().equals(HTML.Tag.INPUT.toString())) {
        input = element.findElement(TEXT_INPUT_LOCATOR);
      } else {
        input = element;
      }
    }
    return input;
  }

  public void clear() {
    getTextInput().clear();
  }

  public String getAttribute(String arg0) {
    return getTextInput().getAttribute(arg0);
  }

  public void sendKeys(CharSequence... arg0) {
    getTextInput().sendKeys(arg0);
  }
}

There is also multi selection combobox which behaves in a bit different way. You can easily extend this one or just put a comment here and I'll post it
it is important to highlight that main actions are done by webdriver, not js, which makes it more user alike. Though it was impossible to avoid js completely - it would impact performance and stability a lot.

5 comments:

  1. Hi Tim

    Can you tell me where the value assigned to private static final String COMBO_EXT_JS_OBJECT

    Comes from ?

    Thanks
    Sean

    ReplyDelete
  2. Hi SeanM,
    that is just a part of js I customly execute to obtain combobox as extjs object.
    getCmp() returns it, but usually you depend webdriver locator on inner input, which usually has -inputEl suffix
    it is done to have several different ways letting webdriver find combobox within DOM

    welcome :)

    ReplyDelete
  3. Is there a public jar or source code for the ExtJS Objects? (Checkbox, Grid, etc)
    Thanks!
    Taylor

    ReplyDelete
    Replies
    1. well, no ...
      I thought of creating some but have not found time for that yet
      probably later, it might include grid, combo .. not sure for checkbox it is necessary

      by the way, I do not use approach from a post anymore
      it is not very stable, much better to set/get value via extjs native events and execute them via javascript native executor

      if you have some peculiar problem I can help you with it

      Delete
  4. Thank you. This code was very helpful and only possible breakthrough!

    ReplyDelete