跳至主要內容

Shadow DOM 支援 & 可重複使用的元件物件

·7 分鐘閱讀

Shadow DOM 是構成網頁元件的關鍵瀏覽器功能之一。網頁元件是建構可重複使用元素的好方法,並且能夠擴展到完整的網路應用程式。樣式封裝是賦予 Shadow DOM 力量的功能,在 E2E 或 UI 測試方面一直有點麻煩。不過,事情變得容易了一些,因為 WebdriverIO v5.5.0 透過兩個新指令 shadow$shadow$$ 引入了對 Shadow DOM 的內建支援。讓我們深入探討它們的用途。

歷史

隨著 shadow DOM 規範的 v0 版本,出現了 /deep/ 選擇器。這個特殊的選擇器可以查詢元素 shadowRoot 內部。在這裡,我們查詢位於 my-element 自訂元素 shadowRoot 內部的按鈕

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

/deep/ 選擇器存在時間短暫,並且據傳有一天會被取代。

由於 /deep/ 已被棄用並隨後被移除,開發人員找到了其他方法來取得他們的 Shadow 元素。典型的方法是在 WebdriverIO 中使用自訂指令。這些指令使用 execute 指令來串聯 querySelector 和 shadowRoot.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 中 $ 指令使用函式選擇器的能力。它們的工作方式與現有的 $$$ 指令相同,您可以在元素上呼叫它,但它們查詢元素的 Shadow DOM,而不是查詢元素的 light DOM (如果因任何原因您未使用任何 polyfill,它們會回退到查詢 light DOM)。

由於它們是元素指令,因此在建構查詢時不再需要從根文件開始。一旦有了元素,呼叫 element.shadow$('selector') 會在該元素的 shadowRoot 內部查詢符合給定選擇器的元素。從任何元素,您可以根據需要深入鏈接 $shadow$ 指令。

頁面物件

就像它們的對應指令 $$$ 一樣,shadow 指令使頁面物件易於編寫、閱讀和維護。讓我們假設我們正在處理看起來像這樣的頁面

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

這使用了兩個自訂元素 my-appapp-login。我們可以看到 my-app 位於 body 的 light DOM 中,並且它的 light 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 元素,您只需要變更一個選擇器。

元件物件

單獨遵循頁面物件模式非常強大。網頁元件的最大吸引力在於您可以建立可重複使用的元素。但是,僅使用頁面物件的缺點是,您最終可能會在不同的頁面物件中重複程式碼和選擇器,以便能夠與封裝在網頁元件中的元素互動。

元件物件模式嘗試減少該重複,並將元件的 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 網頁元件的任何頁面或區段的測試中使用,而無需了解該元件的結構。如果您稍後決定變更網頁元件的內部結構,您只需要更新元件物件。

未來

目前,WebDriver 協定不提供對 Shadow DOM 的原生支援,但已針對它進行了進展。一旦規範最終確定,WebdriverIO 將實作該規範。shadow 指令很有可能會在幕後發生變更,但我非常有信心它們的用法將與今天相同,並且使用它們的測試程式碼幾乎不需要任何重構。

瀏覽器支援

IE11-Edge:IE 或 Edge 中不支援 Shadow DOM,但可以使用 polyfill。shadow 指令與 polyfill 非常有效。

Firefox:在 Firefox 中對輸入欄位呼叫 setValue(value) 會導致錯誤,抱怨輸入「無法透過鍵盤存取」。目前的一種解決方法是使用自訂指令 (或元件物件上的方法),透過 browser.execute(function) 設定輸入欄位的值。

Safari:WebdriverIO 有一些安全機制來協助減輕過時元素參考的問題。這是一個非常好的功能,但不幸的是,Safari 的 webdriver 在嘗試與其他瀏覽器中屬於過時元素參考的項目互動時,不會提供正確的錯誤回應。這很不幸,但同時,快取元素參考通常是一種不好的做法。透過使用上面概述的頁面和元件物件模式,通常可以完全減輕過時元素參考的問題。

Chrome:它正常運作。🎉

歡迎!我能幫您什麼嗎?

WebdriverIO AI Copilot