8
votes

Environment: Angular (v5 Application with Polymer Web Components. Protractor for running e2e tests.

Angular CLI: 1.6.4
Node: 6.10.0
Angular: 5.2.0
@angular/cli: 1.6.4
typescript: 2.5.3

Below given is my polymer web component shadow root expanded in chrome. You could see input type = "text" inside this custom element.

I am unable to access input element inside custom polymer component using protractor by.deepCss.

var polymerFirstName = element(by.className('polyFName'));

var inputs = polymerFirstName.element(by.deepCss('input')); // returns nothing.

enter image description here

I need to access the inner input element so that I can perform UI Automation tasks like.

element(by.deepCss('input')).clear();

element(by.deepCss('input')).sendKeys('Ritchie');

If I try to invoke .clear or .sendKeys directly on Polymer components it will fail with "Failed: invalid element state: Element must be user-editable in order to clear it". Basically I cannot call .clear or .sendKeys on to custom input element created using Polymer.

How can I access inner input element inside shadow DOM from a protractor test?

Thanks

Basanth

3
what does polymerFirstName evaluate to?zmii
polymerFirstName is custom element(Polymer) my_input_textBachu
Why do you use deepCss instead of css? Is it really shadow DOM?Kacper

3 Answers

8
votes

Just create a new locator :

/**
 * Usage:
 *   O  element(by.css_sr('#parentElement #innerElement'))          <=> $('#parentElement #innerElement')
 *   O  element(by.css_sr('#parentElement::sr #innerElement'))      <=> $('#parentElement').shadowRoot.$('#innerElement')
 *   O  element.all(by.css_sr('#parentElement .inner-element'))     <=> $$('#parentElement .inner-element')
 *   O  element.all(by.css_sr('#parentElement::sr .inner-element')) <=> $$('#parentElement').shadowRoot.$$('.inner-element')
 *   O  parentElement.element(by.css_sr('#innerElement'))           <=> parentElement.$('#innerElement')
 *   O  parentElement.element(by.css_sr('::sr #innerElement'))      <=> parentElement.shadowRoot.$('#innerElement')
 *   O  parentElement.all(by.css_sr('.inner-element'))              <=> parentElement.$$('.inner-element')
 *   O  parentElement.all(by.css_sr('::sr .inner-element'))         <=> parentElement.shadowRoot.$$('.inner-element')
 */
by.addLocator('css_sr', (cssSelector: string, opt_parentElement, opt_rootSelector) => {
    let selectors = cssSelector.split('::sr');
    if (selectors.length === 0) {
        return [];
    }

    let shadowDomInUse = (document.head.createShadowRoot || document.head.attachShadow);
    let getShadowRoot  = (el) => ((el && shadowDomInUse) ? el.shadowRoot : el);
    let findAllMatches = (selector: string, targets: any[], firstTry: boolean) => {
        let using, i, matches = [];
        for (i = 0; i < targets.length; ++i) {
            using = (firstTry) ? targets[i] : getShadowRoot(targets[i]);
            if (using) {
                if (selector === '') {
                    matches.push(using);
                } else {
                    Array.prototype.push.apply(matches, using.querySelectorAll(selector));
                }
            }
        }
        return matches;
    };

    let matches = findAllMatches(selectors.shift().trim(), [opt_parentElement || document], true);
    while (selectors.length > 0 && matches.length > 0) {
        matches = findAllMatches(selectors.shift().trim(), matches, false);
    }
    return matches;
});

See here for help.

You can call it like this :

inputs = element(by.css_sr('.polyFName::sr input'))
// OR
inputs = polymerFirstName.element(by.css_sr('::sr input')
inputs.clear();
1
votes

As a quick fix you might want to use something like this:

async function findShadowDomElement(shadowHostSelector, shadowElementSelector): Promise<WebElement> {
  let shadowHost = browser.findElement(by.css(shadowHostSelector));
  let shadowRoot: any = await browser.executeScript("return arguments[0].shadowRoot", shadowHost);
  return shadowRoot.findElement(by.css(shadowElementSelector));
}

Use can use this function like this:

let e: WebElement = await findShadowDomElement('ion-toast', '.toast-button');

Note that you now have a WebElement not an ElementFinder! So you have to use WebElement functions:

expect<any>(await e.isDisplayed()).toBeTruthy();
await e.click();
0
votes

With the help of the node module "query-selector-shadow-dom", we can identify the Web elements in the Shadow DOM with ease. No need to traverse through all the Shadow roots. Using element(by.shadowDomCss('')), we can point to the Web element inside the Shadow Root at any level.