跳至主要内容

Shadow DOM 支持与可复用组件对象

·阅读时长 7 分钟

Shadow DOM 是构成 Web Components 的关键浏览器功能之一。Web Components 是一种构建可复用元素的绝佳方法,并且能够扩展到完整的 Web 应用程序。样式封装是赋予 Shadow DOM 强大功能的功能,但在进行端到端或 UI 测试时一直是一个难题。不过,事情变得稍微简单了一些,因为 WebdriverIO v5.5.0 通过两个新命令引入了对 Shadow DOM 的内置支持,即 shadow$shadow$$。让我们深入了解一下它们的功能。

历史

随着 Shadow DOM 规范的 v0 版本的发布,出现了 /deep/ 选择器。这个特殊的选择器使得查询元素的 shadowRoot 内部成为可能。这里我们正在查询一个位于 my-element 自定义元素的 shadowRoot 内部的按钮。

$('body my-element /deep/ button');

/deep/ 选择器生命周期短暂,并且有传言称将来会被替换

随着 /deep/ 被弃用并随后移除,开发人员找到了其他方法来访问其 Shadow 元素。典型的方法是在 WebdriverIO 中使用自定义命令。这些命令使用 execute 命令将 querySelectorshadowRoot.querySelector 调用串联起来以查找元素。这通常有效,因此,查询不是基本的字符串查询,而是被放入数组中。数组中的每个字符串都表示一个 Shadow 边界。使用这些命令看起来像这样

const myButton = browser.shadowDomElement(['body my-element', 'button']);

/deep/ 选择器和 JavaScript 方法的缺点是,为了查找元素,查询始终需要从文档级别开始。这使得测试有点笨拙且难以维护。类似这样的代码并不少见

it('submits the form', ()=> {
const myInput = browser.shadowDomElement(BASE_SELECTOR.concat(['my-deeply-nested-element', 'input']));
const myButton = browser.shadowDomElement(BASE_SELECTOR.concat(['my-deeply-nested-element', 'button']));
myInput.setValue('test');
myButton.click();
});

shadow$shadow$$ 命令

这些命令利用了 WebdriverIO v5 中 $ 命令使用函数选择器功能的能力。它们的工作原理与现有的 $$$ 命令相同,您可以在元素上调用它,但它们不是查询元素的光 DOM,而是查询元素的 Shadow DOM(如果由于某种原因您没有使用任何 polyfills,它们会回退到查询光 DOM)。

由于它们是元素命令,因此在构建查询时不再需要从根文档开始。获取元素后,调用 element.shadow$('selector') 会在该元素的 shadowRoot 中查询与给定选择器匹配的元素。从任何元素开始,您可以根据需要深度链接 $shadow$ 命令。

页面对象

$$$ 类似,Shadow 命令使页面对象的编写、阅读和维护变得轻而易举。假设我们正在处理一个看起来像这样的页面

<body>
<my-app>
<app-login></app-login>
</my-app>
</body>

这使用了两个自定义元素,my-appapp-login。我们可以看到 my-app 位于 body 的光 DOM 中,并且在其光 DOM 内部有一个 app-login 元素。与该页面交互的页面对象的示例可能如下所示

class LoginPage {

open() {
browser.url('/login');
}

get app() {
// my-app lives in the document's light DOM
return browser.$('my-app');
}
get login() {
// app-login lives in my-app's light DOM
return this.app.$('app-login');
}

get usernameInput() {
// the username input is inside app-login's shadow DOM
return this.login.shadow$('input #username');
}

get passwordInput() {
// the password input is inside app-login's shadow DOM
return this.login.shadow$('input[type=password]');
}
get submitButton() {
// the submit button is inside app-login's shadow DOM
return this.login.shadow$('button[type=submit]');
}

login(username, password) {
this.login.setValue(username);
this.username.setValue(password);
this.submitButton.click();
}
}

在上面的示例中,您可以看到如何轻松利用页面对象的 getter 方法深入到应用程序的不同部分。这使您的选择器保持简洁和集中。例如,如果您决定移动 app-login 元素,则只需更改一个选择器即可。

组件对象

遵循页面对象模式本身就非常强大。Web Components 的最大优势在于您可以创建可复用元素。但是,仅使用页面对象的缺点是,您可能最终会在不同的页面对象中重复代码和选择器,以便能够与 Web Components 中封装的元素进行交互。

组件对象模式试图减少这种重复,并将组件的 API 移到它自己的对象中。我们知道,为了与元素的 Shadow DOM 交互,我们首先需要宿主元素。使用组件对象的基类可以使此操作非常简单。这是一个基本的组件基类,它在其构造函数中获取 host 元素,并将该元素的查询展开到浏览器对象,以便它可以在许多页面对象(或其他组件对象)中重复使用,而无需了解页面本身的任何信息

class Component {

constructor(host) {
const selectors = [];
// Crawl back to the browser object, and cache all selectors
while (host.elementId && host.parent) {
selectors.push(host.selector);
host = host.parent;
}
selectors.reverse();
this.selectors_ = selectors;
}

get host() {
// Beginning with the browser object, reselect each element
return this.selectors_.reduce((element, selector) => element.$(selector), browser);
}
}

module.exports = Component;

然后我们可以为我们的 app-login 组件编写一个子类

const Component = require('./component');

class Login extends Component {

get usernameInput() {
return this.host.shadow$('input #username');
}

get passwordInput() {
return this.host.shadow$('input[type=password]');
}

get submitButton() {
return this.login.shadow$('button[type=submit]');
}

login(username, password) {
this.usernameInput.setValue(username);
this.passwordInput.setValue(password);
this.submitButton.click();
}
}

module.exports = Login;

最后,我们可以在登录页面对象中使用组件对象

const Login = require('./components/login');

class LoginPage {

open() {
browser.url('/login');
}

get app() {
return browser.$('my-app');
}

get loginComponent() {
// return a new instance of our login component object
return new Login(this.app.$('app-login'));
}

}

现在,此组件对象可用于使用 app-login Web Components 的应用程序的任何页面或部分的测试,而无需了解该组件的结构。如果您以后决定更改 Web Components 的内部结构,则只需更新组件对象即可。

未来

目前,WebDriver 协议未提供对 Shadow DOM 的原生支持,但已经取得了一些进展。一旦规范最终确定,WebdriverIO 将实现该规范。shadow 命令很有可能在幕后发生变化,但我非常有信心它们的使用方式将与今天相同,并且使用它们的测试代码将几乎不需要重构。

浏览器支持

IE11-Edge:IE 或 Edge 不支持 Shadow DOM,但可以使用 polyfills。Shadow 命令与 polyfills 配合使用效果很好。

Firefox:在 Firefox 中对输入字段调用 setValue(value) 会导致错误,抱怨输入“无法通过键盘访问”。目前的解决方法是使用自定义命令(或组件对象上的方法),通过 browser.execute(function) 设置输入字段的值。

Safari:WebdriverIO 有一些安全机制可以帮助减轻陈旧元素引用的问题。这是一个非常好的功能,但不幸的是,Safari 的 WebDriver 在尝试与其他浏览器中的陈旧元素引用交互时,不会提供正确的错误响应。这很遗憾,但同时,缓存元素引用通常是一种不好的做法。通过使用上面概述的页面和组件对象模式,通常可以完全缓解陈旧元素引用问题。

Chrome:它可以正常工作。🎉

欢迎!我如何提供帮助?

WebdriverIO AI Copilot