在Protractor测试中处理 Shadow DOM
原文链接:engineering.wingify.com/posts/handl…
概述
Shadow DOM 已经缓慢而稳定地成为现代 Web 应用程序不可或缺的一部分。在此之前,Web 平台仅提供了一种将一大块代码与另一块代码隔离的方法——iframe。但是对于大多数封装要求,框架太重并且不允许。进入影子DOM。通过这种方式,浏览器可以将 DOM 元素的子树包含到呈现的文档中,同时仍将其与主文档的 DOM 树分开。
问题
虽然我们的前端自动化测试在 Protractor-with-Jasmine 实现下运行良好,但处理 shadow DOM 仍然是一个难以捉摸的挑战。看,量角器确实提供了一个开箱即用的选择器来处理 shadow DOM 元素 - deepCSS。但故事的真相是,对 shadow DOM 元素的处理deepCSS充满了已报告但未解决的问题,以及建议但尚未实施或合并的修复程序。
解决方案
所以我们决定制作一个自定义定位器来处理 shadow DOM 元素,因为我们应用程序中的很多元素都是通过 shadow DOM 实现的。
我们的方法是像任何其他树一样遍历 shadow DOM 树,从根到节点,并且只遍历匹配我们所需路径的路径,如选择器路径所指示。记下 shadow 树根节点的直接父节点允许我们将 shadow DOM 树映射到主页 DOM 树。
// 将选择器路径拆分为退化的阴影根级别
const selectors = cssSelector.split('::sr');
// 处理没有提供 CSS 选择器的情况
if (selectors.length === 0) {
return [];
}
// 将影子 DOM 树附加到指定元素的直接父元素
const shadowDomInUse = document.head.attachShadow;
/**
* 判断给定的元素是否是影子根
* @param {Object} el - web element
*/
const getShadowRoot = function (el) {
return ((el && shadowDomInUse) ? el.shadowRoot : el);
};
还必须记住,不止一个元素可以匹配任何给定的选择器路径。所以匹配的元素将保存在一个数组中。此外,我们递归地运行它,以便我们遍历任何给定节点的所有匹配分支。
/**
* 找到与给定选择器匹配的所有元素,将它们压入一个数组
* @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) {
// 遍历targets中的根级元素,否则如果不是第一遍
// 递归遍历目标中嵌套的 shadow DOM
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;
};
我们通过 Protractor 提供的方法将其添加为自定义匹配器addLocator()以添加自定义定位器。使其成为可导出模块允许我们将其导入量角器配置文件中,然后在任何规范文件中引用选择器。
/**
* 添加影子根定位器;允许选择页面上 shadow DOM 内的元素
*/
exports.addShadowRootLocator = function () {
by.addLocator('css_sr', function (cssSelector, optParentElement) {
// 将选择器路径拆分为退化的阴影根级别
const selectors = cssSelector.split('::sr');
// 处理没有提供 CSS 选择器的情况
if (selectors.length === 0) {
if (selectors.length === 0) {
return [];
}
// 将影子 DOM 树附加到指定元素的直接父元素
const shadowDomInUse = document.head.attachShadow;
// determines whether the given element is a shadow root
const getShadowRoot = function (el) {
return ((el && shadowDomInUse) ? el.shadowRoot : el);
};
//找到与给定选择器匹配的所有元素,将它们压入一个数组
const findAllMatches = function (selector, targets, firstTry) {
let using, i;
var matches = [];
for (i = 0; i < targets.length; ++i) {
// 遍历targets中的根级元素,否则如果不是第一遍
// 递归遍历目标中嵌套的 shadow DOM
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(), [optParentElement || document], true);
// 如果选择器路径不为空和直接子节点,则调用其余的子节点
// 匹配中存在的父节点
while (selectors.length > 0 && matches.length > 0) {
matches = findAllMatches(selectors.shift().trim(), matches, false);
}
// 返回匹配数组
return matches;
});
};
因此,现在每当我们需要为位于“阴影”中的某些元素提供路径时,我们这样做:
let playerSidebar = element(by.css_sr('::sr .player-sidebar'));
外卖
为解决现有代码中的限制或缺陷制定定制解决方案是开源软件开发的本质。随着量角器在 2022 年末即将结束支持,该框架仍然可以通过此类实现保持活力,建立在其提供的灵活性和可靠性之上。