Geb UI 自動化手冊(2:Browser)

2. 瀏覽器(Browser)

        Geb 執行的入口點是 Browser 對象。每個 Browser 對象底層都綁定了一個 WebDriver 實例(用於驅動瀏覽器進行自動化),並且具有 “當前頁面” 的概念,表示瀏覽器當前所處的頁面。

        Browser 對象是通過 Configuration (配置)來創建的。Configuration 中指定了使用的 WebDriver 實現,用於解析相對 URL 的基礎 URL (baseUrl),及其他各種配置。使用配置機制允許將設置 Geb 如何運行的操作獨立出來,這意味着同一份 Geb 代碼或測試套件能夠運行在不同的瀏覽器和網站上。後續關於 Configuration 的章節包含更詳細的關於如何管理配置參數以及有哪些可配置參數的介紹。

Browser 類的默認構造函數就是簡單的從配置機制加載配置:

import geb.Browser

def browser = new Browser()

然而,如果你需要指定 WebDriver 實現(或需要設置 Browser 上的其他可配置屬性) ,就可以利用 Groovy 的基於 map 的構造函數語法來指定:

import geb.Browser
import org.openqa.selenium.htmlunit.HtmlUnitDriver

def browser = new Browser(driver: new HtmlUnitDriver())

上面的代碼和這下面的等效

def browser = new Browser()
browser.driver = new HtmlUnitDriver()

任何通過此方式設置的屬性值都會覆蓋從配置機制中獲取到的屬性值。

注意:如果 Browser 的 driver 對象在第一次使用後被改變,行爲將是未定義的。所以儘量避免以這種方式是在 driver,使用配置機制設置是比較推薦的方式。

如果有非常多的定製化配置需求,可以考慮創建自己的 Configuration 對象,並使用它來創建 Browser,類似於使用配置加載器的方式:

import geb.Browser
import geb.ConfigurationLoader

def loader = new ConfigurationLoader("a-custom-environment")
def browser = new Browser(loader.conf)

如果可能,應該堅持使用無參的構造函數並且通過內置的配置機制來管理 Geb,因爲它提供了更大的靈活性並且使配置和代碼分離。

注:Geb 的集成項目一般都會移除創建瀏覽器的操作,框架會幫你做好這些事,你只需要管理對應的配置即可。

 

2.1 Browser 的 drive 方法

        Browser 類有一個靜態的 drive 方法能使編寫 Geb 腳本更加方便。

Browser.drive {
    go "signup"
    assert $("h1").text() == "Signup Page"
}

這與下面的寫法等效:

def browser = new Browser()
browser.go "signup"
assert browser.$("h1").text() == "Signup Page"

drive 方法能夠接受 Browser 構造函數中可用的所有參數(如:空,Configuration,或者各種屬性),也可以接受一個已經打開的瀏覽器實例和閉包作爲參數。閉包將會綁定到給定的瀏覽器實例上執行,結果就是閉包內所有頂層的方法調用和屬性訪問都會到綁定的瀏覽器實例上去解析。

drive 方法總會返回其使用的瀏覽器對象,因此如果你需要在閉包執行完後關閉瀏覽器的話,可以像這樣做:

Browser.drive {
    //…
}.quit()

更多關於什麼時候以及爲什麼要手動關閉瀏覽器,可以查看後續關於 driver 的章節。

 

2.2 發起請求

2.2.1 基準 URL

        Browser 實例內維護了一個 baseUrl 屬性,該屬性用於解析所有相對的 URL。這個屬性的值可以來自於配置,也可以顯式地設置在 Browser 對象上。

        在指定基準 URL 和相對 URL 時候都要十分注意結尾或開頭的斜線(/),因爲 / 的位置對 URL 解析有很大影響。下表展示了各種不同 URL 的解析結果。

基準 URL 要訪問的相對 URL 解析後的 URL

http://myapp.com/

abc

http://myapp.com/abc

http://myapp.com

abc

http://myapp.comabc

http://myapp.com

/abc

http://myapp.com/abc

http://myapp.com/abc/

def

http://myapp.com/abc/def

http://myapp.com/abc

def

http://myapp.com/def

http://myapp.com/abc/

/def

http://myapp.com/def

http://myapp.com/abc/def/

jkl

http://myapp.com/abc/def/jkl

http://myapp.com/abc/def

jkl

http://myapp.com/abc/jkl

http://myapp.com/abc/def

/jkl

http://myapp.com/jkl

最理想的方式就是書寫基準 URL 時使用後綴的 /,書寫相對 URL 時不使用前綴的 /

 

2.2.2 使用頁面

        Page 對象中可以定義一個 url 屬性,當導航到指定的 Page 對象時就會用到這個 url 屬性。導航到 Page 對象所代表的頁面是通過 Browser 對象的 to() 或 via() 方法實現的。

class SignupPage extends Page {
    static url = "signup"
}

Browser.drive {
    to SignupPage
    assert $("h1").text() == "Signup Page"
    assert page instanceof SignupPage
}

to() 和 via() 方法將會向解析後的 URL 發起請求,並把 Browser 對象中的 page 實例設置爲他們所訪問的 Page 類型的一個實例。絕大部分的 Geb 腳本或測試都是從一個 to() 或 via() 方法調用開始的。

後續關於高級頁面導航的章節將會講解如何在 Page 對象上使用更復雜的 URL 解析方式。

 

2.2.3 直接發起請求

        你也可以直接使用 Browser 對象的 go() 方法向一個 URL 直接發起請求,而不設置或改變 Browser 的 page 對象。

import geb.Page
import geb.spock.GebSpec

class GoogleHomePage extends Page {
    static url = "http://www.google.com"
}

class GoogleSpec extends GebSpec {

    def "go 方法不設置瀏覽器的 page"() {
        given:
        Page oldPage = page

        when:
        go "http://www.google.com"

        then:
        oldPage == page
        currentUrl.contains "google"
    }

    def "to 方法會設置瀏覽器的 page 並改變當前URL(currentUrl)"() {
        given:
        Page oldPage = page

        when:
        to GoogleHomePage

        then:
        oldPage != page
        currentUrl.contains "google"
    }

}

下面這個例子中使用的基準 URL 是:http://localhost/

Browser.drive {
    go()                                                 //1

    go "signup"                                          //2

    go "signup", param1: "value1", param2: "value2"      //3
}

//1:訪問基準 URL

//2:訪問一個相對於基準 URL 的相對 URL

//3:訪問一個帶有請求參數的 URL: http://localhost/signup?param1=value1&param2=value2

 

2.3 Browser 實例中的 Page 對象

        Browser 實例中有一個對當前頁面對象的引用,保存在 page 屬性中。剛開始,所有 Browser 實例都保存了一個類型爲 Page 都 page 對象,這個 Page 類型提供了基本都導航器功能,是所有具體頁面對象的超類。

但其實我們很少直接訪問 page 屬性。Browser 對象將會把所有它不能處理的方法調用和屬性讀寫委派給其 page 對象處理。

Browser.drive {
    go "signup"

    assert $("h1").text() == "Signup Page"         //1
    assert page.$("h1").text() == "Signup Page"    //1
}

//1:這兩種調用方式是等效的,因爲瀏覽器會把它不能處理的 $("h1") 方法調用自動委派給其 page 實例去處理

 

page 對象提供了 $() 方法,Browser 對象沒有此方法。這種轉發機制使得代碼很簡潔,減少了不必要的代碼“噪聲”。

注:$() 是用來和頁面內容交互的,關於此方法的更多信息請參考後續的 Navigator API 章節

當使用頁面對象模式時,我們需要創建 Page 類型的子類,Page 使用了強大的領域特定語言(DSL) 來定義頁面內容,使得你可以使用有意義的名字來引用頁面內容,而不是直接使用普通的 tag 名字或 CSS 表達式。

class SignupPage extends Page {
    static url = "signup"

    static content = {
        heading { $("h1").text() }
    }
}

Browser.drive {
    to SignupPage
    assert heading == "Signup Page"
}

頁面對象會在後續的 Pages 章節詳談,該章中我們也會探索定義頁面內容的 DSL。

 

2.3.1 改變 Browser 的 page 對象

        前面我們以及提到過 to() 方法能夠改變 Browser 的 page 對象。除此之外,我們還可以使用 Browser 的 page() 方法來改變其 page 對象,而不需要向 to() 一樣發起一次新的請求。

<T extends Page> T page(Class<T> pageType) 方法允許我們把 Browser 實例的 page 對象設置爲給定類型 pageType 的一個新實例。這裏的類型 pageType 必須是 Page 或 Page 的子類。該方法不會驗證指定的 page 對象是否真的和瀏覽器當前的頁面內容相匹配(不會做 at 檢查)。

<T extends Page> T page(T pageInstance) 方法允許我們把 Browser 實例的 page 對象設置爲給定實例 pageInstance。和前面的方法類似,該方法也不會驗證指定的 page 對象是否真的和瀏覽器當前的頁面內容相匹配。

Page page(Class<? extends Page>[]  potentialPageTypes) 方法允許我們指定一組可能的頁面類型。每個可能的類型都會被實例化,並且會檢查每個實例化的頁面對象是否與瀏覽器當前的頁面內容匹配(通過允許每個頁面實例的 at 方法來檢查)。所有傳入的頁面類都必須定義 at 檢查器,否則將會拋出 UndefinedAtCheckerException 異常。

Page page(Page[]  potentialPageInstances) 方法允許我們指定一組可能的頁面實例。每個可能的頁面實例都會被初始化並檢查其是否與瀏覽器當前的頁面內容匹配(通過允許每個頁面實例的 at 方法來檢查)。所有傳入的頁面實例都必須定義 at 檢查器,否則將會拋出 UndefinedAtCheckerException 異常。

 

2.4 At 檢查器

        Page 中可以定義 at 檢查器,Browser 對象可以使用 at 檢查器來校驗它當前是否處於某個頁面。

class SignupPage extends Page {
    static url = "signup"

    static at = {
        $("h1").text() == "Signup Page"
    }
}

Browser.drive {
    to SignupPage
}

注意:最好不要在 at 檢查器中使用顯式的 return 語句。Geb 會對 at 檢查器中的所有檢查點進行轉換,使得檢查器中的每條語句都被加上斷言(就像 Spock 中的 then: 語句塊一樣)。多虧這個機制,它使得 at 檢查器失敗的時候,你能立刻看到檢查器求值失敗的原因。

to() 方法接受一個頁面類型或頁面實例作爲參數,並且會驗證瀏覽器最終會停留在其參數所指定的頁面。如果在發起到某個頁面的請求過程中會發生重定向,最終把瀏覽器帶到一個不同的頁面,我們就應該使用 via() 方法:

Browser.drive {
    via SecurePage
    at AccessDeniedPage
}

Brower 對象有 <T extends Page> T at(Class<T> pageType)<T extends Page> T at(T page) 兩種形式的 at 方法能夠用來測試瀏覽器當前是否在參數所指定的頁面。

上面的 at AccessDeniedPage 方法調用在 at 檢查器通過的時候將會返回一個頁面對象。而如果 at 檢查器校驗沒有通過的話,默認情況下(查看隱式斷言章節獲取更多信息),將會拋出 AssertionError 異常,即使 at 檢查器中並沒有顯式的斷言語句;如果隱式斷言被關閉的話,將會返回 null

當頁面對象改變時,爲了使不合預期的測試快速失敗,通常比較推薦的做法是使用 to() 方法(該方法會隱式的對頁面對象進行 at 檢查)或 via() 方法後面緊跟着一個 at() 方法來校驗是否在預期的頁面。如果不做 at 檢查的話,後續步驟的失敗可能比較奇怪又不好定位,因爲頁面內容和預期的不匹配,內容查找可能返回離奇的結果。

如果向 at() 方法中傳入了一個沒有定義 at 檢查器的 Page 子類或實例,將會拋出 UndefinedAtCheckerException,因爲做顯式的 at 檢查時,頁面中必須要定義 at 檢查器,這與使用 to() 方法時進行的隱式的 at 檢查不同。這種行爲是有意要提醒你,當你嘗試顯式驗證是否在某個頁面時,你本意可能也想爲這個頁面定義 at 檢查器。但隱式 at 檢查時,卻不會強制要求你這樣做。

如果 at 檢查器驗證成功,at() 也會同時更新 Browser 對象的 page 實例,這就是說,當成功驗證 Browser 處於某個頁面後,你不需要再次手動的將其 page 對象設置爲剛纔經過驗證的頁面,at() 已經幫你做了。

Page 上定義的頁面內容(content),支持聲明該內容被點擊後,瀏覽器頁面的類型應該如何改變。當這種頁面內容被點擊後,瀏覽器會自動對該內容中聲明的頁面進行 at 檢查,若果該頁面定義了 at 檢查器的話(詳見後續 DSL 章節中關於 to 參數的說明)。

class LoginPage extends Page {
    static url = "/login"

    static content = {
        loginButton(to: AdminPage) { $("input", type: "submit") }
        username { $("input", name: "username") }
        password { $("input", name: "password") }
    }
}

class AdminPage extends Page {
    static at = {
        $("h1").text() == "Admin Page"
    }
}

Browser.drive {
    to LoginPage

    username.value("admin")
    password.value("p4sw0rd")
    loginButton.click()

    assert page instanceof AdminPage
}

 

2.5 頁面變化監聽器

        當瀏覽器的 page 實例發生變化時,是可以通過 PageChangeListener 接口監聽到的。一旦監聽器被註冊,它的 pageWillChange() 方法就會被立即被調用一次,它的 newPage 參數會被設置成當前 page 對象,oldPage 參數被設置成 null。在之後每一次頁面發生改動時,oldPage 的值將會是瀏覽器當前的 page 對象,newPage 的值將是瀏覽器馬上要被設置成的那個 page 對象。

class RecordingPageChangeListener implements PageChangeListener {
    Page oldPage
    Page newPage

    @Override
    void pageWillChange(Browser browser, Page oldPage, Page newPage) {
        this.oldPage = oldPage
        this.newPage = newPage
    }
}

class FirstPage extends Page {
}

class SecondPage extends Page {
}

def listener = new RecordingPageChangeListener()
def browser = new Browser()

browser.page(FirstPage)
browser.registerPageChangeListener(listener)

assert listener.oldPage == null
assert listener.newPage instanceof FirstPage

browser.page(SecondPage)

assert listener.oldPage instanceof FirstPage
assert listener.newPage instanceof SecondPage

可以隨時移除頁面變動監聽器:

browser.removePageChangeListener(listener)

removePageChangeListener(PageChangeListener listener) 將返回 true,如果 listener 是已經註冊過的,並且現在已被移除,否則將返回 false。

監聽器不可以註冊兩次。如果嘗試註冊一個已經註冊過的監聽器(例如已經有另一個和此監聽器相等的(基於equals比較)監聽器被註冊了),將會拋出 PageChangeListenerAlreadyRegisteredException。

 

2.6 處理多個標籤和窗口

        當你的 Web 應用涉及多 Tab 或 Window 時(例如:當點擊一個設置了 target 屬性的鏈接時打開了一個新標籤頁),你可以使用 withWindow() 或 withNewWindow() 方法在其他窗口的上下文中執行操作。如果你真需要知道當前窗口的名稱或想獲得所有打開窗口的名稱,可以使用 Browser.getCurrentWindow() 和 Browser.getAvailableWindow() 方法來獲取,但是對於處理多窗口這種場景,還是推薦使用 withWindow() 和 withNewWindow() 方法。

2.6.1 處理已經打開的窗口

        如果你已經知道你想操作的窗口的名稱,就可以使用 withWindow(String windowName, Closure block) 方法在該窗口的上下文中執行操作。

假設下面這段 HTML 代碼是在 baseUrl 指定的頁面中渲染的:

<a href="http://www.gebish.org" target="myWindow">Geb</a>

下面這段代碼將能執行成功:

Browser.drive {
    go()
    $("a").click()
    withWindow("myWindow") {
        assert title == "Geb - Very Groovy Browser Automation"
    }
}

如果你不知道要操作的窗口的名稱,但是你知道一些關於該窗口頁面內容的信息,你就可以使用 withWindow(Closure specification, Closure block) 方法來操作。該方法的第一個閉包參數在你想要操作的窗口中執行後,應該要能返回 true,第二個閉包參數就是你想要在該窗口的上下文中執行的操作。如果沒有參數能使窗口定位閉包 specification 返回 true,那麼就會拋出 NoSuchWindowException。

因此,假設頁面上有如下代碼:

<a href="http://www.gebish.org" target="_blank">Geb</a>

下面這些操作將會成功:

Browser.drive {
    go()
    $("a").click()
    withWindow({ title == "Geb - Very Groovy Browser Automation" }) {
        assert $(".slogan").text().startsWith("Very Groovy browser automation.")
    }
}

注意:如果 withWindow 方法的第二個閉包參數 block,在執行過程中改變了 browser 對象的當前頁面 page 實例(例如,使用了 page(Page) or at(Page) 方法),當 withWindow() 方法返回後,browser 的 page 實例將會恢復回其原來的值(即調用 withWindow 方法前的值)

withWindow() 方法接受的選項

        在調用 withWindow() 方法時,可以傳入一些額外的選項,來簡化和這些已經打開的窗口的交互。通用的語法如下:

withWindow(«window specification», «option name»: «option value», ...) {
    «action executed within the context of the window»
}

close 選項:默認值 false

如果你向 close 選項傳入了任何 Groovy 中的 “真值”,那個在傳遞給 withWindow() 方法的最後一個閉包參數中的代碼執行完後,該窗口就會被關閉。

page 選項:默認值 null

如果你向 page 選項傳入了 Page 或其子類,或他們的實例,那麼在執行 withWindow() 方法的最後一個閉包參數中的代碼前,Geb 會先將 browser 的 page 實例設置爲通過 page 選項傳入的值,並且在代碼執行完後進行恢復。如果傳入的 page 所屬的類定義了 at 檢查器,Geb 會進對 at 檢查器進行校驗。

 

2.6.2 處理新打開的窗口

        如果你想在你執行操作過程中新打開的窗口的上下文中執行代碼,可以使用 withNewWindow(Closure windowOpeningBlock, Closure block) 方法來處理。

假設下面這段 HTML 代碼是在 baseUrl 指定的頁面中渲染的:

<a href="http://www.gebish.org" target="_blank">Geb</a>

下面這段代碼將能執行成功:

Browser.drive {
    go()
    withNewWindow({ $('a').click() }) {
        assert title == 'Geb - Very Groovy Browser Automation'
    }
}

如果第一個參數指定的閉包執行後,打開了 0 個或多個窗口,Geb 將會拋出 NoNewWindowException。

注意:如果 withNewWindow 方法的第二個閉包參數 block,在執行過程中改變了 browser 對象的當前頁面 page 實例(例如,使用了 page(Page) or at(Page) 方法),當 withNewWindow() 方法返回後,browser 的 page 實例將會恢復回其原來的值(即調用 withNewWindow 方法前的值)

withNewWindow() 方法接受的選項

在調用 withNewWindow() 方法時,可以傳入一些額外的選項,來簡化和這些新打開的窗口的交互。通用的語法如下:

withNewWindow({ «window opening action» }, «option name»: «option value», ...) {
    «action executed within the context of the window»
}

close 選項:默認值 false

如果你向 close 選項傳入了任何 Groovy 中的 “真值”,那個在傳遞給 withNewWindow() 方法的最後一個閉包參數中的代碼執行完後,該新打開的窗口就會被關閉。

page 選項:默認值 null

如果你向 page 選項傳入了 Page 或其子類,或他們的實例,那麼在執行 withNewWindow() 方法的最後一個閉包參數中的代碼前,Geb 會先將 browser 的 page 實例設置爲通過 page 選項傳入的值,並且在代碼執行完後進行恢復。

wait 選項:默認值 null

如果傳給 withNewWindow() 方法的第一用於打開新窗口的閉包中執行的打開窗口的操作是異步的,並且你需要等待新窗口打開,這時就可以通過 wait 選項來通知 Geb 等待窗口打開。wait 選項可以接受的值和後續章節中將涉及的頁面內容定義中的 wait 選項的可接受的值是一樣的。

假設下面這段 HTML 是在 baseUrl 指定的頁面中進行渲染的:

<a href="http://google.com" target="_blank" id="new-window-link">Google</a>

假設該頁面中同時還包括下面這段 javaScript 代碼:

function openNewWindow() {
    setTimeout(function() {
        document.getElementById('new-window-link').click();
    }, 200);
}

那麼下面的代碼將能執行成功:

Browser.drive {
    go()
    withNewWindow({ js.openNewWindow() }, wait: true) {
        assert title == 'Google'
    }
}

 

2.7 暫停和調試

        如果能在代碼中指定的地方暫停 Geb 的執行,這對於排查問題將很有幫助。可以使用兩種方法來實現:

  • 在 IDE 中設置斷點,然後以 debug 模式運行 JVM
  • 使用 Browser 的 pause() 方法

儘管前者允許檢查 JVM 內的變量以及在斷點處使用 Geb 的類和方法,但後者設置起來更加方便快捷,暫停後可以在瀏覽器打開開發者工具來檢查 DOM 文檔樹以及 javascript 腳本中變量,而這在大多數情況下對解決問題已經足夠了。

注:如果你想在調用 pause() 方法暫停後能夠繼續執行,將全局 javascript 變量 geb.unpause 設爲 true 即可。這通常是在瀏覽器的開發者工具的控制檯 (console) 中執行 geb.unpause = true; 來實現。如果在調用 pause() 方法及恢復執行這段時間,瀏覽器頁面重新加載過或者導航到了一個新頁面,就需要在設置 geb.unpause = true 之前重新構建頂級的 geb 對象。這通常在瀏覽器控制檯中執行一下 geb = {}; 即可。

 

2.8 本地存儲和會話存儲

        可以通過 Browser 實例的 localStorage 和 sessionStorage 屬性來分別訪問本地存儲和會話存儲。可以通過他們來讀寫底層的存儲,就像操作普通的 map 一樣。下面是讀寫本來存儲的一個例子:

Browser.drive {
    localStorage["username"] = "Alice"

    assert localStorage["username"] == "Alice"
}

loaclStorage 和 sessionStorage 屬性都是 geb.webstorage.WebStorage 類型。可以查看 javadoc 來了解除了讀寫之外,他們支持的其他操作。

注意:並不是所有的瀏覽器實現都支持訪問 web 存儲。當瀏覽器不支持訪問 web 存儲時,Geb 將拋出:geb.error.WebStorageNotSupportedException 異常。

 

2.9 退出瀏覽器

        Browser 對象有 quit() 和 close() 方法來退出瀏覽器。它們簡單的將這些方法的調用委派給底層的 WebDriver 實例。可以查看後續的 瀏覽器驅動管理 章節來獲取更多關於何時退出以及爲何需要退出瀏覽器的信息。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章