前幾天 Protractor 的課程終於上完了,有學員就馬上在自己的網站上練習 E2E 的自動化測試。 寫了下方的程式碼

failed test
  • ts
1
2
3
4
5
6
7
8
9
it('should click the link', async () => {
await browser.waitForAngularEnabled(false);
await browser.get('https://xxx.sample.com/#/');
const link = element(by.className('anticon-down'));
const linkIsEnabled = EC.elementToBeClickable(link);
// 點擊連結之前,先確認連結能被點擊!!!
await browser.wait(linkIsEnabled, 10000);
await link.click();
});

這程式碼看起來合情合理,結果在 link.click() 出現錯誤。 錯誤訊息大概是呈現這樣子。
Failed: unknown error: Element < i class="anticon-down">< / i > is not clickable at point (165, 720). Other element would receive the click: < div class="Loader__foreground" style="display: table; width: 100%; height: 100%; text-align: center; z-index: 20; color: white;">...< /div>

這段的意思是說,我要點的連結的時候,點到其他 DOM 物件

說明

程式碼看起來很合理,人工操作上也都沒有問題,可是用 Protractor 測試的時候,竟然點到 其他元件,這是什麼情況??
首先要先知道, Protractor 會盡量模擬使用者的行為去操作,以 click 動作來說, Protractor 會模擬使用者操作滑鼠,移動到該座標,點下滑鼠左鍵。 其次,無法點擊有 三種可能

  • 連結被 disabled
  • 連結看不到
  • 連結被其他 DOM 物件蓋住了

因為我們已經使用 ExpectedConditions.elementToBeClickable 確定連結是可以點的,就剩下一個可能,就是連結被其他DOM 物件蓋住了

所以我合理的推測,網頁載入的時候,有一個看不見的 DOM 物件把畫面蓋住了,而且載入結束後,那個 DOM 物件就會消失。 且根據錯誤訊息,那個 DOM 物件用的 class 是 Loader__foreground,為了驗證這件事情,我寫了以下程式碼

print out page source
  • ts
1
2
3
4
5
6
7
it('should print out page source', async () => {
await browser.waitForAngularEnabled(false);
await browser.get('https://xxx.sample.com/#/');
const loader = element(by.className('Loader__foreground'));
await browser.wait(EC.presenceOf(loader), 10000);
console.log(await browser.getPageSource());
});

Bingo ,發現一段神奇的 HTML。 果然有一個 DOM 元件是 z-index: 20 且寬高都 100%。 這表示說,網頁在讀取階段,這個透明的 DOM 元件把畫面都蓋住了。

HTML with loader
  • html
1
2
3
4
5
6
7
8
9
< div class="Loader__background"
style="display: block; position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 10;">
< div class="Loader__foreground"
style="display: table; width: 100%; height: 100%; text-align: center; z-index: 20; color: white;">
< div class="Loader__message"
style="display: block; vertical-align: middle; position: fixed; top: 0px; right: 0px; color: rgb(255, 255, 255); background-color: rgb(14, 119, 202); height: 35px; line-height: 35px; font-size: 14px; padding: 0px 30px;">
loading ...< /div>
< /div>
< /div>

可以成功點擊連結的範例

所以我調整程式碼,確定讀取畫面消失後,才點擊連結。

success
  • ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
it('should click the link', async () => {
await browser.waitForAngularEnabled(false);
await browser.get('https://xxx.sample.com/#/');
const link = element(by.className('anticon-down'));
const loader = element(by.className('Loader__foreground'));

// 先等 loader 畫面有出來
await browser.wait(EC.presenceOf(loader), 10000);

// 等待 loader 消失,且連結可以被點擊
const loaderIsOff = EC.stalenessOf(loader);
const linkIsEnabled = EC.elementToBeClickable(link);
await browser.wait(EC.and(loaderIsOff, linkIsEnabled), 10000);

// 點擊連結
await link.click();

// 觀察結果
await browser.sleep(5000);
});

打完收工!!

小結

寫 E2E 真的會有很多鬼打強的情況,這時候,更要仔細閱讀錯誤訊息,因為錯誤訊息其實蠻清楚的,只是要思考一下出現這錯誤訊息的可能原因。

其他小密技(建議不要使用)

如果真的都沒辦法點,又找不到原因,只好使用注入 javascript 點擊 !?

注入 javascript
  • ts
1
await browser.executeScript('document.querySelectorAll(\'.anticon-down\')[0].click()');

參考

[Random Element is not clickable at point (x, y) errors]
[WebDriver click() vs JavaScript click()]