From 5f1b88ef7bb29c6185cacb972326bf9b8abd6301 Mon Sep 17 00:00:00 2001 From: firstor Date: Mon, 4 Dec 2017 23:56:17 -0800 Subject: [PATCH 1/4] Add new Protractor locator - shadowRoot to find an element throughout the Shadow DOM --- lib/clientsidescripts.js | 57 +++++++++++++++++++++++++++++++++++++--- lib/locators.ts | 35 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index c741c8d7e..5887577fd 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -86,7 +86,7 @@ function getNg1Hooks(selector, injectorPlease) { return {$injector: $injector}; } } - } catch(err) {} + } catch(err) {} } function trySelector(selector) { var els = document.querySelectorAll(selector); @@ -277,7 +277,7 @@ function findRepeaterRows(repeater, exact, index, using) { var row = rows[index] || [], multiRow = multiRows[index] || []; return [].concat(row, multiRow); } -functions.findRepeaterRows = wrapWithHelpers(findRepeaterRows, repeaterMatch); +functions.findRepeaterRows = wrapWithHelpers(findRepeaterRows, repeaterMatch); /** * Find all rows of an ng-repeat. @@ -654,6 +654,57 @@ 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. + * + * 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 = cssSelector.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. @@ -697,7 +748,7 @@ functions.testForAngular = function(attempts, ng12Hybrid, asyncCallback) { if (n < 1) { if (definitelyNg1 && window.angular) { callback({message: 'angular never provided resumeBootstrap'}); - } else if (ng12Hybrid && !window.angular) { + } else if (ng12Hybrid && !window.angular) { callback({message: 'angular 1 never loaded' + window.getAllAngularTestabilities ? ' (are you sure this app ' + 'uses ngUpgrade? Try un-setting ng12Hybrid)' : ''}); diff --git a/lib/locators.ts b/lib/locators.ts index 5cf944a30..e67d792fd 100644 --- a/lib/locators.ts +++ b/lib/locators.ts @@ -489,4 +489,39 @@ 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`. + * + * @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 + '")'; + } + }; + }; } From 6fbf48e94c5b2dd4b398a68440f264f06588866b Mon Sep 17 00:00:00 2001 From: firstor Date: Tue, 5 Dec 2017 00:10:55 -0800 Subject: [PATCH 2/4] Add additional comments related with Shadow DOM compatiblity into the shadowRoot locator --- lib/clientsidescripts.js | 2 ++ lib/locators.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index 5887577fd..1c8ff15d7 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -657,6 +657,8 @@ functions.findByCssContainingText = function(cssSelector, searchText, using) { /** * 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') diff --git a/lib/locators.ts b/lib/locators.ts index e67d792fd..c9487b9bb 100644 --- a/lib/locators.ts +++ b/lib/locators.ts @@ -493,6 +493,8 @@ export class ProtractorBy extends WebdriverBy { /** * 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 From 80484f1f7a67f6611178a7dac31d1644c7971ccf Mon Sep 17 00:00:00 2001 From: firstor Date: Tue, 5 Dec 2017 00:42:01 -0800 Subject: [PATCH 3/4] Fix lint errors --- lib/clientsidescripts.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index 1c8ff15d7..f2dd4be51 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -670,13 +670,14 @@ functions.findByCssContainingText = function(cssSelector, searchText, using) { * 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 {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 = cssSelector.split('::sr'); + var selectors = selector.split('::sr'); if (selectors.length === 0) { return []; } From b6772e545f4d0e1c78d128a8a265eba100f59d06 Mon Sep 17 00:00:00 2001 From: firstor Date: Wed, 6 Dec 2017 00:03:06 -0800 Subject: [PATCH 4/4] Add test spec for shadowRoot locator --- spec/basic/locators_spec.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/basic/locators_spec.js b/spec/basic/locators_spec.js index 6d64bb727..34b1c5574 100644 --- a/spec/basic/locators_spec.js +++ b/spec/basic/locators_spec.js @@ -394,6 +394,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);