前面大概敘述過 NightwatchJS 的運作方式,接著繼續來說明一些可能會有雷的地方。當然,我覺得這不是每個人都會遇到,應該是我天生比較帶賽的關係。

像是鋤頭打到自己的腳之類的事情。


運作順序

畢竟他還是 JavaScript 的關係,所以運作的方式可能會跟我們想像的有點出入。舉個例子來說,

browser
    .verify.ok(true, '第 1 次 Verify')
browser
    .click('button[type="submit"], function() {
        browser.verify.ok(true, '第 2 次 Verify')
    })
    .verify.ok(true, '第 3 次 Verify');

基本上,看到回呼 (Callback) 就一定不會是上面那個順序,真實順序是這樣,

  1. 第 1 次 Verify
  2. 第 3 次 Verify
  3. 第 2 次 Verify

所以 Nightwatch 提供了一個 .perform 來確保順序的問題。但是!

BUT!

你以為會這麼順利嗎?

browser
    .verify.ok(true, '第 1 次 Verify')
browser
    .perform(function(browser, done) {
        browser.click('button[type="submit"], function() {
            browser.verify.ok(true, '第 2 次 Verify');
            done();
        })
    })
    .verify.ok(true, '第 3 次 Verify');

執行結果就是,

  1. 第 1 次 Verify
  2. 第 3 次 Verify
  3. 第 2 次 Verify

因為 .perform 本身是非同步,所以搭配 verify 或是 assert 原生系列 ( *1 ) 的方法,結果還是會一樣。所以 .perform 只對於 Nightwatch 的其他命令會有效果。

test_verify_order

所以,你會看到 Nightwatch 官方範例當中,都把 verify 或是 assert 放在最後,或是放在 Callback 裡面去跑。反正測試也不管順序,只管對錯而已,所以在哪裡判斷對錯好像都沒差。

但是不是每一種判斷對錯都沒差的好嗎!

所以,一旦遇到這種狀況,我個人會使用 Promise 並且把一系列的動作,打包成 Page Object 或是 Custom Commands 來執行,可以確保執行結果都結束後,才去做判斷的事情。不過這個有點太進階了,之後或許在找時間來介紹。

*1 主要是 NightwatchJS 的 assertverify 可以使用 NodeJS 的 assert 系列命令,這些在測試步驟中都會偷跑。

.click 的莫名雷

一般來說 .click 代表我用滑鼠去點擊該元件,不過,你有時候會發現好像 沒有效果 。然後使用 Callback 來去追蹤回傳的執行結果,你會發現一些奇妙的狀況,

  1. 該元件點不到
  2. 該元件不能點
  3. 該元件沒有顯示所以點不到

我不知道這是否跟瀏覽器有關,不過主要的起因有幾點,

  1. 該元件完全沒出現在畫面上,連 DOM Tree 都沒有
  2. 該元件沒有 click 事件
  3. 使用套件綁定在非原生擁有 click 事件的元件
  4. 該元件是出現在 viewport 以外的畫面,外加 body 捲軸被鎖定

所以,扣除第 1 點之外,其他的就得另外想辦法。通常是說,我們可以利用 getLocationInView 來定位元件,真的有定位到之後,再去做 click 的動作。換成我們人工操作的方法就是,

  1. 先在畫面上看是不是有指定的元件(必須要 visible
  2. 捲動畫面到可以看到那個元件
  3. 滑鼠移上去
  4. 按下滑鼠左鍵

所以說,我在做 .click 之前,會做一個叫 scrollToElement 的命令,

exports.command = function scrollToElement (selector) {
  const self = this;

  this
    .perform(function(browser, done) {
      browser
        .waitForElementPresent(selector, 5000)
        .getLocationInView(selector, function(result) {
          browser
            .execute('scrollTo(0, ' + result.value.y + ')')
            .pause(500)
            .moveToElement(selector, 1, 1, function(result) {
              done();
            });
        });
    });

  return this;
};

那麼我就可以這樣寫,

browser
    .scrollToElement('button[type="submit"]')
    .click('button[type="submit"]')

可能會有人覺得多此一舉吧(我猜

Callback Hell

在 Nightwatch 當中,每一個命令其實都有 Callback 可以使用。不過,為了確保動作的執行順序,所以會出現一大堆 Callback 的狀況,例如,

browser
    .perform(function(browser, done) {
        browser.click('button[type="submit"], function() {
            browser
                .waitForElementVisible('.page-2')
                .getText('.pages', function(result) {
                    if (result.value === 2) {
                        browser.getAttribute('.page' + result.value, 'class', function(result) {
                            if (result.value.indexOf('last-page') > -1) {
                                browser.verify('ok', '最後一頁');
                            }
                        });
                    }
                })
        })
    })
    .verify.ok(true, '第 1 次 Verify');

以上是一個很爛的例子。

為了避免這種狀況,就如同我上面說的,可以使用 Promise 或是 async/await 語法糖來包裝。不過,這些情況通常都是相當 機歪特殊的場合才會使用。之後或許在找時間另闢篇幅來說一些這種狀況。

小結

有沒有測試比較難寫的八卦?