浏览器测试(Laravel Dusk)

简介

Laravel Dusk 提供了一个直观且易于使用的浏览器自动化测试 API。默认情况下,Dusk 不需要在计算机上安装 JDK 或 Selenium,而是安装单独的 ChromeDriver。不过,您也可以自由使用任何其它兼容 Selenium 的驱动。

安装

首先,在项目中添加 Composer 依赖 laravel/dusk

composer require --dev laravel/dusk

安装 Dusk 后,需要注册服务提供者 Laravel\Dusk\DuskServiceProvider。通常情况下,此步骤会通过 Laravel 的服务提供者自动注册自动完成。

如果手动注册 Dusk 的服务提供者,不要在生产环境中注册,这会导致任意用户都可以在应用中进行身份认证。

安装 Dusk 扩展包后,运行 Artisan 命令 dusk:install

php artisan dusk:install

tests 目录中会创建一个 Browser 目录,里面并包含示例测试文件。接下来,在 .env 文件中设置 APP_URL 环境变量。此值应该和在浏览器中访问应用的 URL 一致。

要运行测试,可以使用 Artisan 命令 duskdusk 命令接收 phpunit 命令接收的任何参数:

php artisan dusk

如果上次运行 dusk 后有失败的测试,可以先使用 dusk:fails 命令重新运行失败的测试,从而节省时间:

php artisan dusk:fails

使用其它浏览器

默认情况下,Dusk 使用 Google Chrome 并安装单独的 ChromeDriver 进行浏览器测试。不过,您也可以启动自己的 Selenium 服务器并使用任何想要的浏览器运行测试。

首先,打开 tests/DuskTestCase.php 文件,此文件是应用的 Dusk 测试用例基类。在此文件中,可以移除对 startChromeDriver 方法的调用。这样 Dusk 会停止自动启动 ChromeDriver:

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

然后,可以修改 driver 方法连接到您选择的 URL 和端口。此外,还可以修改传递给 WebDriver 的「所需功能」:

/**
 * 创建 RemoteWebDriver 实例
 *
 * @return \Facebook\WebDriver\Remote\RemoteWebDriver
 */
protected function driver()
{
    return RemoteWebDriver::create(
        'http://localhost:4444/wd/hub', DesiredCapabilities::phantomjs()
    );
}

快速入门

生成测试

生成 Dusk 测试,可以使用 Artisan 命令 dusk:make。生成的测试会放在 tests/Browser 目录中:

php artisan dusk:make LoginTest

运行测试

运行浏览器测试,可以使用 Artisan 命令 dusk

php artisan dusk

如果上次运行 dusk 后有失败的测试,可以先使用 dusk:fails 命令重新运行失败的测试,从而节省时间:

php artisan dusk:fails

dusk 命令接收任何运行 PHPUnit 测试时正常接收的参数,允许您仅为给定 运行测试等等:

php artisan dusk --group=foo

手动运行 ChromeDriver

默认情况下,Dusk 会自动尝试启动 ChromeDriver。如果在您的系统上不工作,可以在运行 dusk 命令前手动启动 ChromeDriver。如果选择手动启动 ChromeDriver,需要在 tests/DuskTestCase.php 文件中将如下行注释掉:

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

此外,如果不是在 9515 端口号上启动 ChromeDriver,应该修改同一个类中 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 方法:

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 会自动将浏览器实例传递到回调,此实例是用于和浏览器交互并断言的主对象。

此测试可用于测试 Artisan 命令 make:auth 生成的登录界面。

创建多个浏览器

有时可能需要多个浏览器来完成测试。例如,测试基于 WebSocket 交互的聊天界面时就需要多个浏览器。要创建多个浏览器,在 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();

认证

通常情况下,都会测试需要身份认证的页面。可以使用 Dusk 的 loginAs 方法避免在每个测试中都要和登录页交互。loginAs 方法接收一个用户 ID 或者用户模型实例:

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

使用 loginAs 方法后,所有测试中都会使用此文件中的用户维持用户会话。

数据库迁移

当测试需要迁移时,例如上述认证示例,不能使用 RefreshDatabase Trait。RefreshDatabase Trait 利用了 HTTP 请求中不能使用的数据库事务。所以,应该使用 DatabaseMigrations Trait:

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 选择器与元素进行交互是编写 Dusk 测试时最难的地方之一。随着时间的推移,前端改动可能导致以下 CSS 选择器中断测试:

// HTML

<button>Login</button>

// 测试

$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 提供各种方法与表单和输入元素进行交互。首先,我们看一个在输入框中键入文本的示例:

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

需要注意的是,尽管此方法可以在必要时接收一个 CSS 选择器,但并不要求我们一定要传递 CSS 选择器到 type 方法。如果没有提供 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 方法。与其它相关输入方法一样,此方法不要求完整的 CSS 选择器。如果没有匹配到完整的选择器,Dusk 会搜索包含匹配的 name 属性的多选框:

$browser->check('terms');

$browser->uncheck('terms');

单选按钮

要「选择」单选按钮,可以使用 radio 方法。与其它相关输入方法一样,此方法不要求完整的 CSS 选择器。如果没有匹配到完整的选择器,Dusk 会搜索包含匹配的 namevalue 属性的单选按钮:

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

添加文件

attach 方法可用于将文件添加到 file 输入元素。与其它相关输入方法一样,此方法不要求完整的 CSS 选择器。如果没有匹配到完整的选择器,Dusk 会搜索包含匹配的 name 属性的文件 input 元素:

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

此方法要求服务器安装并启用了 Zip PHP 扩展。

使用键盘

keys 方法可以为给定元素提供比 type 方法更多样更复杂的输入方式。例如,可以按住功能键输入值。在此示例中,当 taylor 在与给定选择器相匹配的元素中输入时,会按住 shift 键。taylor 打完字后,松开功能键输入 otwell

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

甚至还可以将「快捷键」发送 CSS 选择器选中的元素:

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

限制选择器范围

有时可能希望将所有操作限制在给定选择器中,同时完成多个操作。例如,可能希望只在一个表格中断言一些文本存在,然后点击表格中的一个按钮。可以使用 with 完成此操作。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);

限制为可用的选择器

有时可能希望等待给定选择器,然后和选择器相匹配的元素进行交互。例如,可能希望一直等待直到模态框可用,然后点击模态框中的「OK」按钮。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');

// 最多等待链接 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.dataLoaded');

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

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

等待回调

Dusk 中的很多「等待」方法都依赖底层的 waitUsing 方法。可以直接使用此方法一直等待直到给定回调返回 truewaitUsing 方法接收最多等待的秒数,判断闭包值的间隔时间,闭包和一个可选的失败信息:

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

断言 Vue

Dusk 甚至可以对 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 主机与给定主机相匹配:

$browser->assertHostIs($host);

assertHostIsNot

断言当前 URL 与给定主机不匹配:

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

assertSeeLink

断言页面中存在给定链接:

$browser->assertSeeLink($linkText);

assertDontSeeLink

断言页面中不存在给定链接:

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

页面

有时,测试时需要按顺序执行几个复杂的操作。这会让测试难以阅读和理解。页面允许您定义表达式操作,然后使用单个方法在给定页面上执行这些操作。页面还允许您为应用或单个页面定义常用选择器的快捷操作。

生成页面

要生成页面对象,可以使用 Artisan 命令 dusk:page。所有页面对象都会放在 tests/Browser/Pages 目录中:

php artisan dusk:page Login

配置页面

默认情况下,页面有三个方法: 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 方法,而不是在每个测试中重新编写创建播放列表的逻辑:

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

如上所示,「日期选择器」是一个可能存在于应用的多个页面中的组件示例。在测试用例的几十个测试中手动编写选择日期的浏览器自动化逻辑会很麻烦。因此,我们可以定义一个表示日期选择器的 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',
            '@month-list' => 'div > div.datepicker-months',
            '@day-list' => 'div > div.datepicker-days',
        ];
    }

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

使用组件

定义组件后,可以在任何测试中轻松选择日期选择器里的日期。并且,如果选择日期的逻辑改变了,我们只需要更新组件:

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 1.0

如果使用 CircleCI 1.0 运行 Dusk 测试,可以使用以下配置文件快速开始。与 TravisCI 一样,我们会使用 php artisan serve 命令启动 PHP 内置的 Web 服务器:

dependencies:
  pre:
      - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
      - sudo dpkg -i google-chrome.deb
      - sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
      - rm google-chrome.deb

test:
    pre:
        - "./vendor/laravel/dusk/bin/chromedriver-linux":
            background: true
        - cp .env.testing .env
        - "php artisan serve":
            background: true

    override:
        - php artisan dusk

CircleCI 2.0

如果使用 CircleCI 2.0 运行 Dusk 测试,可以在构建时添加这些步骤:

 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.1
cp .env.testing .env
composer install --no-interaction
nohup bash -c "./vendor/laravel/dusk/bin/chromedriver-linux 2>&1 &"
nohup bash -c "php artisan serve 2>&1 &" && sleep 5
php artisan dusk

Heroku CI

要在 Heroku CI 上运行 Dusk 测试,可以在 Heroku 的 app.json 文件中添加如下 Google Chrome 构建包和脚本:

{
  "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 测试,需要使用「启用 sudo」的 Ubuntu 14.04(Trusty)环境。由于 Travis CI 不是一个图形化环境,因此我们需要进行一些额外步骤启动 Chrome 浏览器。此外,我们会使用 php artisan serve 启动 PHP 内置的 Web 服务器:

language: php
sudo: required
dist: trusty

php:
  - 7.2

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

In your .env.testing file, adjust the value of APP_URL:

APP_URL=http://127.0.0.1:8000