diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index ad795ec51..40f9a3704 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -704,6 +704,60 @@ functions.findByCssContainingText = function(cssSelector, searchText, using) { return matches; }; +/** + * Find an element throughout the Shadow DOM by extended css selector. + * In order to go into the shadow tree of the element, a special selector `::sr` is used. + * As compatible with both Shadow DOM v1 and prior specification, `::sr` will be ignored + * for the browser with no support of Shadow DOM, so the result will be same. + * + * Usage: + * element(by.shadowRoot('#outer #inner')) <=> $('#outer #inner') + * element(by.shadowRoot('#outer::sr #inner')) <=> $('#outer').shadowRoot.$('#inner') + * element.all(by.shadowRoot('#outer .inner')) <=> $$('#outer .inner') + * element.all(by.shadowRoot('#outer::sr .inner')) <=> $$('#outer').shadowRoot.$$('.inner') + * outer.element(by.shadowRoot('#inner')) <=> outer.$('#inner') + * outer.element(by.shadowRoot('::sr #inner')) <=> outer.shadowRoot.$('#inner') + * outer.all(by.shadowRoot('.inner')) <=> outer.$$('.inner') + * outer.all(by.shadowRoot('::sr .inner')) <=> outer.shadowRoot.$$('.inner') + * + * @param {string} selector An extended css selector, each `::sr` in which + * will go into the shadow root of the element. + * @param {Element} using The scope of the search. + * + * @return {Array.} The matching elements. + */ +functions.findByShadowRoot = function(selector, using) { + var selectors = selector.split('::sr'); + if (selectors.length === 0) { + return []; + } + + var shadowDomInUse = (document.head.createShadowRoot || document.head.attachShadow); + var getShadowRoot = function (el) { + return ((el && shadowDomInUse) ? el.shadowRoot : el); + }; + var findAllMatches = function (selector /*string*/, targets /*array*/, firstTry /*boolean*/) { + var scope, i, matches = []; + for (i = 0; i < targets.length; ++i) { + scope = (firstTry) ? targets[i] : getShadowRoot(targets[i]); + if (scope) { + if (selector === '') { + matches.push(scope); + } else { + Array.prototype.push.apply(matches, scope.querySelectorAll(selector)); + } + } + } + return matches; + }; + + var matches = findAllMatches(selectors.shift().trim(), [using || document], true); + while (selectors.length > 0 && matches.length > 0) { + matches = findAllMatches(selectors.shift().trim(), matches, false); + } + return matches; +}; + /** * Tests whether the angular global variable is present on a page. Retries * in case the page is just loading slowly. diff --git a/lib/locators.ts b/lib/locators.ts index f6852ea02..da8d11d47 100644 --- a/lib/locators.ts +++ b/lib/locators.ts @@ -491,4 +491,41 @@ export class ProtractorBy extends WebdriverBy { // When that is supported, switch it here. return By.css('* /deep/ ' + selector); }; + + /** + * Find an element throughout the Shadow DOM by extended css selector. + * In order to go into the shadow tree of the element, use a special selector `::sr`. + * As compatible with both Shadow DOM v1 and prior specification, `::sr` will be ignored + * for the browser with no support of Shadow DOM, so the result will be same. + * + * @alias by.shadowRoot(selector) + * @view + *
+ * + * <"shadow tree"> + * + * <"shadow tree"> + * + * + * + *
+ * @example + * var span1 = element(by.shadowRoot('#outerspan::sr #span1')); + * var span2 = element(by.shadowRoot('#outerspan::sr #span1::sr #span2')); + * + * @param {string} selector an extended css selector, each `::sr` in which will go into the shadow root of the element + * @returns {Locator} location strategy + */ + shadowRoot(selector: string): Locator { + return { + findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): + wdpromise.Promise => { + return driver.findElements( + By.js(clientSideScripts.findByShadowRoot, selector, using, rootSelector)); + }, + toString: (): string => { + return 'by.shadowRoot("' + selector + '")'; + } + }; + }; } diff --git a/spec/basic/locators_spec.js b/spec/basic/locators_spec.js index 0f370b00b..84f796517 100644 --- a/spec/basic/locators_spec.js +++ b/spec/basic/locators_spec.js @@ -414,6 +414,27 @@ describe('locators', function() { }); }); + describe('by shadowRoot', function() { + beforeEach(function() { + browser.get('index.html#/shadow'); + }); + + it('should find items inside the shadow DOM', function() { + var parent = element(by.shadowRoot('#innerDiv::sr #parentDiv')); + var parentHeading = parent.element(by.shadowRoot('.parentshadowheading')); + var youngerChildHeading = parent.element(by.shadowRoot('::sr .youngershadowheading')); + + expect(parentHeading.isPresent()).toBe(true); + expect(youngerChildHeading.isPresent()).toBe(true); + + expect(parentHeading.getText()).toEqual('Parent'); + expect(youngerChildHeading.getText()).toEqual('Younger Child'); + + expect(element(by.shadowRoot('.originalcontent')).getText()) + .toEqual('original content'); + }); + }); + it('should determine if an element is present', function() { expect(browser.isElementPresent(by.binding('greet'))).toBe(true); expect(browser.isElementPresent(by.binding('nopenopenope'))).toBe(false);