owned this note
owned this note
Published
Linked with GitHub
---
tags: laravel,單元測試
---
# 單元測試 - 瀏覽器測試
### 簡介
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 了
```php
//tests\DuskTestCase.php
//準備執行 Dusk 測試
public static function prepare()
{
// static::startChromeDriver();
}
```
然後,你可以修改 driver() 來連接到你選定的 URL 和端口。此外,你可以修改 「desired capabilities」(期望能力),它將會被傳遞給 WebDriver
```php
//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。它將會在每一次的測試時重新執行資料庫遷移
```php
//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` 文件中註解掉下面這一行:
```php
//tests\DuckTestCase.php
//為 Dusk 測試做準備
public static function prepare()
{
// static::startChromeDriver();
}
```
此外如果你的 ChromeDriver 並非運行在端口 9515,你需要修改同一個類別的 driver() ,修改成正確的端口號:
```php
//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() 來創建一個瀏覽器實例:
```php
//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 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() 的回呼中,用更多的參數作為名字來區分瀏覽器實例,然後傳給回呼去加入多個瀏覽器實例,看下面這個例子會比較清楚:
```php
$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() 中去呼叫它:
```php
//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() 就是我們自定義的瀏覽器宏
```php
$this->browse(function ($browser) use ($user) {
$browser->visit('/pay')
->scrollToElement('#credit-card-details')
->assertSee('Enter Credit Card Details');
});
```
#### 用戶認證
我們經常會測試需要身份驗證過後才能進入的頁面,你可以使用 Dusk 的 loginAs() 來避免在每次測試期間與登錄頁面進行交互。 loginAs() 接受用戶 ID 或者是用戶模型實例:
```php
use App\Models\User;
$this->browse(function ($browser) {
$browser->loginAs(User::find(1))
->visit('/home');
});
```
>注意:
>
>使用 loginAs() 後,用戶會話在該文件中的所有測試被維護,也就不需再次進行登入
#### Cookies
你可以使用 cookie() 來獲取或者設置加密過的 cookie 的值。預設情況下,所有被 Laravel 所建立的 cookie 都會被加密
```php
//獲取加密 cookie
$browser->cookie('name');
//設置加密 cookie
$browser->cookie('name', 'Zack');
```
使用 plainCookie() 則可以獲取或者設置未加密過的 cookie 的值
```php
//獲取未加密 cookie
$browser->plainCookie('name');
//設置未加密 cookie
$browser->plainCookie('name', 'Zack');
```
你可以使用 deleteCookie() 刪除指定的 cookie
```php
$browser->deleteCookie('name');
```
#### 執行 JavaScript
你能夠使用 script() 以便於在瀏覽器內去執行 JavaScript 腳本
```php
//單行腳本
$output = $browser->script('document.documentElement.scrollTop = 0');
//多行腳本
$output = $browser->script([
'document.body.scrollTop = 0',
'document.documentElement.scrollTop = 0',
]);
```
#### 獲取截圖
你可以使用 screenshot() 來截圖並將其以指定文件名來進行儲存,所有截圖都將存放在 tests/Browser/screenshots 目錄之下
```php
$browser->screenshot('filename');
```
#### 將控制台輸出結果保存到硬碟內
你可以使用 storeConsoleLog() 將控制台內容輸出到指定文件名的檔案並寫入硬碟內,控制台輸出預設存放在 tests/Browser/console 資料夾內
```php
$browser->storeConsoleLog('filename');
```
#### 將頁面的源碼保存到硬碟內
你可以使用 storeSource() 將頁面當前源代碼輸出到指定文件名的檔案並寫入硬碟內,頁面源代碼預設會存放到 tests/Browser/source 資料夾內
```php
$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()
```php
// 獲取值...
$value = $browser->value('selector');
// 設置值...
$browser->value('selector', 'value');
// 獲取輸入元素的值...
$value = $browser->inputValue('field');
```
##### 獲取文本
text() 可以用於獲取對應指定選擇器元素的文本
```php
$text = $browser->text('selector');
```
##### 獲取屬性
最後,attribute() 可以用於獲取對應指定選擇器元素的屬性
```php
$attribute = $browser->attribute('selector', 'value');
```
#### 使用表單
##### 輸入值
Dusk 提供了多種方法來與表單和輸入元素進行交互。首先,讓我們看一個在輸入項中輸入值的範例:
```php
$browser->type('email', 'info@goblinlab.org');
```
> 注意
>
> 儘管該方法在需要時可以傳入,但其實我們不需要將 CSS 選擇器傳遞給 type() 。如果沒有提供 CSS 選擇器的話,Dusk 會搜索包含指定 name 屬性的輸入項,最後 Dusk 還會嘗試尋找包含指定 name 屬性的 textarea輸入項
要想將文本附加到一個輸入項之後而不清除其內容, 你可以使用 append() :
```php
$browser->type('tags', 'foo')
->append('tags', ', bar, baz');
```
你可以使用 clear() 來清除輸入值
```php
$browser->clear('email');
```
你可以使用 typeSlowly() 去指示 Dusk 緩慢的輸入。預設情況下,Dusk 在兩次按鍵之間將暫停100毫秒。要自定義按鍵之間的時間量,你可以將適當的毫秒數作為方法的第二個參數傳入
```php
$browser->typeSlowly('tel', '(02)82752408');
$browser->typeSlowly('tel', '(02)82752408', 300);
```
你還可以使用 appendSlowly() 來緩慢添加文本
```php
$browser->type('tags', 'foo')
->appendSlowly('tags', ', bar, baz');
```
#### 下拉選單
需要在下拉選單中選擇值,你可以使用 select() 。 類似於 type() , select() 並不是一定要傳入 CSS 選擇器。 當使用 select() 時,你應該傳遞選項實際的值而不是它的顯示文字:
```php
$browser->select('size', 'Large');
```
你也可以通過省略第二個參數來隨機選擇一個選項
```php
$browser->select('size');
```
#### 複選框
使用「check」 複選框時,你可以使用 check() 。 像其他許多與輸入項相關的方法,並不是必須傳入 CSS 選擇器。 如果準確的選擇器無法找到的時候,Dusk 會搜索能夠與 name 屬性對應的複選框:
```php
$browser->check('terms');
```
uncheck()用來取消複選框的勾選
```php
$browser->uncheck('terms');
```
#### 單選按鈕
使用 「select」中單選按鈕選項時,你可以使用 radio() 。 像很多其他的與輸入項相關的方法一樣, 它也並不是必須傳入 CSS 選擇器。如果準確的選擇器無法被找到的時候, Dusk 會搜索能夠與 name 屬性或者 value 屬性相對應的單選按鈕
```php
$browser->radio('size', 'large');
```
#### 附件
attach() 可以附加一個文件到 file input 元素中。 像很多其他的與輸入項相關的方法一樣,他也並不是必須傳入 CSS 選擇器。如果準確的選擇器沒有被找到的時候, Dusk 會搜索與 name 屬性對應的檔案輸入框
$browser->attach('photo', __DIR__.'/photos/mountains.png');
>注意:
>
>attach() 需要使用 PHP Zip 擴展,因此你的伺服器必須安裝此擴展
#### 按下按鈕
press() 能夠用來點擊頁面上的按鈕元素。方法的第一參數可以是按鈕上的文字,也可以是 CSS 或 Dusk 選擇器
```php
$browser->press('Login');
```
當提交表單後,大多數應用會在表單的提交按鈕被按下後失效,直到表單的提交請求結束後才會重新啟用。要按下按鈕並等待該按鈕重新啟用,可以使用 pressAndWaitFor()
```php
//按下按鈕並等待按鈕重新啟用,最多等待5秒
$browser->pressAndWaitFor('Save');
//按下按鈕並等待按鈕重新啟用,最多等待1秒
$browser->pressAndWaitFor('Save', 1);
```
#### 點擊超連結
要點擊超連結,可以在瀏覽器實例下使用 clickLink() 。 clickLink() 將點擊指定文字的超連結:
```php
$browser->clickLink($linkText);
```
你可以使用 seeLink() 來確定具有指定顯示文字的超連結在頁面上是否可見:
```php
if ($browser->seeLink($linkText)) {
// ...
}
```
>注意:
>
>這些方法與 jQuery 交互。 如果頁面上沒有 jQuery , Dusk 會自動將其注入到頁面中,以便在測試期間使用
#### 使用鍵盤
keys() 讓你可以在指定元素中輸入比 type() 更加複雜的輸入序列。例如,你可以在輸入值的同時按下按鍵。在這個例子中,輸入 goblin 時, shift 鍵也同時被按下。當 goblin 輸入完之後, 將會輸入 lab 而不會按下任何組合按鍵:
```php
$browser->keys('selector', ['{shift}', 'goblin'], 'lab');
```
另一個有用的 keys() 用法,是寄送快捷鍵組合給你應用的主要 CSS 選擇器
```php
$browser->keys('.app', ['{command}', 'j']);
```
所有包在 {} 中的鍵盤按鍵,都對應定義於 Facebook\WebDriver\WebDriverKeys 類別中,你可以在[這裏](https://php-webdriver.github.io/php-webdriver/1.0.3/Facebook/WebDriver/WebDriverKeys.html)找到。
#### 使用滑鼠
##### 點擊元素
click() 方法可用於「點擊」與給定選擇器對應的元素:
```php
$browser->click('.selector');
```
clickAtXPath() 方法可用於「單擊」與指定 XPath 表達式對應的元素
```php
$browser->clickAtXPath('//div[@class = "selector"]');
```
clickAtPoint() 可用於「點擊」相對於瀏覽器可視區域的指定坐標對上的最高元素
```php
$browser->clickAtPoint($x = 0, $y = 0);
```
doubleClick() 可用於模擬滑鼠的雙擊
```php
$browser->doubleClick();
```
rightClick() 可用於模擬滑鼠的右擊:
```php
$browser->rightClick();
$browser->rightClick('.selector');
```
clickAndHold() 可用於模擬被單擊並按住的滑鼠按鈕。 隨後呼叫 releaseMouse() 將取消此行為並放開滑鼠按鈕
```php
$browser->clickAndHold()
->pause(1000)
->releaseMouse();
```
##### 滑鼠懸停
mouseover() 可用於與指定選擇器對應的元素的滑鼠懸停動作:
```php
$browser->mouseover('.selector');
```
##### 拖拉操作
drag() 用於將與指定選擇器對應的元素拖到其它元素:
```php
$browser->drag('.from-selector', '.to-selector');
```
或者,你也可以在單一方向上拖動元素:
```php
$browser->dragLeft('.selector', $pixels = 10);
$browser->dragRight('.selector', $pixels = 10);
$browser->dragUp('.selector', $pixels = 10);
$browser->dragDown('.selector', $pixels = 10);
```
最後,你可以將元素拖動指定的偏移量
```php
$browser->dragOffset('.selector', $x = 10, $y = 10);
```
#### JavaScript 對話框
Dusk 提供了幾種與 JavaScript 對話框交互的方法。比如,你能夠使用 waitForDialog() 去等待 JavaScript 對話框出現,這個方法接受一個可選參數來設定要等待幾秒鐘來等對話框出現
```php
// 等待對話框顯示:
$browser->waitForDialog($seconds = null);
```
```php
// assertDialogOpened() 用來確認對話框已經顯示,並且上面顯示的訊息與指定值相同
$browser->assertDialogOpened('Dialog message');
```
假如 JavaScript 對話框包含輸入項,你能夠使用 typeInDialog() 來輸入內容:
```php
$browser->typeInDialog('Hello World');
```
為了要透過按下 "OK" 按鈕來關閉一個打開的 JavaScript 對話框,你能夠呼叫 acceptDialog()
```php
$browser->acceptDialog();
```
為了要透過按下 "Cancel" 按鈕來關閉一個打開的 JavaScript 對話框,你能夠呼叫 dismissDialog()
```php
$browser->dismissDialog();
```
#### 選擇器作用範圍
有時可能希望在指定的選擇器範圍內執行多個操作。比如,可能想要確認表格中存在某些文字,然後點擊表格中的一個按鈕。可以使用 with() 實現此需求。回呼函數內所有被執行的操作都被限定在原始的選擇器上:
```php
$browser->with('.table', function ($table) {
$table->assertSee('Hello World')
->clickLink('Delete');
});
```
你可能偶爾需要在當前範圍之外執行確認。 你可以使用 elsewhere() 來完成此操作:
```php
$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 秒後拋出異常。如果有必要,可以傳入自定義超時時長作為其第二個參數:
```php
// 等待選擇器 5 秒時間...
$browser->waitFor('.selector');
// 等待選擇器 1 秒時間...
$browser->waitFor('.selector', 1);
```
你也能夠等到直至對應選擇器包含指定的文字
```php
// 等待選擇器最多 5 秒的時間,直到選擇器對應元素包含指定文字...
$browser->waitForTextIn('.selector', 'Hello World');
// 等待選擇器最多 1 秒的時間,直到選擇器對應元素包含指定文字...
$browser->waitForTextIn('.selector', 'Hello World', 1);
// 等待選擇器直到選擇器對應元素消失...
$browser->waitUntilMissing('.selector');
// 等待選擇器最多 1 秒的時間,直到選擇器對應元素消失...
$browser->waitUntilMissing('.selector', 1);
```
#### 選擇器可用時,限定作用域範圍
有時,你或許希望等待指定選擇器,然後與對應選擇器的元素進行交互。例如,你可能希望等到 "modal" 視窗可用,然後在 "modal" 視窗中點擊「確定」按鈕。在這種情況下,可以使用 whenAvailable() 。指定回呼內的所有要執行的元素操作都將被限定在起始選擇器上:
```php
$browser->whenAvailable('.modal', function ($modal) {
$modal->assertSee('Hello World')
->press('OK');
});
```
##### 等待文字
waitForText() 可以用於等待頁面上指定文字被顯示在頁面上
```php
//等待頁面上出現指定文字最多 5 秒時間...
$browser->waitForText('Hello World');
//等待頁面上出現指定文字最多 1 秒時間...
$browser->waitForText('Hello World', 1);
```
你也能夠使用 waitUntilMissingText() 去等待直到指定文字消失於頁面上
```php
//等待頁面上指定文字消失,最多 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() 等待頁面跳轉到指定路徑
```php
$browser->waitForLocation('/secret');
```
還可以等待被命名的路由跳轉:
```php
$browser->waitForRoute($routeName, $parameters);
```
##### 等待頁面重新加載
如果要在頁面重新加載後確認,可以使用 waitForReload() :
```php
$browser->click('.some-action')
->waitForReload()
->assertSee('something');
```
##### 等待 JavaScript 表達式
有時會希望暫停執行測試,直到指定的 JavaScript 表達式執行結果為 true。可以使用 waitUntil() 輕易地達成此目的。傳送一個表達式給此方法,不需要包含 return 關鍵字或者結束分號
```php
// 等待表達式為 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,以及一個可選的失敗訊息
```php
$browser->waitUsing(10, 1, function () use ($something) {
return $something->isReady();
}, "Something wasn't ready in time.");
```
#### 滾動到視圖中
有時您可能無法點擊某個元素,因為該元素在瀏覽器的可見區域之外。 scrollIntoView() 可以將元素滾動到瀏覽器可視窗口內:
```php
$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 組件狀態進行斷言:
```php
//Vue 基礎測試案例
public function testVue()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertVue('user.name', 'Taylor', '@profile-component');
});
}
```
---
### 可用的斷言
Dusk 提供了各種你可以對應使用的確認方法。所有可用的確認列表如下:
#### assertTitle()
確認頁面的標題(title)為指定值
```php
$browser->assertTitle($title);
```
#### assertTitleContains()
確認頁面標題(title)包含指定值
```php
$browser->assertTitleContains($title);
```
#### assertUrlIs()
確認當前的 URL(不包含 query 字串)滿足指定的字串:
```php
$browser->assertUrlIs($url);
```
#### assertSchemeIs()
確認當前的 URL scheme 滿足指定的 scheme:
```php
$browser->assertSchemeIs($scheme);
```
#### assertSchemeIsNot()
確認當前的 URL scheme 不滿足指定的 scheme:
```php
$browser->assertSchemeIsNot($scheme);
```
#### assertHostIs()
確認當前的 URL host 滿足指定的 host:
```php
$browser->assertHostIs($host);
```
#### assertHostIsNot()
確認當前的 URL host 不滿足指定的 host:
```php
$browser->assertHostIsNot($host);
```
#### assertPortIs()
確認當前的 URL 端口滿足指定的端口:
```php
$browser->assertPortIs($port);
```
#### assertPortIsNot()
確認當前的 URL 端口不滿足指定的端口:
```php
$browser->assertPortIsNot($port);
```
#### assertPathBeginsWith()
確認當前的 URL 以指定的字串開頭:
```php
$browser->assertPathBeginsWith('/home');
```
#### assertPathIs()
確認當前的路徑滿足指定的路徑:
```php
$browser->assertPathIs('/home');
```
#### assertPathIsNot()
確認當前的路徑不滿足指定的路徑:
```php
$browser->assertPathIsNot('/home');
```
#### assertRouteIs()
確認指定的 URL 不滿足指定的命名路由的 URL:
```php
$browser->assertRouteIs($name, $parameters);
```
#### assertQueryStringHas()
確認指定的 query 參數存在:
```php
$browser->assertQueryStringHas($name);
```
確認指定的 query 參數存在,並且是指定的值:
```php
$browser->assertQueryStringHas($name, $value);
```
#### assertQueryStringMissing()
確認指定的 query 參數不存在:
```php
$browser->assertQueryStringMissing($name);
```
#### assertFragmentIs()
確認當前的 fragment 滿足指定的 fragment:
```php
$browser->assertFragmentIs('anchor');
```
#### assertFragmentBeginsWith()
確認當前的 fragment 以指定的 fragment 開頭:
```php
$browser->assertFragmentBeginsWith('anchor');
```
#### assertFragmentIsNot()
確認當前的 fragment 不滿足指定的 fragment:
```php
$browser->assertFragmentIsNot('anchor');
```
#### assertHasCookie()
確認指定的 cookie 存在:
```php
$browser->assertHasCookie($name);
```
#### assertHasPlainCookie()
確認存在指定的未加密 cookie:
```php
$browser->assertHasPlainCookie($name);
```
#### assertCookieMissing()
確認指定的 cookie 不存在:
```php
$browser->assertCookieMissing($name);
```
#### assertPlainCookieMissing()
確認指定的未加密 cookie不存在:
```php
$browser->assertPlainCookieMissing($name);
```
#### assertCookieValue()
確認加密的 cookie 具有指定值:
```php
$browser->assertCookieValue($name, $value);
```
#### assertPlainCookieValue()
確認未加密的 cookie 具有指定值:
```php
$browser->assertPlainCookieValue($name, $value);
```
#### assertSee()
確認在頁面中有指定的文本:
```php
$browser->assertSee($text);
```
#### assertDontSee()
確認在頁面中沒有指定的文字:
```php
$browser->assertDontSee($text);
```
#### assertSeeIn()
確認在選擇器中有指定的文字:
```php
$browser->assertSeeIn($selector, $text);
```
#### assertDontSeeIn()
確認指定的字串在選擇器中不存在:
```php
$browser->assertDontSeeIn($selector, $text);
```
#### assertSeeAnythingIn()
確認在選擇器中存在任何字串:
```php
$browser->assertSeeAnythingIn($selector);
```
#### assertSeeNothingIn()
確認在選擇器中不存在任何字串:
```php
$browser->assertSeeNothingIn($selector);
```
#### assertScript()
確認指定的 JavaScript 表達式的值為指定的值:
```php
$browser->assertScript('window.isLoaded')
->assertScript('document.readyState', 'complete');
```
#### assertSourceHas()
確認在頁面中存在給定的源代碼:
```php
$browser->assertSourceHas($code);
```
#### assertSourceMissing()
確認頁面中沒有給定的源代碼:
```php
$browser->assertSourceMissing($code);
```
#### assertSeeLink()
確認在頁面中存在指定的超連結:
```php
$browser->assertSeeLink($linkText);
```
#### assertDontSeeLink()
確認頁面中沒有指定的超連結:
```php
$browser->assertDontSeeLink($linkText);
```
#### assertInputValue()
確認輸入框(input)有指定的值:
```php
$browser->assertInputValue($field, $value);
```
#### assertInputValueIsNot()
確認輸入框沒有指定的值:
```php
$browser->assertInputValueIsNot($field, $value);
```
#### assertChecked()
確認複選框(checkbox)有被選中:
```php
$browser->assertChecked($field);
```
#### assertNotChecked()
確認複選框(checkbox)沒有被選中:
```php
$browser->assertNotChecked($field);
```
#### assertRadioSelected()
確認單選框(radio)被選中:
```php
$browser->assertRadioSelected($field, $value);
```
#### assertRadioNotSelected()
確認單選框(radio)沒有被選中:
```php
$browser->assertRadioNotSelected($field, $value);
```
#### assertSelected()
確認下拉框有指定的值:
```php
$browser->assertSelected($field, $value);
```
#### assertNotSelected()
確認下拉框沒有指定的值:
```php
$browser->assertNotSelected($field, $value);
```
#### assertSelectHasOptions()
確認指定的陣列值是可選項:
```php
$browser->assertSelectHasOptions($field, $values);
```
#### assertSelectMissingOptions()
確認給定的陣列值是不可選的:
```php
$browser->assertSelectMissingOptions($field, $values);
```
#### assertSelectHasOption()
確認指定的值在指定的輸入項是可供選擇的:
```php
$browser->assertSelectHasOption($field, $value);
```
#### assertSelectMissingOption()
確認指定的值在指定的輸入項是無法選擇的:
```php
$browser->assertSelectMissingOption($field, $value);
```
#### assertValue()
確認選擇器範圍內的元素存在指定的值:
```php
$browser->assertValue($selector, $value);
```
#### assertAttribute()
確認與指定選擇器對應的元素在提供的屬性中具有指定的值:
```php
$browser->assertAttribute($selector, $attribute, $value);
```
#### assertAriaAttribute()
確認與指定選擇器對應的元素在指定的 aria 屬性中具有指定的值:
```php
$browser->assertAriaAttribute($selector, $attribute, $value);
```
例如,指定標記 <button aria-label="Add"> </button>,你可以像這樣聲明 aria-label 屬性:
```php
$browser->assertAriaAttribute('button', 'label', 'Add')
```
#### assertDataAttribute()
確認與指定選擇器對應的元素在提供的 data 屬性中具有指定的值:
```php
$browser->assertDataAttribute($selector, $attribute, $value);
```
例如,指定標記 <tr id="row-1" data-content="attendees"></tr>, 您可以像這樣聲明 data-label 屬性:
```php
$browser->assertDataAttribute('#row-1', 'content', 'attendees')
```
#### assertVisible()
確認選擇器範圍內的元素為可見:
```php
$browser->assertVisible($selector);
```
#### assertPresent()
確認選擇器範圍內的元素是存在的:
```php
$browser->assertPresent($selector);
```
#### assertNotPresent()
確認選擇器範圍內的元素在源代碼是不存在的:
```php
$browser->assertNotPresent($selector);
```
#### assertMissing()
確認選擇器範圍內的元素不存在:
```php
$browser->assertMissing($selector);
```
#### assertDialogOpened()
確認含有指定訊息的 JavaScript 對話框已經打開:
```php
$browser->assertDialogOpened($message);
```
#### assertEnabled()
確認指定的欄位是啟用的:
```php
$browser->assertEnabled($field);
```
#### assertDisabled()
確認指定的欄位是停用的:
```php
$browser->assertDisabled($field);
```
#### assertButtonEnabled()
確認指定的按鈕是啟用的:
```php
$browser->assertButtonEnabled($button);
```
#### assertButtonDisabled()
確認指定的按鈕是關閉的:
```php
$browser->assertButtonDisabled($button);
```
#### assertFocused()
確認焦點在於指定的欄位:
```php
$browser->assertFocused($field);
```
#### assertNotFocused()
確認焦點不在指定的欄位:
```php
$browser->assertNotFocused($field);
```
#### assertAuthenticated()
確認用戶已經授權,即已經登入
```php
$browser->assertAuthenticated();
```
#### assertGuest()
確認用戶未授權,即尚未登入
```php
$browser->assertGuest();
```
#### assertAuthenticatedAs()
確認該用戶已通過身份驗證為指定用戶:
```php
$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 組件的狀態
```php
//基本Vue測試
public function testVue()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertVue('user.name', 'Taylor', '@profile-component');
});
}
```
#### assertVueIsNot()
確認 Vue 組件資料的屬性不對應指定的值:
```php
$browser->assertVueIsNot($property, $value, $componentSelector = null);
```
#### assertVueContains()
確認 Vue 組件資料的屬性是一個陣列,並且該陣列包含指定的值:
```php
$browser->assertVueContains($property, $value, $componentSelector = null);
```
#### assertVueDoesNotContain()
確認 Vue 組件資料的屬性是一個陣列,並且該陣列不包含指定的值:
```php
$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 來導航到具體頁面:
```php
//取得頁面網址
public function url()
{
return '/login';
}
```
##### assert()
assert() 可以作出任何確認來驗證瀏覽器是否在指定頁面上。這個方法並不是必須的,你可以根據你自己的需求來做出這些確認。這些確認會在你導航到這個頁面的時候自動執行:
```php
//確認瀏覽器當前處於指定頁面
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
```
#### 導航至頁面
一旦頁面配置好之後,你可以使用 visit() 導航至頁面:
```php
use Tests\Browser\Pages\Login;
$browser->visit(new Login);
```
有時您可能已經在指定的頁面上,需要將頁面的選擇器和方法「加載」到當前的測試上下文中。 這在通過按鈕轉址到指定頁面而沒有明確導航到該頁面時很常見。 在這種情況下,您可以使用 on() 去加載頁面:
```php
use Tests\Browser\Pages\CreatePlaylist;
$browser->visit('/dashboard')
->clickLink('Create Playlist')
->on(new CreatePlaylist)
->assertSee('@create');
```
#### 選擇器簡寫
elements() 允許你為頁面中的任何 CSS 選擇器定義簡單易記的簡寫。例如,讓我們為應用登入頁中的 email 輸入框定義一個快速且容易記憶的簡寫。例如,讓我們來為應用的登入頁去定義 "email" 輸入項的簡寫:
```php
//獲取頁面的元素簡寫
public function elements()
{
return [
'@email' => 'input[name=email]',
];
}
```
現在你可以用這個簡寫來代替之前在頁面中使用的完整 CSS 選擇器:
```php
$browser->type('@email', 'taylor@laravel.com');
```
#### 全局的選擇器簡寫
安裝 Dusk 之後,Page 父類別存放在你的 tests/Browser/Pages 資料夾。該類別中包含一個 siteElements() ,這個方法可以用來定義全局的選擇器簡寫,這樣在你應用中每個頁面都可以使用這些全局選擇器簡寫了:
//獲取站點全域的選擇器簡寫
public static function siteElements()
{
return [
'@element' => '#selector',
];
}
#### 頁面方法
除了頁面中已經定義的預設方法之外,你還可以定義在整個測試過程中會使用到的其他方法。例如,假設我們正在開發一個音樂管理應用,在應用中都可能需要一個公共的方法來創建列表,而不是在每一頁、每一個測試類別中都重寫一遍創建播放列表的邏輯,這時候你可以在你的頁面類別中定義一個 createPlaylist():
```php
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');
}
}
```
一旦方法被定義之後,你可以在任何使用到該頁的測試中使用這個方法了。瀏覽器實例會自動作為第一參數傳遞到該頁面方法:
```php
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);
});
}
}
#### 使用組件
組件定義一旦完成,在任何測試頁面的日期選擇器中選定一個日期就很輕鬆了。並且如果需要修改選定日期的邏輯,僅修改該組件即可:
```php
//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 檔案:
```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不支持。可到[這裡](http://chromedriver.chromium.org/downloads)下載當前 Chrome對應的版本,可以到 "關於Google Chrome" 來查詢當前 Chrome 瀏覽器的版本