介绍

Laravel Dusk 提供了富有表现力、简单易用的浏览器自动化及测试 API 。默认情况下,Dusk 不需要在你的机器上安装 JDK 或者 Selenium 。而是需要使用单独的 ChromeDriver 进行安装。当然,你也可以自由使用其他的兼容 Selenium 的驱动程序。

安装

你应该先向你的 Composer 添加 laravel/dusk 依赖 :

composer require --dev laravel/dusk

如果你是手动注册 Dusk 服务提供者,一定 不能 在你的生产环境中注册,这样可能会导致一些不守规矩的用户拥有控制你应用的权限。

安装好 Dusk 包后,运行 dusk:install 命令:

php artisan dusk:install

Browser 目录将会在 tests 目录下被创建,并且包含一个测试用例。接下来,在你的 .env 文件中设置 APP_URL 变量。这个值应该与你在浏览器中打开本应用的 URL 匹配。

要运行测试,使用 dusk 命令。 dusk 命令可以使用与 phpunit 命令同样的参数:

php artisan dusk

如果上次运行 dusk 命令时测试失败,则可以通过使用 dusk:fails 命令重新运行失败的测试来节省时间:

php artisan dusk:fails

Dusk 要求 ChromeDriver 二进制文件是可执行的。如果在 Dusk 运行时遇到问题,可以使用以下命令确保二进制文件是可执行的:chmod -R 0755 vendor/laravel/dusk/bin

使用其他浏览器

默认情况下, Dusk 使用 Google Chrome 浏览器和一个单独的 ChromeDriver 的安装来运行你的浏览器测试。当然,你可以运行你自己的 Selenium 服务,用任何你想用的浏览器来进行测试。

如果要这么做,打开 tests/DuskTestCase.php 文件,这个是应用测试用例的基类。在这个文件中,你可以移除对 startChromeDriver 方法的调用。这样 Dusk 就不会自动启动 ChromeDriver 了。

/**
 * 准备执行 Dusk 测试。
 *
 * @beforeClass
 * @return void
 */
public static function prepare()
{
    // static::startChromeDriver();
}

然后,你可以修改 driver 方法来连接到你选定的 URL 和端口。此外,你可以修改 「desired capabilities」(期望能力),它将会被传递给 WebDriver:

/**
 * 创建 RemoteWebDriver 实例。
 *
 * @return \Facebook\WebDriver\Remote\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 命令来运行你的浏览器测试:

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 文件中注释掉下面这一行:

/**
 * 为 Dusk 测试做准备。
 *
 * @beforeClass
 * @return void
 */
public static function prepare()
{
    // static::startChromeDriver();
}

另外,如果你的 ChromeDriver 运行在非 9515 端口 ,你需要修改同一个类中的 driver 方法:

/**
 * 创建 RemoteWebDriver 实例。
 *
 * @return \Facebook\WebDriver\Remote\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

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

    /**
     * 一个基本的浏览器测试例子。
     *
     * @return void
     */
    public function testBasicExample()
    {
        $user = factory(User::class)->create([
            'email' => 'taylor@laravel.com',
        ]);

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

在上面的例子中,browse 方法接收了一个回调参数。Dusk 会自动将这个浏览器实例注入到回调过程中,而且这个浏览器实例可以和你的应用进行交互和断言。

这个测试例子可以用来测试 make:auth 命令生成的登录界面。

创建多个浏览器

有时候你可能需要多个浏览器才能正确的进行测试。例如,使用多个浏览器测试通过 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 Taylor')
          ->assertSee('Jeffrey Way');
});

改变浏览器窗口大小

你可以使用 resize 方法去调整浏览器的窗口大小:

$browser->resize(1920, 1080);

maximize 方法可以将浏览器窗口最大化:

$browser->maximize();

浏览器宏

如果你想定义一个可以在各种测试中重复使用的自定义浏览器方法,可以在 Browser 类中使用 macro 方法。通常,你应该从 服务提供者boot 方法中调用它:

<?php

namespace App\Providers;

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

class DuskServiceProvider extends ServiceProvider
{
    /**
     * 注册Dusk的浏览器宏
     *
     * @return void
     */
    public function boot()
    {
        Browser::macro('scrollToElement', function ($element = null) {
            $this->script("$('html, body').animate({ scrollTop: $('$element').offset().top }, 0);");

            return $this;
        });
    }
}

macro 方法接收一个名称作为第一个参数,第二个参数则是一个闭包。 当调用浏览器宏作为一个 Browser 的实现的方法时,浏览器宏的闭包将会执行:

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

认证

你可能经常会测试一些需要认证的页面。你可以使用 Dusk 的 loginAs 方法来避免每个测试都去登陆页面登陆一次。 loginAs 可以使用用户 ID 或者用户模型实例:

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

使用 loginAs 方法后,该用户的 session 将会持久化的供其他测试用例使用。

数据库迁移

就像上面的认证例子一样,当你的测试用例需要迁移的时候,你不应该使用 RefreshDatabase trait。 RefreshDatabase trait 使用了不适用于 HTTP 请求的数据库事务。取而代之,我们要用 DatabaseMigrations trait:

<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;
}

与元素交互

Dusk 选择器

选择一个好的 CSS 选择器用于元素交互是编写 Dush 测试最困难的部分之一。随着时间推移,前端的更改可能会导致类似以下的 CSS 选择器中断测试:

// HTML...

<button>Login</button>

// Test...

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

Dusk 选择器让你专注于编写有效的测试,而不是去记忆 CSS 选择器。要定义一个选择器,只需在你的 HTML 元素中添加一个 dusk 属性。然后,在选择器前面添加 @ 去操作 Dusk 测试中的附加元素:

// HTML...

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

// Test...

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

点击链接

要点击链接的话,你可以在浏览器实例上使用 clickLink 方法。clickLink 方法将会点击指定显示文本的链接:

$browser->clickLink($linkText);

{注意} 这个方法可以与 jQuery 进行交互。如果页面上没有 jQuery,Dusk 会自动将其注入页面,保证在测试的期间可用。

文本、值 & 属性

检索和设置值

Dusk 提供了几种与当前显示文本,值和属性进行交互的方法。例如,要获取与指定选择器匹配的元素的「值」,请使用 value 方法:

// 检索值...
$value = $browser->value('selector');

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

检索文本

text 这个方法可以用来匹配指定选择器中元素的显示文本:

$text = $browser->text('selector');

检索属性

最后, attribute 这个方法 可以用来匹配指定选择器中元素的属性:

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

表单的使用

输入值

Dusk 提供了与表单和input元素交互的各种方法。首先让我们看一个在 input 框中输入文本的例子:

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

注意, 虽然 type 方法可以传递 CSS 选择器做为一个参数,但这并不是强制要求的。如果没有提供 CSS 选择器, Dusk 会搜索与 name 属性相同的 input 。如果还是没有找到,Dusk 会尝试查找传入值与 name 属性相同的 textarea

要想将文本附加到一个字段之后而且不清除其内容, 你可以使用 append 方法:

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

你可以使用 clear 方法清除输入值:

$browser->clear('email');

下拉菜单

需要在下拉菜单中选择值,你可以使用 select 方法。 类似于 type 方法, select 方法并不是一定要传入 CSS 选择器。 当使用 select 方法时,你应该传递选项实际的值而不是它的显示文本:

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

你也可以通过省略第二个参数来随机选择一个选项:

$browser->select('size');

复选框

使用「check」 复选框时,你可以使用 check 方法。 像其他许多与input 相关的方法,并不是必须传入 CSS 选择器。 如果准确的选择器无法找到的时候,Dusk 会搜索能够与 name 属性匹配的复选框:

$browser->check('terms');

$browser->uncheck('terms');

单选按钮

使用 「select」中单选按钮选项时,你可以使用 radio 这个方法。 像很多其他的与输入相关的方法一样, 它也并不是必须传入 CSS 选择器。如果准确的选择器无法被找到的时候, Dusk 会搜索能够与 name 属性或者 value 属性相匹配的单选按钮:

$browser->radio('version', 'php7');

附件

attach 方法可以附加一个文件到 file input 元素中。 像很多其他的与输入相关的方法一样,他也并不是必须传入CSS 选择器。如果准确的选择器没有被找到的时候, Dusk 会搜索与 name 属性匹配的文件输入框:

$browser->attach('photo', __DIR__.'/photos/me.png');

{注意} attach 方法需要使用 PHP Zip 扩展,你的服务器必须安装了此扩展。

使用键盘

keys 方法让你可以再指定元素中输入比 type 方法更加复杂的输入序列。例如,你可以在输入值的同时按下按键。在这个例子中, 输入 taylor 时, shift 键也同时被按下。当 taylor 输入完之后, 将会输入 otwell 而不会按下任何按键:

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

你甚至可以在你的应用中选中某个元素之后按下「快捷键」:

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

{提示} 所有包在 {} 中的键盘按键, 都对应定义于 Facebook\WebDriver\WebDriverKeys 类中,你可以在 GitHub 中找到。

使用鼠标

点击元素

click 方法可用于「点击」与给定选择器匹配的元素:

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

鼠标悬停

mouseover 方法可用于与给定选择器匹配的元素的鼠标悬停动作:

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

拖放

drag 方法用于将与指定选择器匹配的元素拖到其它元素:

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

或者,可以在单一方向上拖动元素:

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

JavaScript 对话框

Dusk 提供了几种与 JavaScript 对话框交互的方法:

// 等待对话框显示:
$browser->waitForDialog($seconds = null);

// 断言对话框已经显示,并且其消息与给定值匹配:
$browser->assertDialogOpened('value');

// 在打开的 JavaScript 提示对话框中输入给定值:
$browser->typeInDialog('Hello World');

通过点击确定按钮关闭打开的 JavaScript 对话框:

$browser->acceptDialog();

通过点击取消按钮关闭打开的 JavaScript 对话框(仅对确认对话框有效):

$browser->dismissDialog();

选择器作用范围

有时可能希望在给定的选择器范围内执行多个操作。比如,可能想要断言表格中存在某些文本,然后点击表格中的一个按钮。可以使用 with 方法实现此需求。回调函数内所有被执行的操作都被限定在原始的选择器上:

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

等待元素

在测试大面积使用 JavaScript 的应用时,在进行测试之前,经常需要「等待」指定元素或数据可用。Dusk 使之更容易。使用一系列方法,可以等到页面元素可用,甚至给定的 JavaScript 表达式执行结果为 true

等待

如果需要测试暂停指定的毫秒数,可以使用 pause 方法:

$browser->pause(1000);

等待选择器

waitFor 方法可以用于暂停执行测试,直到页面上与给定 CSS 选择器匹配的元素被显示。默认情况下,将在暂停超过 5 秒后抛出异常。如果有必要,可以传递自定义超时时长作为其第二个参数:

// 等待选择器 5 秒时间...
$browser->waitFor('.selector');

// 等待选择器 1 秒时间...
$browser->waitFor('.selector', 1);

还可以等待指定选择器从页面消失:

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

$browser->waitUntilMissing('.selector', 1);

选择器可用时限定作用域范围

偶尔可能希望等待选择器然后与其互动。例如,可能希望等待模态窗口可用,然后点击模态窗口的「确定」按钮。 whenAvailable 方法能够用于这种情况。给定回调内的所有要执行的元素操作都将被限定在起始选择器上:

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

等待文本

waitForText 方法可以用于等待页面上给定文字被显示:

// 等待指定文本 5 秒时间...
$browser->waitForText('Hello World');

// 等待指定文本 1 秒时间...
$browser->waitForText('Hello World', 1);

等待链接

waitForLink 方法用于等待给定链接文字在页面上显示:

// 等待指定链接 5 秒时间...
$browser->waitForLink('Create');

// 等待给定链接 2 秒时间...
$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.dataLoaded');

$browser->waitUntil('App.data.servers.length > 0');

// 等待表达式为 true 1 秒时间...
$browser->waitUntil('App.data.servers.length > 0', 1);

等待 Vue 表达式

下面的方法可以用于等待给定的 Vue 组件属性包含或不包含给定值:

// 等待组件属性包含给定值...
$browser->waitUntilVue('user.name', 'Taylor', '@user');

// 等待组件属性不包含给定值...
$browser->waitUntilVueIsNot('user.name', null, '@user');

等待回调

Dusk 的许多「等待」方法都依赖底层的 waitUsing 方法。可以直接使用此方法来等待给定的回调返回 truewaitUsing 方法接受等待的最大秒数,闭包执行的间隔时长,被执行的闭包,还有可靠的失败消息:

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

做出 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 测试例子。
 *
 * @return void
 */
public function testVue()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->assertVue('user.name', 'Taylor', '@profile-component');
    });
}

可用的断言

Dusk 提供了一系列可用的断言方法。所有断言如下:

assertTitle assertTitleContains assertUrlIs assertSchemeIs assertSchemeIsNot assertHostIs assertHostIsNot assertPortIs assertPortIsNot assertPathBeginsWith assertPathIs assertPathIsNot assertRouteIs assertQueryStringHas assertQueryStringMissing assertFragmentIs assertFragmentBeginsWith assertFragmentIsNot assertHasCookie assertCookieMissing assertCookieValue assertPlainCookieValue assertSee assertDontSee assertSeeIn assertDontSeeIn assertSourceHas assertSourceMissing assertSeeLink assertDontSeeLink assertInputValue assertInputValueIsNot assertChecked assertNotChecked assertRadioSelected assertRadioNotSelected assertSelected assertNotSelected assertSelectHasOptions assertSelectMissingOptions assertSelectHasOption assertValue assertVisible assertPresent assertMissing assertDialogOpened assertEnabled assertDisabled assertFocused assertNotFocused assertVue assertVueIsNot assertVueContains assertVueDoesNotContain

assertTitle

断言网页标题匹配指定的文本:

$browser->assertTitle($title);

assertTitleContains

断言网页标题包含指定的文本:

$browser->assertTitleContains($title);

assertUrlIs

断言当前 URL (不带查询字符串) 匹配指定的字符串:

$browser->assertUrlIs($url);

assertSchemeIs

断言当前 URL 匹配与给定的字符串匹配:

$browser->assertSchemeIs($scheme);

assertSchemeIsNot

断言当前 URL 匹配与给定的字符串不匹配:

$browser->assertSchemeIsNot($scheme);

assertHostIs

断言当前 URL 的 host 与给定的值匹配:

$browser->assertHostIs($host);

assertHostIsNot

断言当前 URL 的 host 与给定的值不匹配:

$browser->assertHostIsNot($host);

assertPortIs

断言当前 URL 的端口值与给定的值匹配:

$browser->assertPortIs($port);

assertPortIsNot

断言当前 URL 的端口值与给定的值不匹配:

$browser->assertPortIsNot($port);

assertPathBeginsWith

断言当前 URL 开始于指定的路径:

$browser->assertPathBeginsWith($path);

assertPathIs

断言当前路径匹配指定的路径:

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

assertPathIsNot

断言当前路径不匹配指定的路径:

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

assertRouteIs

断言当前 URL 匹配指定的命名路由的 URL:

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

assertQueryStringHas

断言存在指定的查询字符串参数:

$browser->assertQueryStringHas($name);

断言指定的查询字符串参数存在,并且该参数的值为指定的值:

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

assertQueryStringMissing

断言不存在指定的查询字符串参数:

$browser->assertQueryStringMissing($name);

assertFragmentIs

断言目前的分片符合指定的分片:

$browser->assertFragmentIs('anchor');

assertFragmentBeginsWith

断言目前的分片以指定的分片开头:

$browser->assertFragmentBeginsWith('anchor');

assertFragmentIsNot

断言目前的分片不符合指定的分片:

$browser->assertFragmentIsNot('anchor');

assertHasCookie

断言存在指定的 cookie:

$browser->assertHasCookie($name);

assertCookieMissing

断言不存在指定的 cookie:

$browser->assertCookieMissing($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);

assertSourceHas

断言当前页存在指定的源码:

$browser->assertSourceHas($code);

assertSourceMissing

断言当前页不存在指定的源码:

$browser->assertSourceMissing($code);

断言当前页存在指定的链接:

$browser->assertSeeLink($linkText);

断言当前页不存在指定的链接:

$browser->assertDontSeeLink($linkText);

assertInputValue

断言输入框存在指定的值:

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

assertInputValueIsNot

断言输入框不存在指定的值:

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

assertChecked

断言指定的复选框被选中:

$browser->assertChecked($field);

assertNotChecked

断言指定的复选框未选中:

$browser->assertNotChecked($field);

assertRadioSelected

断言指定的单选按钮被选取:

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

assertRadioNotSelected

断言指定的单选按钮未被选取:

$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);

assertValue

断言选择器范围内的元素存在指定的值:

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

assertVisible

断言选择器范围内的元素可见:

$browser->assertVisible($selector);

assertPresent

断言选择器范围内的元素是存在的:

$browser->assertPresent($selector);

assertMissing

断言选择器范围内的元素不存在:

$browser->assertMissing($selector);

assertDialogOpened

断言含有指定消息的 JavaScript 对话框已经打开:

$browser->assertDialogOpened($message);

assertEnabled

断言指定的字段是启用的:

$browser->assertEnabled($field);

assertDisabled

断言指定的字段是停用的:

$browser->assertDisabled($field);

assertFocused

断言焦点在于指定的字段:

$browser->assertFocused($field);

assertNotFocused

断言焦点不在指定的字段:

$browser->assertNotFocused($field);

assertVue

断言 Vue 组件数据的属性匹配指定的值:

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

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 Artisan 命令可以生成页面对象。所有的页面对象都位于 tests/Browser/Pages 目录:

php artisan dusk:page Login

配置页面

页面默认拥有 3 个方法: urlassertelements。 在这里我们先详述 urlassert 方法, elements 方法将会 选择器简写 中详述。

url 方法

url 方法应该返回表示页面 URL 的路径。 Dusk 将会在浏览器中使用这个 URL 来导航到具体页面:

/**
 * 获得页面 URL 路径。
 *
 * @return string
 */
public function url()
{
    return '/login';
}

assert 方法

assert 方法可以作出任何断言来验证浏览器是否在指定页面上。这个方法并不是必须的。你可以根据你自己的需求来做出这些断言。这些断言会在你导航到这个页面的时候自动执行:

/**
 * 断言浏览器当前处于指定页面。
 *
 * @return void
 */
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 输入框定义一个简写:

/**
 * 获取页面的元素简写。
 *
 * @return array
 */
public function elements()
{
    return [
        '@email' => 'input[name=email]',
    ];
}

现在你可以用这个简写来代替之前在页面中使用的完整 CSS 选择器:

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

全局的选择器简写

安装 Dusk 之后,Page 基类存放在你的 tests/Browser/Pages 目录。该类中包含一个 siteElements 方法,这个方法可以用来定义全局的选择器简写,这样在你应用中每个页面都可以使用这些全局选择器简写了:

/**
 * 获取站点全局的选择器简写。
 *
 * @return array
 */
public static function siteElements()
{
    return [
        '@element' => '#selector',
    ];
}

页面方法

除了页面中已经定义的默认方法之外,你还可以定义在整个测试过程中会使用到的其他方法。例如,假设我们正在开发一个音乐管理应用,在应用中都可能需要一个公共的方法来创建列表,而不是在每一页、每一个测试类中都重写一遍创建播放列表的逻辑,这时候你可以在你的页面类中定义一个 createPlaylist 方法:

<?php

namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class Dashboard extends Page
{
    // 其他页面方法...

    /**
     * 创建一个新的播放列表。
     *
     * @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和功能片断,比如说导航条或信息通知弹窗。因此,组件并不会绑定于某个明确的 URL。

组件的生成

为了生成一个组件, 使用 Artisan 命令 dusk:component 即可生成组件。新生成的组件位于 test/Browser/Components 目录中:

php artisan dusk:component DatePicker

如上所示,这是生成一个“日期选择器”(date picker) 组件的示例,这个组件可能会贯穿使用在你应用程序的许多页面中。在整个测试套件的大量测试页面中,手动编写日期选择的浏览器自动化逻辑会非常麻烦。 更方便的替代办法是,定义一个表示日期选择器的 Dusk 组件,然后把自动化逻辑封装在该组件内:

<?php

namespace Tests\Browser\Components;

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

class DatePicker extends BaseComponent
{
    /**
     * 获取组件的 root selector
     *
     * @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',
            '@month-list' => 'div > div.datepicker-months',
            '@day-list' => 'div > div.datepicker-days',
        ];
    }

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

组件的使用

组件定义一旦完成,在任何测试页面的日期选择器中选定一个日期就很轻松了。并且,如果需要修改选定日期的逻辑,仅修改该组件即可:

<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    /**
     * 基本的组件测试示例
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
                    ->within(new DatePicker, function ($browser) {
                        $browser->selectDate(1, 2018);
                    })
                    ->assertSee('January');
        });
    }
}

持续集成

CircleCI

如果使用 CircleCI 运行 Dusk 测试,则可以参考此配置文件作为起点。与 TravisCI 一样,我们将使用 php artisan serve 命令启动 PHP 的内置 Web 服务器:

version: 2
jobs:
    build:
        steps:
            - run: sudo apt-get install -y libsqlite3-dev
            - run: cp .env.testing .env
            - run: composer install -n --ignore-platform-reqs
            - run: npm install
            - run: npm run production
            - run: vendor/bin/phpunit

            - run:
                name: Start Chrome Driver
                command: ./vendor/laravel/dusk/bin/chromedriver-linux
                background: true

            - run:
                name: Run Laravel Server
                command: php artisan serve
                background: true

            - run:
                name: Run Laravel Dusk Tests
                command: php artisan dusk

Codeship

Codeship 中运行 Dusk 测试,需要在你的 Codeship 项目中添加以下命令。当然,这些命令只是作为范例,你可以根据需要随意添加额外的命令:

phpenv local 7.2
cp .env.testing .env
mkdir -p ./bootstrap/cache
composer install --no-interaction --prefer-dist
php artisan key:generate
nohup bash -c "php artisan serve 2>&1 &" && sleep 5
php artisan dusk

Heroku CI

Heroku CI 中运行 Dusk 测试时,请将下列 Google Chrome 构建包和脚本添加到你的 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 > /dev/null 2>&1 &' && php artisan dusk"
      }
    }
  }
}

Travis CI

Travis CI 中运行 Dusk 测试时,你可以参考 .travis.yml 的配置。由于 Travis CI 不是图形环境,因此我们需要采取一些额外的步骤来启动 Chrome 浏览器。此外,我们将使用 php artisan serve 启动 PHP 的内置 Web 服务器:

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

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

script:
  - php artisan dusk

.env.testing 文件中,调整 APP_URL 的值:

APP_URL=http://127.0.0.1:8000