【Laravel官方導讀】Browse Testing 完整攻略

【Laravel官方導讀】Browse Testing 完整攻略

Browse Testing 完整攻略

簡介

Laravel Dusk 提供了直覺、簡單易用的瀏覽器自動化及測試 API 。預設情況下,Dusk 不需要在你的機器上安裝 JDK 或者 Selenium 。而是需要使用單獨的 ChromeDriver 進行安裝。當然,你也可以自由使用其他的兼容 Selenium 的驅動

安裝

首先,利用 Composer 在項目中添加 laravel/dusk 這個依賴套件 :

composer require --dev laravel/dusk

注意:

如果你是手動註冊 Dusk 服務供應器,一定不要在你的正式環境中註冊,這樣可能會導致一些不守規矩的用戶擁有控制你應用的權限

在安裝好 Dusk 套件後,執行 dusk:install 命令。這個命令將會建立一個 tests/Browser 資料夾以及一個 Dusk 示範測試用例:

php artisan dusk:install

接下來,在 .env 檔案中設置 APP_URL 參數。這個值應該與你在瀏覽器中打開應用的網址首頁 URL 相同

假如你是使用 Laravel Sail 來管理你本地開發環境,請別忘了參考 Sail 官方文件來設定並執行 Dsk 測試

管理 ChromeDriver 安裝

如果你想安裝與 Laravel Dusk 附帶版本不同的 ChromeDriver,可以使用 dusk:chrome-driver 命令:

安裝你所屬作業系統的最新版本 ChromeDriver

php artisan dusk:chrome-driver

安裝你所屬作業系統的指定版本 ChromeDriver

php artisan dusk:chrome-driver 86

為所有支持的作業系統安裝指定版本的 ChromeDriver...

php artisan dusk:chrome-driver --all

安裝符合你作業系統的 Chrome/Chromium 版本的 ChromeDriver

php artisan dusk:chrome-driver --detect

注意: Dusk 要求 ChromeDriver 的二進位文件 (binaries) 是可執行的。如果在 Dusk 運行時遇到問題,可以使用以下命令來調整其權限,以確保二進位文件 (binaries) 是可執行的

chmod -R 0755 vendor/laravel/dusk/bin

使用其他瀏覽器

預設情況下, Dusk 使用 Google Chrome 瀏覽器和一個單獨安裝的 ChromeDriver 來運行你的瀏覽器測試。當然,你可以運行自己的 Selenium 服務來用任何你想用的瀏覽器來進行測試

如果要這麼做,打開 tests/DuskTestCase.php 文件,這個是應用測試用例的父類別。在這個文件中,移除對 startChromeDriver() 的呼叫。這樣 Dusk 就不會自動啟動 ChromeDriver 了

//tests\DuskTestCase.php

//準備執行 Dusk 測試
public static function prepare()
{
    // static::startChromeDriver();
}

然後,你可以修改 driver() 來連接到你選定的 URL 和端口。此外,你可以修改 「desired capabilities」(期望能力),它將會被傳遞給 WebDriver

//tests\DuskTestCase.php

//創建 RemoteWebDriver 實例
protected function driver()
{
    return RemoteWebDriver::create(
        'http://localhost:4444/wd/hub', DesiredCapabilities::phantomjs()
    );
}

快速開始

創建測試

要創建一個 Dusk 測試,可以使用 dusk:make 命令。創建的測試將會被放在 tests/Browser 目錄中:

php artisan dusk:make LoginTest

資料庫遷移

大部分你寫的測試將會和從資料庫取值的頁面來進行交互;然而你的 Dusk 測試應永遠都不使用 RefreshDatabase 這個 Trait。該 Trait 利用資料庫的 Transaction 機制在 HTTP 請求時是不適用的。替代方案是使用 DatabaseMigration 這個 Trait。它將會在每一次的測試時重新執行資料庫遷移

//tests\Browser\ExampleTest.php

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Chrome;
use Tests\DuskTestCase;

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;
}

另外,在執行 Dusk 測試時也不能夠使用 SQLite in-memory 資料庫。這是因為瀏覽器執行在它自己的線程因而無法取用位在其它線程的 in-memory 資料庫

執行測試

如要運作你的瀏覽器測試,請執行 dusk 命令,如下:

php artisan dusk

如果上次運行 dusk 命令時測試失敗,則可以通過使用 dusk:fails 命令重新運行失敗的測試以節省時間

php artisan dusk:fails

dusk 命令接受任何能用於 PHPUnit 的參數。例如,讓你可以在指定 group 中運行測試

php artisan dusk --group=foo

手動開啟 ChromeDriver

預設情況下,Dusk 會嘗試自動運行 ChromeDriver。如果你在特定的系統中不能運行,可以在運行 dusk 命令前通過手動的方式來運行 ChromeDriver。 如果你選擇手動運行 ChromeDriver,你需要在你的 tests/DuskTestCase.php 文件中註解掉下面這一行:

//tests\DuckTestCase.php

//為 Dusk 測試做準備
public static function prepare()
{
    // static::startChromeDriver();
}

此外如果你的 ChromeDriver 並非運行在端口 9515,你需要修改同一個類別的 driver() ,修改成正確的端口號:

//tests\DuckTestCase.php

//創建 RemoteWebDriver 實例
protected function driver()
{
    return RemoteWebDriver::create(
        'http://localhost:9515', DesiredCapabilities::chrome()
    );
}

環境處理

為了讓 Dusk 使用自己的環境設定來運行測試,你需要在專案根目錄創建一個名為 .env.dusk.{environment} 的文件。例如,你想用 local 環境來運行 dusk 命令就需要創建一個 .env.dusk.local 文件

運行測試的時候,Dusk 會備份你的 .env 文件並且重命名你的 Dusk 環境文件為 .env。當測試結束後,它會再還原你的 .env 文件內容

創建瀏覽器

首先讓我們來寫一個測試用例,這個例子用來驗證能夠使用登入系統。生成測試用例之後,我們可以對它稍作調整並讓它可以跳轉到登入界面,輸入登入帳密之後,點擊「登入」按鈕。我們通過在測試用例中呼叫 browse() 來創建一個瀏覽器實例:

//tests\Browser\ExampleTest.php

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Chrome;
use Tests\DuskTestCase;

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

        //這個方法會在每次測試前執行
        public function setUp(): void
    {
        parent::setUp();
        $this->artisan('migrate:refresh');
    }

    public function test_basic_example()
    {
        $user = User::factory()->create([
            'email' => 'info@goblinlab.org',
        ]);

        $this->browse(function ($browser) use ($user) {
            $browser->visit('/login')
                    ->type('email', $user->email)
                    ->type('password', 'password')
                    ->press('Login')
                    ->assertPathIs('/home');
        });
    }
}

如上例所示,browse() 接收了一個 Closure 作為參數。Dusk 會自動將這個瀏覽器實例注入到回呼過程中,而且這個瀏覽器實例可以和你的應用進行交互並進行各種確認


工具箱

創建多個瀏覽器

有時候你可能需要多個瀏覽器才能正確的進行測試。例如,使用多個瀏覽器測試通過 websockets 進行通訊的在線聊天頁面。想要創建多個瀏覽器,需要在 browse() 的回呼中,用更多的參數作為名字來區分瀏覽器實例,然後傳給回呼去加入多個瀏覽器實例,看下面這個例子會比較清楚:

$this->browse(function ($first, $second) {
    $first->loginAs(User::find(1))
          ->visit('/home')
          ->waitForText('Message');

    $second->loginAs(User::find(2))
           ->visit('/home')
           ->waitForText('Message')
           ->type('message', 'Hey Taylor')
           ->press('Send');

    $first->waitForText('Hey Zack')
          ->assertSee('Goblin Lab Studio');
});

網址切換

visit() 能用來將網址切換到某個指定的 URI:

$browser->visit('/login');

如果你有為路由取名字的習慣,也能夠使用 visitRoute() 來切換到指定的命名路由:

$browser->visitRoute('login');

你也能夠回到上一頁或者是到下一頁,透過 back() 和 forward():

$browser->back();

$browser->forward();

你也能夠使用 refresh() 來重載頁面:

$browser->refresh();

改變瀏覽器窗口大小

你可以使用 resize() 去調整瀏覽器的窗口大小

$browser->resize(1920, 1080);

maximize() 可以用來將瀏覽器窗口最大化

$browser->maximize();

fitContent() 將自動調整瀏覽器的窗口大小以對應頁面的內容

$browser->fitContent();

測試失敗時,Dusk 會自動將瀏覽器窗口縮放至內容大小並拍下螢幕快照,你可以通過呼叫 disableFitOnFailure() 來關閉這一特性

$browser->disableFitOnFailure();

你可以使用 move() 將瀏覽器窗口移動到螢幕上的其他位置:

$browser->move($x = 100, $y = 100);

瀏覽器宏(Browser Macro)

如果你想定義一個可以在各種測試中重複使用的自定義瀏覽器方法,可以呼叫 Browser 類別的 macro() 。通常情況下,你會從服務提供者的 boot() 中去呼叫它:

//app\Providers\DuskServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Dusk\Browser;

class DuskServiceProvider extends ServiceProvider
{
    //註冊Dusk的瀏覽器宏
    public function boot()
    {
        Browser::macro('scrollToElement', function ($element = null) {
            $this->script("$('html, body').animate({ scrollTop: $('$element').offset().top }, 0);");

            return $this;
        });
    }
}

macro() 接受一個方法名作為其第一個參數,並接受 Closure 作為其第二個參數。當對著瀏覽器實例呼叫與宏同名的方法時,將會執行宏的Closure

下面這個例子所呼叫的 scrollToElement() 就是我們自定義的瀏覽器宏

$this->browse(function ($browser) use ($user) {
    $browser->visit('/pay')
            ->scrollToElement('#credit-card-details')
            ->assertSee('Enter Credit Card Details');
});

用戶認證

我們經常會測試需要身份驗證過後才能進入的頁面,你可以使用 Dusk 的 loginAs() 來避免在每次測試期間與登錄頁面進行交互。 loginAs() 接受用戶 ID 或者是用戶模型實例:

use App\Models\User;

$this->browse(function ($browser) {
    $browser->loginAs(User::find(1))
          ->visit('/home');
});

注意:

使用 loginAs() 後,用戶會話在該文件中的所有測試被維護,也就不需再次進行登入

Cookies

你可以使用 cookie() 來獲取或者設置加密過的 cookie 的值。預設情況下,所有被 Laravel 所建立的 cookie 都會被加密

//獲取加密 cookie
$browser->cookie('name');

//設置加密 cookie
$browser->cookie('name', 'Zack');

使用 plainCookie() 則可以獲取或者設置未加密過的 cookie 的值


//獲取未加密 cookie
$browser->plainCookie('name');

//設置未加密 cookie
$browser->plainCookie('name', 'Zack');

你可以使用 deleteCookie() 刪除指定的 cookie

$browser->deleteCookie('name');

執行 JavaScript

你能夠使用 script() 以便於在瀏覽器內去執行 JavaScript 腳本


//單行腳本
$output = $browser->script('document.documentElement.scrollTop = 0');

//多行腳本
$output = $browser->script([
    'document.body.scrollTop = 0',
    'document.documentElement.scrollTop = 0',
]);

獲取截圖

你可以使用 screenshot() 來截圖並將其以指定文件名來進行儲存,所有截圖都將存放在 tests/Browser/screenshots 目錄之下

$browser->screenshot('filename');

將控制台輸出結果保存到硬碟內

你可以使用 storeConsoleLog() 將控制台內容輸出到指定文件名的檔案並寫入硬碟內,控制台輸出預設存放在 tests/Browser/console 資料夾內

$browser->storeConsoleLog('filename');

將頁面的源碼保存到硬碟內

你可以使用 storeSource() 將頁面當前源代碼輸出到指定文件名的檔案並寫入硬碟內,頁面源代碼預設會存放到 tests/Browser/source 資料夾內

$browser->storeSource('filename');

與元素交互

Dusk 選擇器

編寫 Dusk 測試最困難的部分之一就是選擇良好的 CSS 選擇器以便與元素進行交互。 隨着時間的推移,前端框架的更改可能會導致如下所示的 CSS 選擇器無法通過測試:

// HTML結構...

<button>Login</button>

// 測試用例...

$browser->click('.login-page .container div > button');

為解決這個問題,Dusk 選擇器可以讓你專注於編寫有效的測試,而不必記住 CSS 選擇器。要定義一個選擇器,你需要添加一個 dusk 屬性在 HTML 元素中。然後在選擇器前面加上 @ 用來在 Dusk 測試中操作該元素,請看上面這個例子的進化版本

// HTML結構...

<button dusk="login-button">Login</button>

// 測試用例...

$browser->click('@login-button');

文本(Text)、值(Value) & 屬性(Attributes)

獲取 & 設置值

Dusk 提供了多個方法用於和頁面元素的當前顯示文本、值和屬性進行交互,例如,要獲取對應指定選擇器的元素的「值」可以使用 value()


// 獲取值...
$value = $browser->value('selector');

// 設置值...
$browser->value('selector', 'value');

// 獲取輸入元素的值...
$value = $browser->inputValue('field');
獲取文本

text() 可以用於獲取對應指定選擇器元素的文本

$text = $browser->text('selector');
獲取屬性

最後,attribute() 可以用於獲取對應指定選擇器元素的屬性

$attribute = $browser->attribute('selector', 'value');

使用表單

輸入值

Dusk 提供了多種方法來與表單和輸入元素進行交互。首先,讓我們看一個在輸入項中輸入值的範例:

$browser->type('email', 'info@goblinlab.org');

注意

儘管該方法在需要時可以傳入,但其實我們不需要將 CSS 選擇器傳遞給 type() 。如果沒有提供 CSS 選擇器的話,Dusk 會搜索包含指定 name 屬性的輸入項,最後 Dusk 還會嘗試尋找包含指定 name 屬性的 textarea輸入項

要想將文本附加到一個輸入項之後而不清除其內容, 你可以使用 append() :

$browser->type('tags', 'foo')
        ->append('tags', ', bar, baz');

你可以使用 clear() 來清除輸入值

$browser->clear('email');

你可以使用 typeSlowly() 去指示 Dusk 緩慢的輸入。預設情況下,Dusk 在兩次按鍵之間將暫停100毫秒。要自定義按鍵之間的時間量,你可以將適當的毫秒數作為方法的第二個參數傳入

$browser->typeSlowly('tel', '(02)82752408');

$browser->typeSlowly('tel', '(02)82752408', 300);

你還可以使用 appendSlowly() 來緩慢添加文本

$browser->type('tags', 'foo')
        ->appendSlowly('tags', ', bar, baz');

下拉選單

需要在下拉選單中選擇值,你可以使用 select() 。 類似於 type() , select() 並不是一定要傳入 CSS 選擇器。 當使用 select() 時,你應該傳遞選項實際的值而不是它的顯示文字:

$browser->select('size', 'Large');

你也可以通過省略第二個參數來隨機選擇一個選項

$browser->select('size');

複選框

使用「check」 複選框時,你可以使用 check() 。 像其他許多與輸入項相關的方法,並不是必須傳入 CSS 選擇器。 如果準確的選擇器無法找到的時候,Dusk 會搜索能夠與 name 屬性對應的複選框:

$browser->check('terms');

uncheck()用來取消複選框的勾選

$browser->uncheck('terms');

單選按鈕

使用 「select」中單選按鈕選項時,你可以使用 radio() 。 像很多其他的與輸入項相關的方法一樣, 它也並不是必須傳入 CSS 選擇器。如果準確的選擇器無法被找到的時候, Dusk 會搜索能夠與 name 屬性或者 value 屬性相對應的單選按鈕

$browser->radio('size', 'large');

附件

attach() 可以附加一個文件到 file input 元素中。 像很多其他的與輸入項相關的方法一樣,他也並不是必須傳入 CSS 選擇器。如果準確的選擇器沒有被找到的時候, Dusk 會搜索與 name 屬性對應的檔案輸入框

$browser->attach('photo', DIR.'/photos/mountains.png');

注意:

attach() 需要使用 PHP Zip 擴展,因此你的伺服器必須安裝此擴展

按下按鈕

press() 能夠用來點擊頁面上的按鈕元素。方法的第一參數可以是按鈕上的文字,也可以是 CSS 或 Dusk 選擇器

$browser->press('Login');

當提交表單後,大多數應用會在表單的提交按鈕被按下後失效,直到表單的提交請求結束後才會重新啟用。要按下按鈕並等待該按鈕重新啟用,可以使用 pressAndWaitFor()

//按下按鈕並等待按鈕重新啟用,最多等待5秒
$browser->pressAndWaitFor('Save');

//按下按鈕並等待按鈕重新啟用,最多等待1秒
$browser->pressAndWaitFor('Save', 1);

點擊超連結

要點擊超連結,可以在瀏覽器實例下使用 clickLink() 。 clickLink() 將點擊指定文字的超連結:

$browser->clickLink($linkText);

你可以使用 seeLink() 來確定具有指定顯示文字的超連結在頁面上是否可見:

if ($browser->seeLink($linkText)) {
    // ...
}

注意:

這些方法與 jQuery 交互。 如果頁面上沒有 jQuery , Dusk 會自動將其注入到頁面中,以便在測試期間使用

使用鍵盤

keys() 讓你可以在指定元素中輸入比 type() 更加複雜的輸入序列。例如,你可以在輸入值的同時按下按鍵。在這個例子中,輸入 goblin 時, shift 鍵也同時被按下。當 goblin 輸入完之後, 將會輸入 lab 而不會按下任何組合按鍵:

$browser->keys('selector', ['{shift}', 'goblin'], 'lab');

另一個有用的 keys() 用法,是寄送快捷鍵組合給你應用的主要 CSS 選擇器

$browser->keys('.app', ['{command}', 'j']);

所有包在 {} 中的鍵盤按鍵,都對應定義於 Facebook\WebDriver\WebDriverKeys 類別中,你可以在這裏找到。

使用滑鼠

點擊元素

click() 方法可用於「點擊」與給定選擇器對應的元素:

$browser->click('.selector');

clickAtXPath() 方法可用於「單擊」與指定 XPath 表達式對應的元素

$browser->clickAtXPath('//div[@class = "selector"]');

clickAtPoint() 可用於「點擊」相對於瀏覽器可視區域的指定坐標對上的最高元素

$browser->clickAtPoint($x = 0, $y = 0);

doubleClick() 可用於模擬滑鼠的雙擊

$browser->doubleClick();

rightClick() 可用於模擬滑鼠的右擊:

$browser->rightClick();
$browser->rightClick('.selector');

clickAndHold() 可用於模擬被單擊並按住的滑鼠按鈕。 隨後呼叫 releaseMouse() 將取消此行為並放開滑鼠按鈕

$browser->clickAndHold()
        ->pause(1000)
        ->releaseMouse();
滑鼠懸停

mouseover() 可用於與指定選擇器對應的元素的滑鼠懸停動作:

$browser->mouseover('.selector');
拖拉操作

drag() 用於將與指定選擇器對應的元素拖到其它元素:

$browser->drag('.from-selector', '.to-selector');

或者,你也可以在單一方向上拖動元素:

$browser->dragLeft('.selector', $pixels = 10);
$browser->dragRight('.selector', $pixels = 10);
$browser->dragUp('.selector', $pixels = 10);
$browser->dragDown('.selector', $pixels = 10);

最後,你可以將元素拖動指定的偏移量

$browser->dragOffset('.selector', $x = 10, $y = 10);

JavaScript 對話框

Dusk 提供了幾種與 JavaScript 對話框交互的方法。比如,你能夠使用 waitForDialog() 去等待 JavaScript 對話框出現,這個方法接受一個可選參數來設定要等待幾秒鐘來等對話框出現

// 等待對話框顯示:
$browser->waitForDialog($seconds = null);
// assertDialogOpened() 用來確認對話框已經顯示,並且上面顯示的訊息與指定值相同
$browser->assertDialogOpened('Dialog message');

假如 JavaScript 對話框包含輸入項,你能夠使用 typeInDialog() 來輸入內容:

$browser->typeInDialog('Hello World');

為了要透過按下 "OK" 按鈕來關閉一個打開的 JavaScript 對話框,你能夠呼叫 acceptDialog()

$browser->acceptDialog();

為了要透過按下 "Cancel" 按鈕來關閉一個打開的 JavaScript 對話框,你能夠呼叫 dismissDialog()

$browser->dismissDialog();

選擇器作用範圍

有時可能希望在指定的選擇器範圍內執行多個操作。比如,可能想要確認表格中存在某些文字,然後點擊表格中的一個按鈕。可以使用 with() 實現此需求。回呼函數內所有被執行的操作都被限定在原始的選擇器上:

$browser->with('.table', function ($table) {
    $table->assertSee('Hello World')
          ->clickLink('Delete');
});

你可能偶爾需要在當前範圍之外執行確認。 你可以使用 elsewhere() 來完成此操作:

 $browser->with('.table', function ($table) {
    // Current scope is `body .table`...

    $browser->elsewhere('.page-title', function ($title) {
        // Current scope is `body .page-title`...
        $title->assertSee('Hello World');
    });

    $browser->elsewhereWhenAvailable('.page-title', function ($title) {
        // Current scope is `body .page-title`...
        $title->assertSee('Hello World');
    });
 });

等待元素

在測試大量使用 JavaScript 的應用時,在進行測試之前,經常需要「等待」指定元素或資料可用。Dusk 讓這件事情變得更容易。使用一系列方法,可以等到頁面元素可用,甚至指定的 JavaScript 表達式執行結果為 true

等待

如果需要測試暫停指定的毫秒數,可以使用 pause() :

等待選擇器

waitFor() 可以用於暫停執行測試,直到頁面上與給定 CSS 選擇器對應的元素被顯示。預設情況下,將在暫停超過 5 秒後拋出異常。如果有必要,可以傳入自定義超時時長作為其第二個參數:

// 等待選擇器 5 秒時間...
$browser->waitFor('.selector');

// 等待選擇器 1 秒時間...
$browser->waitFor('.selector', 1);

你也能夠等到直至對應選擇器包含指定的文字

// 等待選擇器最多 5 秒的時間,直到選擇器對應元素包含指定文字...
$browser->waitForTextIn('.selector', 'Hello World');

// 等待選擇器最多 1 秒的時間,直到選擇器對應元素包含指定文字...
$browser->waitForTextIn('.selector', 'Hello World', 1);

// 等待選擇器直到選擇器對應元素消失...
$browser->waitUntilMissing('.selector');

// 等待選擇器最多 1 秒的時間,直到選擇器對應元素消失...
$browser->waitUntilMissing('.selector', 1);

選擇器可用時,限定作用域範圍

有時,你或許希望等待指定選擇器,然後與對應選擇器的元素進行交互。例如,你可能希望等到 "modal" 視窗可用,然後在 "modal" 視窗中點擊「確定」按鈕。在這種情況下,可以使用 whenAvailable() 。指定回呼內的所有要執行的元素操作都將被限定在起始選擇器上:

$browser->whenAvailable('.modal', function ($modal) {
    $modal->assertSee('Hello World')
          ->press('OK');
});
等待文字

waitForText() 可以用於等待頁面上指定文字被顯示在頁面上

//等待頁面上出現指定文字最多 5 秒時間...
$browser->waitForText('Hello World');

//等待頁面上出現指定文字最多 1 秒時間...
$browser->waitForText('Hello World', 1);

你也能夠使用 waitUntilMissingText() 去等待直到指定文字消失於頁面上

//等待頁面上指定文字消失,最多 5 秒時間...
$browser->waitUntilMissingText('Hello World');

//等待頁面上指定文字消失,最多 1 秒時間...
$browser->waitUntilMissingText('Hello World', 1);
等待超連結

waitForLink() 用於等待指定超連結文字在頁面上顯示

//等待頁面上出現指定文字的超連結最多 5 秒時間... $browser->waitForLink('Create');

//等待頁面上出現指定文字的超連結最多 1 秒時間... $browser->waitForLink('Create', 1);

等待頁面跳轉

在使用類似 $browser->assertPathIs('/home') 路徑確認時,如果 window.location.pathname 被異步更新,確認就會失敗。可以使用 waitForLocation() 等待頁面跳轉到指定路徑

$browser->waitForLocation('/secret');

還可以等待被命名的路由跳轉:

$browser->waitForRoute($routeName, $parameters);
等待頁面重新加載

如果要在頁面重新加載後確認,可以使用 waitForReload() :

$browser->click('.some-action')
        ->waitForReload()
        ->assertSee('something');
等待 JavaScript 表達式

有時會希望暫停執行測試,直到指定的 JavaScript 表達式執行結果為 true。可以使用 waitUntil() 輕易地達成此目的。傳送一個表達式給此方法,不需要包含 return 關鍵字或者結束分號

// 等待表達式為 true 最多 5 秒時間...
$browser->waitUntil('App.data.servers.length > 0');

// 等待表達式為 true 最多 1 秒時間...
$browser->waitUntil('App.data.servers.length > 0', 1);
等待 Vue 表達式

下述方法可用於一直等待,直到一個指定的 Vue 組件屬性有特定的值:

// 一直等待,直到組件屬性有特定的值 $browser->waitUntilVue('user.name', 'goblin', '@user');

// 一直等待,直到組件不包含給定的值 $browser->waitUntilVueIsNot('user.name', null, '@user');

等待回呼函數

Dusk 中的許多 「wait」 方法都依賴於底層方法 waitUsing()。你可以直接用這個方法去等待一個回呼函數返回 true。waitUsing() 方法接收一個最大的等待秒數,Closure 執行的內部時間,Closure,以及一個可選的失敗訊息

$browser->waitUsing(10, 1, function () use ($something) {
    return $something->isReady();
}, "Something wasn't ready in time.");

滾動到視圖中

有時您可能無法點擊某個元素,因為該元素在瀏覽器的可見區域之外。 scrollIntoView() 可以將元素滾動到瀏覽器可視窗口內:

$browser->scrollIntoView('.selector')
        ->click('.selector');

滾動到視圖中 有時您可能無法單擊某個元素,因為該元素在瀏覽器的可見區域之外。 scrollIntoView 方法可以將元素滾動到瀏覽器可視窗口內:

$browser->scrollIntoView('selector') ->click('selector');

創建 Vue 斷言

Dusk 甚至還允許你對 Vue 組件資料的狀態進行確認。假設你的應用有如下的 Vue 組件:

// HTML結構...

<profile dusk="profile-component"></profile>

//組件的定義

Vue.component('profile', {
    template: '<div>{{ user.name }}</div>',

    data: function () {
        return {
            user: {
                name: 'Taylor'
            }
        };
    }
});

你可以像這樣對 Vue 組件狀態進行斷言:

//Vue 基礎測試案例
public function testVue()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->assertVue('user.name', 'Taylor', '@profile-component');
    });
}

可用的斷言

Dusk 提供了各種你可以對應使用的確認方法。所有可用的確認列表如下:

assertTitle()

確認頁面的標題(title)為指定值

$browser->assertTitle($title);

assertTitleContains()

確認頁面標題(title)包含指定值

$browser->assertTitleContains($title);

assertUrlIs()

確認當前的 URL(不包含 query 字串)滿足指定的字串:

$browser->assertUrlIs($url);

assertSchemeIs()

確認當前的 URL scheme 滿足指定的 scheme:

$browser->assertSchemeIs($scheme);

assertSchemeIsNot()

確認當前的 URL scheme 不滿足指定的 scheme:

$browser->assertSchemeIsNot($scheme);

assertHostIs()

確認當前的 URL host 滿足指定的 host:

$browser->assertHostIs($host);

assertHostIsNot()

確認當前的 URL host 不滿足指定的 host:

$browser->assertHostIsNot($host);

assertPortIs()

確認當前的 URL 端口滿足指定的端口:

$browser->assertPortIs($port);

assertPortIsNot()

確認當前的 URL 端口不滿足指定的端口:

$browser->assertPortIsNot($port);

assertPathBeginsWith()

確認當前的 URL 以指定的字串開頭:

$browser->assertPathBeginsWith('/home');

assertPathIs()

確認當前的路徑滿足指定的路徑:

$browser->assertPathIs('/home');

assertPathIsNot()

確認當前的路徑不滿足指定的路徑:

$browser->assertPathIsNot('/home');

assertRouteIs()

確認指定的 URL 不滿足指定的命名路由的 URL:

$browser->assertRouteIs($name, $parameters);

assertQueryStringHas()

確認指定的 query 參數存在:

$browser->assertQueryStringHas($name);

確認指定的 query 參數存在,並且是指定的值:

$browser->assertQueryStringHas($name, $value);

assertQueryStringMissing()

確認指定的 query 參數不存在:

$browser->assertQueryStringMissing($name);

assertFragmentIs()

確認當前的 fragment 滿足指定的 fragment:

$browser->assertFragmentIs('anchor');

assertFragmentBeginsWith()

確認當前的 fragment 以指定的 fragment 開頭:

$browser->assertFragmentBeginsWith('anchor');

assertFragmentIsNot()

確認當前的 fragment 不滿足指定的 fragment:

$browser->assertFragmentIsNot('anchor');

assertHasCookie()

確認指定的 cookie 存在:

$browser->assertHasCookie($name);

assertHasPlainCookie()

確認存在指定的未加密 cookie:

$browser->assertHasPlainCookie($name);

assertCookieMissing()

確認指定的 cookie 不存在:

$browser->assertCookieMissing($name);

assertPlainCookieMissing()

確認指定的未加密 cookie不存在:

$browser->assertPlainCookieMissing($name);

assertCookieValue()

確認加密的 cookie 具有指定值:

$browser->assertCookieValue($name, $value);

assertPlainCookieValue()

確認未加密的 cookie 具有指定值:

$browser->assertPlainCookieValue($name, $value);

assertSee()

確認在頁面中有指定的文本:

$browser->assertSee($text);

assertDontSee()

確認在頁面中沒有指定的文字:

$browser->assertDontSee($text);

assertSeeIn()

確認在選擇器中有指定的文字:

$browser->assertSeeIn($selector, $text);

assertDontSeeIn()

確認指定的字串在選擇器中不存在:

$browser->assertDontSeeIn($selector, $text);

assertSeeAnythingIn()

確認在選擇器中存在任何字串:

$browser->assertSeeAnythingIn($selector);

assertSeeNothingIn()

確認在選擇器中不存在任何字串:

$browser->assertSeeNothingIn($selector);

assertScript()

確認指定的 JavaScript 表達式的值為指定的值:

$browser->assertScript('window.isLoaded')
        ->assertScript('document.readyState', 'complete');

assertSourceHas()

確認在頁面中存在給定的源代碼:

$browser->assertSourceHas($code);

assertSourceMissing()

確認頁面中沒有給定的源代碼:

$browser->assertSourceMissing($code);

assertSeeLink()

確認在頁面中存在指定的超連結:

$browser->assertSeeLink($linkText);

assertDontSeeLink()

確認頁面中沒有指定的超連結:

$browser->assertDontSeeLink($linkText);

assertInputValue()

確認輸入框(input)有指定的值:

$browser->assertInputValue($field, $value);

assertInputValueIsNot()

確認輸入框沒有指定的值:

$browser->assertInputValueIsNot($field, $value);

assertChecked()

確認複選框(checkbox)有被選中:

$browser->assertChecked($field);

assertNotChecked()

確認複選框(checkbox)沒有被選中:

$browser->assertNotChecked($field);

assertRadioSelected()

確認單選框(radio)被選中:

$browser->assertRadioSelected($field, $value);

assertRadioNotSelected()

確認單選框(radio)沒有被選中:

$browser->assertRadioNotSelected($field, $value);

assertSelected()

確認下拉框有指定的值:

$browser->assertSelected($field, $value);

assertNotSelected()

確認下拉框沒有指定的值:

$browser->assertNotSelected($field, $value);

assertSelectHasOptions()

確認指定的陣列值是可選項:

$browser->assertSelectHasOptions($field, $values);

assertSelectMissingOptions()

確認給定的陣列值是不可選的:

$browser->assertSelectMissingOptions($field, $values);

assertSelectHasOption()

確認指定的值在指定的輸入項是可供選擇的:

$browser->assertSelectHasOption($field, $value);

assertSelectMissingOption()

確認指定的值在指定的輸入項是無法選擇的:

$browser->assertSelectMissingOption($field, $value);

assertValue()

確認選擇器範圍內的元素存在指定的值:

$browser->assertValue($selector, $value);

assertAttribute()

確認與指定選擇器對應的元素在提供的屬性中具有指定的值:

$browser->assertAttribute($selector, $attribute, $value);

assertAriaAttribute()

確認與指定選擇器對應的元素在指定的 aria 屬性中具有指定的值:

$browser->assertAriaAttribute($selector, $attribute, $value);

例如,指定標記 ,你可以像這樣聲明 aria-label 屬性:

$browser->assertAriaAttribute('button', 'label', 'Add')

assertDataAttribute()

確認與指定選擇器對應的元素在提供的 data 屬性中具有指定的值:

$browser->assertDataAttribute($selector, $attribute, $value);

例如,指定標記 , 您可以像這樣聲明 data-label 屬性:

$browser->assertDataAttribute('#row-1', 'content', 'attendees')

assertVisible()

確認選擇器範圍內的元素為可見:

$browser->assertVisible($selector);

assertPresent()

確認選擇器範圍內的元素是存在的:

$browser->assertPresent($selector);

assertNotPresent()

確認選擇器範圍內的元素在源代碼是不存在的:

$browser->assertNotPresent($selector);

assertMissing()

確認選擇器範圍內的元素不存在:

$browser->assertMissing($selector);

assertDialogOpened()

確認含有指定訊息的 JavaScript 對話框已經打開:

$browser->assertDialogOpened($message);

assertEnabled()

確認指定的欄位是啟用的:

$browser->assertEnabled($field);

assertDisabled()

確認指定的欄位是停用的:

$browser->assertDisabled($field);

assertButtonEnabled()

確認指定的按鈕是啟用的:

$browser->assertButtonEnabled($button);

assertButtonDisabled()

確認指定的按鈕是關閉的:

$browser->assertButtonDisabled($button);

assertFocused()

確認焦點在於指定的欄位:

$browser->assertFocused($field);

assertNotFocused()

確認焦點不在指定的欄位:

$browser->assertNotFocused($field);

assertAuthenticated()

確認用戶已經授權,即已經登入

$browser->assertAuthenticated();

assertGuest()

確認用戶未授權,即尚未登入

$browser->assertGuest();

assertAuthenticatedAs()

確認該用戶已通過身份驗證為指定用戶:

$browser->assertAuthenticatedAs($user);

assertVue()

確認 Vue 組件資料的屬性對應指定的值。想像一下,你的應用包含以下 Vue 組件:

// HTML結構...

<profile dusk="profile-component"></profile>

// 定義組件...

Vue.component('profile', {
    template: '<div>{{ user.name }}</div>',

    data: function () {
        return {
            user: {
                name: 'Taylor'
            }
        };
    }
});

你能夠確認像這樣去確認 Vue 組件的狀態

//基本Vue測試
public function testVue()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->assertVue('user.name', 'Taylor', '@profile-component');
    });
}

assertVueIsNot()

確認 Vue 組件資料的屬性不對應指定的值:

$browser->assertVueIsNot($property, $value, $componentSelector = null);

assertVueContains()

確認 Vue 組件資料的屬性是一個陣列,並且該陣列包含指定的值:

$browser->assertVueContains($property, $value, $componentSelector = null);

assertVueDoesNotContain()

確認 Vue 組件資料的屬性是一個陣列,並且該陣列不包含指定的值:

$browser->assertVueDoesNotContain($property, $value, $componentSelector = null);

頁面

有時候,需要測試一系列複雜的動作,這會使得測試程式碼難以閱讀和理解。通過頁面(Dusk Page)可以定義出語義化的動作,然後在指定頁面中可以使用單個方法。頁面還可以定義應用或單個頁面通用選擇器的快捷方式

生成頁面

dusk:page 這個 Artisan 命令可以生成頁面物件,所有的頁面物件都位於 tests/Browser/Pages 資料夾內:

php artisan dusk:page Login

配置頁面

頁面預設擁有 3 個方法: url(), assert() 和 elements()。 在這裡我們先詳述 url() 和 assert() , elements() 將會在之後詳加說明

url()

url() 應該返回表示頁面 URL 的路徑。 Dusk 將會在瀏覽器中使用這個 URL 來導航到具體頁面:

//取得頁面網址
public function url()
{
    return '/login';
}
assert()

assert() 可以作出任何確認來驗證瀏覽器是否在指定頁面上。這個方法並不是必須的,你可以根據你自己的需求來做出這些確認。這些確認會在你導航到這個頁面的時候自動執行:

//確認瀏覽器當前處於指定頁面
public function assert(Browser $browser)
{
    $browser->assertPathIs($this->url());
}

導航至頁面

一旦頁面配置好之後,你可以使用 visit() 導航至頁面:

use Tests\Browser\Pages\Login;

$browser->visit(new Login);

有時您可能已經在指定的頁面上,需要將頁面的選擇器和方法「加載」到當前的測試上下文中。 這在通過按鈕轉址到指定頁面而沒有明確導航到該頁面時很常見。 在這種情況下,您可以使用 on() 去加載頁面:

use Tests\Browser\Pages\CreatePlaylist;

$browser->visit('/dashboard')
        ->clickLink('Create Playlist')
        ->on(new CreatePlaylist)
        ->assertSee('@create');

選擇器簡寫

elements() 允許你為頁面中的任何 CSS 選擇器定義簡單易記的簡寫。例如,讓我們為應用登入頁中的 email 輸入框定義一個快速且容易記憶的簡寫。例如,讓我們來為應用的登入頁去定義 "email" 輸入項的簡寫:

//獲取頁面的元素簡寫
public function elements()
{
    return [
        '@email' => 'input[name=email]',
    ];
}

現在你可以用這個簡寫來代替之前在頁面中使用的完整 CSS 選擇器:

$browser->type('@email', 'taylor@laravel.com');

全局的選擇器簡寫

安裝 Dusk 之後,Page 父類別存放在你的 tests/Browser/Pages 資料夾。該類別中包含一個 siteElements() ,這個方法可以用來定義全局的選擇器簡寫,這樣在你應用中每個頁面都可以使用這些全局選擇器簡寫了:

//獲取站點全域的選擇器簡寫 public static function siteElements() { return [ '@element' => '#selector', ]; }

頁面方法

除了頁面中已經定義的預設方法之外,你還可以定義在整個測試過程中會使用到的其他方法。例如,假設我們正在開發一個音樂管理應用,在應用中都可能需要一個公共的方法來創建列表,而不是在每一頁、每一個測試類別中都重寫一遍創建播放列表的邏輯,這時候你可以在你的頁面類別中定義一個 createPlaylist():

namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class Dashboard extends Page
{
    // Other page methods...

    /**
     * 創建一個新的播放列表.
     *
     * @param  \Laravel\Dusk\Browser  $browser
     * @param  string  $name
     * @return void
     */
    public function createPlaylist(Browser $browser, $name)
    {
        $browser->type('name', $name)
                ->check('share')
                ->press('Create Playlist');
    }
}

一旦方法被定義之後,你可以在任何使用到該頁的測試中使用這個方法了。瀏覽器實例會自動作為第一參數傳遞到該頁面方法:

use Tests\Browser\Pages\Dashboard;

$browser->visit(new Dashboard)
        ->createPlaylist('My Playlist')
        ->assertSee('My Playlist');

組件

組件類似於 Dusk 的 「頁面物件」,不過它更多的是貫穿整個應用中頻繁重用的 UI 和功能片段,比如說導航Bar或訊息通知彈窗。因此,組件並不會綁定於某個明確的 URL

生成組件

為了生成一個組件,使用 Artisan 命令 dusk:component 即可生成組件。新生成的組件位於 test/Browser/Components 目錄中:

php artisan dusk:component DatePicker

如上所示,這是生成一個 「日期選擇器」(date picker) 組件的範例,這個組件可能會貫穿使用在你應用的許多頁面中。在整個測試套件的大量測試頁面中,手動編寫日期選擇的瀏覽器自動化邏輯會非常麻煩。 更方便的替代辦法是,定義一個表示日期選擇器的 Dusk 組件,然後把自動化邏輯封裝在該組件內:

namespace Tests\Browser\Components;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;

class DatePicker extends BaseComponent
{
    /**
     * 獲取組件的根選擇器
     *
     * @return string
     */
    public function selector()
    {
        return '.date-picker';
    }

    /**
     * 確認瀏覽器頁面包含該組件
     *
     * @param  Browser  $browser
     * @return void
     */
    public function assert(Browser $browser)
    {
        $browser->assertVisible($this->selector());
    }

    /**
     * 讀取組件的元素捷徑方式
     *
     * @return array
     */
    public function elements()
    {
        return [
            '@date-field' => 'input.datepicker-input',
            '@year-list' => 'div > div.datepicker-years',
            '@month-list' => 'div > div.datepicker-months',
            '@day-list' => 'div > div.datepicker-days',
        ];
    }

    /**
     * 選擇指定日期
     *
     * @param  \Laravel\Dusk\Browser  $browser
     * @param  int  $year
     * @param  int  $month
     * @param  int  $day
     * @return void
     */
    public function selectDate(Browser $browser, $year, $month, $day)
    {
        $browser->click('@date-field')
                ->within('@year-list', function ($browser) use ($year) {
                    $browser->click($year);
                })
                ->within('@month-list', function ($browser) use ($month) {
                    $browser->click($month);
                })
                ->within('@day-list', function ($browser) use ($day) {
                    $browser->click($day);
                });
    }
}

使用組件

組件定義一旦完成,在任何測試頁面的日期選擇器中選定一個日期就很輕鬆了。並且如果需要修改選定日期的邏輯,僅修改該組件即可:

//tests\Browser\ExampleTest.php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\Browser\Components\DatePicker;
use Tests\DuskTestCase;

class ExampleTest extends DuskTestCase
{
    /**
     * A basic component test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
                    ->within(new DatePicker, function ($browser) {
                        $browser->selectDate(2019, 1, 30);
                    })
                    ->assertSee('January');
        });
    }
}

持續集成

大部分 Dusk 持續集成設定會預期你的 Laravel 應用使用內建的 PHP 開發伺服器,port 號為8000。因此,在添加持續集成配置文件之前,請確保你的 .env.testing 文件中 APP_URL 配置項的值是 http://127.0.0.1:8000

Heroku CI

如果要在 Heroku CI 執行 Dusk 測試,加入以下 Google Chrome buildpack 和腳本到你的 Heroku app.json 檔案:

{
  "environments": {
    "test": {
      "buildpacks": [
        { "url": "heroku/php" },
        { "url": "https://github.com/heroku/heroku-buildpack-google-chrome" }
      ],
      "scripts": {
        "test-setup": "cp .env.testing .env",
        "test": "nohup bash -c './vendor/laravel/dusk/bin/chromedriver-linux > /dev/null 2>&1 &' && nohup bash -c 'php artisan serve --no-reload > /dev/null 2>&1 &' && php artisan dusk"
      }
    }
  }
}

Travis CI

如果要在 Travis CI 執行 Dusk 測試,使用以下 .travis.yml 配置檔。因為 Travis CI 不是一個視覺化環境,我們將需要做一些額外的步驟來啟動 Chrome 瀏覽器。除此之外,我們將使用 php artisan serve 來啟動 PHP 的內建 Web Server:

language: php

php:
  - 7.3

addons:
  chrome: stable

install:
  - cp .env.testing .env
  - travis_retry composer install --no-interaction --prefer-dist --no-suggest
  - php artisan key:generate
  - php artisan dusk:chrome-driver

before_script:
  - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
  - php artisan serve --no-reload &

script:
  - php artisan dusk
GitHub Actions
If you are using Github Actions to run your Dusk tests, you may use the following configuration file as a starting point. Like TravisCI, we will use the php artisan serve command to launch PHP's built-in web server:

name: CI
on: [push]
jobs:

  dusk-php:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Prepare The Environment
        run: cp .env.example .env
      - name: Create Database
        run: |
          sudo systemctl start mysql
          mysql --user="root" --password="root" -e "CREATE DATABASE 'my-database' character set UTF8mb4 collate utf8mb4_bin;"
      - name: Install Composer Dependencies
        run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader
      - name: Generate Application Key
        run: php artisan key:generate
      - name: Upgrade Chrome Driver
        run: php artisan dusk:chrome-driver `/opt/google/chrome/chrome --version | cut -d " " -f3 | cut -d "." -f1`
      - name: Start Chrome Driver
        run: ./vendor/laravel/dusk/bin/chromedriver-linux &
      - name: Run Laravel Server
        run: php artisan serve --no-reload &
      - name: Run Dusk Tests
        env:
          APP_URL: "http://127.0.0.1:8000"
        run: php artisan dusk
      - name: Upload Screenshots
        if: failure()
        uses: actions/upload-artifact@v2
        with:
          name: screenshots
          path: tests/Browser/screenshots
      - name: Upload Console Logs
        if: failure()
        uses: actions/upload-artifact@v2
        with:
          name: console
          path: tests/Browser/console

錯誤排除

連線失敗

出現 Failed to connect to localhost port 9515: Connection refused 這樣的訊息錯誤

可能解決方法:

在 Mac 環境內,Dusk 要求 ChromeDriver 的二進位文件 (binaries) 是可執行的。如果在 Dusk 運行時遇到問題,可以使用以下命令來調整其權限,以確保二進位文件 (binaries) 是可執行的

chmod -R 0755 vendor/laravel/dusk/bin

Chrome Driver 版本錯誤

出現 Facebook\WebDriver\Exception\SessionNotCreatedException: session not created: This version of ChromeDriver only supports Chrome version xx 這樣的錯誤訊息

可能解決方案:

代表目前Chrome版本,Dusk Driver不支持。可到這裡下載當前 Chrome對應的版本,可以到 "關於Google Chrome" 來查詢當前 Chrome 瀏覽器的版本


分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪