在Protractor测试中处理阴影DOM的实例
概述
影子DOM已经慢慢地、稳定地成为现代Web应用程序的一个组成部分。在此之前,网络平台只提供了一种方法来将一大块代码与另一块代码隔离开来--iframe。但对于大多数封装要求来说,框架太重,而且不允许。进入影子DOM。通过这一点,浏览器可以将DOM元素的子树纳入到渲染的文档中,仍然保持它与主文档的DOM树分离。
问题
虽然我们的前端自动化测试在Protractor-with-Jasmine实现下运行良好,但处理影子DOM仍然是一个难以捉摸的挑战。看,protractor确实提供了一个开箱即用的选择器来处理阴影DOM元素--deepCSS
。但事实是,通过deepCSS
来处理影子DOM元素充满了报告但未解决的问题,以及建议但未实施或合并的修正。
解决方案
所以我们决定做一个自定义的定位器来处理影子DOM元素,因为我们应用中的很多元素是通过影子DOM实现的。
我们的方法是像其他树一样遍历影子DOM树,从根到节点,并且只遍历与我们所需路径相匹配的路径,就像选择器路径所决定的那样。记下影子树根节点的直接父节点,可以让我们把影子DOM树映射到主页面DOM树上:
// 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.head.attachShadow;
/**
* Determines whether the given element is a shadow root
* @param {Object} el - web element
*/
const getShadowRoot = function (el) {
return ((el && shadowDomInUse) ? el.shadowRoot : el);
};
还必须牢记,不止一个元素可以匹配任何给定的选择器路径。所以匹配的元素会被保存在一个数组中。此外,我们以递归方式运行,这样我们就可以遍历任何给定节点的所有匹配分支:
/**
* finds all elements matching the given selector, pushes them in an array
* @param {string} selector - CSS selector
* @param {Object} targets - Targetted element
* @param {boolean} firstTry - Whether this is the first attempt to look for the element at the path
*/
const findAllMatches = function (selector, targets, firstTry) {
let using, i;
var matches = [];
for (i = 0; i < targets.length; ++i) {
// traverse root level elements in targets, otherwise if not the first pass
// traverse the nested shadow DOMs in the targets recursively
using = (firstTry) ? targets[i] : getShadowRoot(targets[i]);
if (using) {
if (selector === '') {
// if the selector is empty push the current element in the matches
matches.push(using);
} else {
// get the node list of elements matching the selector, push it in the matches
Array.prototype.push.apply(matches, using.querySelectorAll(selector));
}
}
}
return matches;
};
我们通过Protractor提供的addLocator()
方法将其作为一个自定义匹配器加入,以添加自定义定位器。将其作为一个可导出的模块允许我们在protractor配置文件中导入这个模块,然后在任何规格文件中引用这个选择器:
/**
* Adds shadow root locator; enables selection of elements inside shadow DOMs on a page
*/
exports.addShadowRootLocator = function () {
by.addLocator('css_sr', function (cssSelector, optParentElement) {
// 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.head.attachShadow;
// determines whether the given element is a shadow root
const getShadowRoot = function (el) {
return ((el && shadowDomInUse) ? el.shadowRoot : el);
};
// finds all elements matching the given selector, pushes them in an array
const findAllMatches = function (selector, targets, firstTry) {
let using, i;
var matches = [];
for (i = 0; i < targets.length; ++i) {
// traverse root level elements in targets, otherwise if not the first pass
// traverse the nested shadow DOMs in the targets recursively
using = (firstTry) ? targets[i] : getShadowRoot(targets[i]);
if (using) {
if (selector === '') {
// if the selector is empty push the current element in the matches
matches.push(using);
} else {
// get the node list of elements matching the selector, push it in the matches
Array.prototype.push.apply(matches, using.querySelectorAll(selector));
}
}
}
return matches;
};
// invoke for the first pass on immediate children of the immediate parent node
let matches = findAllMatches(selectors.shift().trim(), [optParentElement || document], true);
// invoke for the rest of the child nodes if the selector path is not empty and immediate child nodes
// of the parent node present in matches
while (selectors.length > 0 && matches.length > 0) {
matches = findAllMatches(selectors.shift().trim(), matches, false);
}
// return array of matches
return matches;
});
};
因此,现在只要我们需要为驻扎在 "阴影 "中的某些元素提供一个路径,我们就可以这样做。
let playerSidebar = element(by.css_sr('::sr .player-sidebar'));
经验之谈
为解决现有代码中的局限性或缺陷而制定定制的解决方案是开源软件开发的本质。随着Protractor在2022年底支持期的结束,这个框架仍然可以通过这样的实现来保持活力,建立在它所提供的灵活性和可靠性之上。