Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Protractor: Unable select input element inside a shadow DOM (Polymer) using by.deepCss('input')

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

like image 762
Bachu Avatar asked Jan 23 '18 13:01

Bachu


People also ask

What is a shadow DOM element?

Shadow DOM allows hidden DOM trees to be attached to elements in the regular DOM tree — this shadow DOM tree starts with a shadow root, underneath which you can attach any element, in the same way as the normal DOM.

What is shadow DOM with example?

Shadow DOM lets you place the children in a scoped subtree, so document-level CSS can't restyle the button by accident, for example. This subtree is called a shadow tree. The shadow root is the top of the shadow tree. The element that the tree is attached to ( <my-header> ) is called the shadow host.

How do you handle shadow root in protractor?

// split the selector path into degenerate shadow root levels const selectors = cssSelector. split('::sr'); // handling the case where no CSS selector is provided if (selectors. length === 0) { return []; } // attach a shadow DOM tree to the specified element's immediate parent const shadowDomInUse = document.


2 Answers

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();
like image 97
JiggyJinjo Avatar answered Nov 12 '22 02:11

JiggyJinjo


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();
like image 44
Linus Avatar answered Nov 12 '22 02:11

Linus