Dusk

未匹配的标注
本文档最新版为 12.x,旧版本可能放弃维护,推荐阅读最新版!

Laravel Dusk

简介

[!WARNING]
Pest 4 现已支持浏览器自动化测试,相比 Laravel Dusk,在性能和易用性方面都有显著提升。对于新项目,我们推荐使用 Pest 进行浏览器测试。

Laravel Dusk 提供了清晰易用的浏览器自动化和测试 API。默认情况下,Dusk 不要求你在本机安装 JDK 或 Selenium,而是使用独立安装的 ChromeDriver。不过,你也可以使用任何其他兼容 Selenium 的驱动。

安装

首先,你应该安装 Google Chrome,并为项目添加 laravel/dusk Composer 依赖:

composer require laravel/dusk --dev

[!WARNING]
如果你手动注册 Dusk 的服务提供者,绝不要在生产环境中注册它,否则可能导致任意用户都能以已认证身份访问你的应用。

安装 Dusk 包后,执行 dusk:install Artisan 命令。该命令会创建 tests/Browser 目录和一个 Dusk 测试示例,并安装适用于当前操作系统的 Chrome Driver 二进制文件:

php artisan dusk:install

接下来,在应用的 .env 文件中设置 APP_URL 环境变量。该值应与你在浏览器中访问应用时使用的 URL 一致。

[!NOTE]
如果你使用 Laravel Sail 管理本地开发环境,也请参阅 Sail 文档中配置和运行 Dusk 测试的说明。

管理 ChromeDriver 安装

如果你想安装与 Laravel Dusk 的 dusk:install 命令所安装版本不同的 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

[!WARNING]
Dusk 要求 chromedriver 二进制文件具有可执行权限。如果运行 Dusk 时遇到问题,请使用以下命令确保这些文件可执行:chmod -R 0755 vendor/laravel/dusk/bin/。

使用其他浏览器

默认情况下,Dusk 使用 Google Chrome 和独立安装的 ChromeDriver 运行浏览器测试。不过,你也可以启动自己的 Selenium 服务器,并在任意浏览器上运行测试。

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

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

接下来,你可以修改 driver 方法,连接到所需的 URL 和端口。此外,还可以修改传递给 WebDriver 的「期望能力」配置:

use Facebook\WebDriver\Remote\RemoteWebDriver;

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

入门

生成测试

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

php artisan dusk:make LoginTest

每次测试后重置数据库

你编写的大多数测试都会与从应用数据库读取数据的页面交互。不过,Dusk 测试绝不应使用 RefreshDatabase trait。该 trait 使用数据库事务,而事务无法跨 HTTP 请求生效。你可以选择使用 DatabaseMigrations 或 DatabaseTruncation trait。

使用数据库迁移

DatabaseMigrations trait 会在每次测试前运行数据库迁移。不过,每次测试都删除并重新创建数据库表,通常比截断表更慢:

<?php

use Illuminate\Foundation\Testing\DatabaseMigrations;

pest()->use(DatabaseMigrations::class);

//
<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

    //
}

[!WARNING]
执行 Dusk 测试时不能使用 SQLite 内存数据库。浏览器在独立进程中运行,无法访问其他进程中的内存数据库。

使用数据库截断

DatabaseTruncation trait 会在第一个测试运行时执行数据库迁移,确保数据库表已正确创建。随后的测试则只会截断数据库表,相比重新运行所有数据库迁移,速度更快:

<?php

use Illuminate\Foundation\Testing\DatabaseTruncation;

pest()->use(DatabaseTruncation::class);

//
<?php

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class ExampleTest extends DuskTestCase
{
    use DatabaseTruncation;

    //
}

默认情况下,该 trait 会截断除 migrations 表之外的所有表。如果你想自定义需要截断的表,可以在测试类中定义 $tablesToTruncate 属性:

[!NOTE]
如果你使用 Pest,应该在基类 DuskTestCase 或测试文件所继承的其他类中定义这些属性或方法。

/**
 * 指定需要截断的表。
 *
 * @var array
 */
protected $tablesToTruncate = ['users'];

你也可以在测试类中定义 $exceptTables 属性,指定哪些表不需要截断:

/**
 * 指定不需要截断的表。
 *
 * @var array
 */
protected $exceptTables = ['users'];

要指定哪些数据库连接中的表需要截断,可以在测试类中定义 $connectionsToTruncate 属性:

/**
 * 指定需要截断其表的数据库连接。
 *
 * @var array
 */
protected $connectionsToTruncate = ['mysql'];

如果你想在截断数据库之前或之后执行代码,可以在测试类中定义 beforeTruncatingDatabase 或 afterTruncatingDatabase 方法:

/**
 * 执行数据库开始截断前的操作。
 */
protected function beforeTruncatingDatabase(): void
{
    //
}

/**
 * 执行数据库完成截断后的操作。
 */
protected function afterTruncatingDatabase(): void
{
    //
}

运行测试

要运行浏览器测试,请执行 dusk Artisan 命令:

php artisan dusk

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

php artisan dusk:fails

dusk 命令接受 Pest / PHPUnit 测试运行器通常支持的所有参数,例如只运行指定分组中的测试:

php artisan dusk --group=foo

[!NOTE]
如果你使用 Laravel Sail 管理本地开发环境,请参阅 Sail 文档中配置和运行 Dusk 测试的说明。

手动启动 ChromeDriver

默认情况下,Dusk 会尝试自动启动 ChromeDriver。如果这在你的系统上无法正常工作,可以在运行 dusk 命令前手动启动 ChromeDriver。此时,你应该注释掉 tests/DuskTestCase.php 文件中的以下代码行:

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

此外,如果你启动 ChromeDriver 时使用了 9515 以外的端口,应该修改同一类中的 driver 方法,使用正确的端口:

use Facebook\WebDriver\Remote\RemoteWebDriver;

/**
 * 创建 RemoteWebDriver 实例。
 */
protected function driver(): RemoteWebDriver
{
    return RemoteWebDriver::create(
        'http://localhost:9515', DesiredCapabilities::chrome()
    );
}

环境处理

要让 Dusk 在运行测试时使用独立的环境文件,请在项目根目录创建 .env.dusk.{environment} 文件。例如,如果你将在 local 环境中执行 dusk 命令,就应该创建 .env.dusk.local 文件。

运行测试时,Dusk 会备份 .env 文件,并将 Dusk 环境文件重命名为 .env。测试完成后,原来的 .env 文件会恢复。

浏览器基础

创建浏览器

首先,让我们编写一个测试,验证能否登录应用。生成测试后,可以修改它,使浏览器访问登录页面、输入凭据并点击「Login」按钮。要创建浏览器实例,可以在 Dusk 测试中调用 browse 方法:

<?php

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;

pest()->use(DatabaseMigrations::class);

test('basic example', function () {
    $user = User::factory()->create([
        'email' => 'taylor@laravel.com',
    ]);

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

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

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

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

如上例所示,browse 方法接收一个闭包。Dusk 会自动向闭包传入浏览器实例,它是用于与应用交互并进行断言的主要对象。

创建多个浏览器

有时,你需要多个浏览器才能正确执行测试。例如,测试通过 WebSocket 交互的聊天界面时,可能就需要多个浏览器。要创建多个浏览器,只需为传递给 browse 方法的闭包添加更多浏览器参数:

$this->browse(function (Browser $first, Browser $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');
});

导航

你可以使用 visit 方法,访问应用中给定的 URI:

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

你可以使用 visitRoute 方法,访问命名路由:

$browser->visitRoute($routeName, $parameters);

你可以使用 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 方法。通常,应该在服务提供者的 boot 方法中调用该方法:

<?php

namespace App\Providers;

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

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

            return $this;
        });
    }
}

macro 函数的第一个参数是名称,第二个参数是闭包。当你在 Browser 实例上将宏作为方法调用时,就会执行这个闭包:

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

身份验证

你经常需要测试要求身份验证的页面。使用 Dusk 的 loginAs 方法,可以避免在每次测试中都操作应用的登录界面。该方法接收可认证模型的主键,或一个可认证模型实例:

use App\Models\User;
use Laravel\Dusk\Browser;

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

[!WARNING]
使用 loginAs 方法后,用户会话会在该文件内的所有测试中保持。

Cookie

你可以使用 cookie 方法获取或设置加密 Cookie 的值。默认情况下,Laravel 创建的所有 Cookie 都是加密的:

$browser->cookie('name');

$browser->cookie('name', 'Taylor');

你可以使用 plainCookie 方法获取或设置未加密 Cookie 的值:

$browser->plainCookie('name');

$browser->plainCookie('name', 'Taylor');

你可以使用 deleteCookie 方法删除给定的 Cookie:

$browser->deleteCookie('name');

执行 JavaScript

你可以使用 script 方法,在浏览器中执行任意 JavaScript 语句:

$browser->script('document.documentElement.scrollTop = 0');

$browser->script([
    'document.body.scrollTop = 0',
    'document.documentElement.scrollTop = 0',
]);

$output = $browser->script('return window.location.pathname');

截图

你可以使用 screenshot 方法截图,并以给定的文件名保存。所有截图都会存储在 tests/Browser/screenshots 目录中:

$browser->screenshot('filename');

你可以使用 responsiveScreenshots 方法,在不同断点尺寸下截取一组截图:

$browser->responsiveScreenshots('filename');

你可以使用 screenshotElement 方法,为页面上的特定元素截图:

$browser->screenshotElement('#selector', 'filename');

将控制台输出保存到磁盘

你可以使用 storeConsoleLog 方法,以给定的文件名将当前浏览器的控制台输出写入磁盘。控制台输出会保存在 tests/Browser/console 目录中:

$browser->storeConsoleLog('filename');

将页面源码保存到磁盘

你可以使用 storeSource 方法,以给定的文件名将当前页面源码写入磁盘。页面源码会保存在 tests/Browser/source 目录中:

$browser->storeSource('filename');

与元素交互

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>
// 测试代码……

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

如有需要,你可以通过 selectorHtmlAttribute 方法,自定义 Dusk 选择器使用的 HTML 属性。通常,应该在应用 AppServiceProvider 的 boot 方法中调用该方法:

use Laravel\Dusk\Dusk;

Dusk::selectorHtmlAttribute('data-dusk');

文本、值和属性

获取和设置值

Dusk 提供了多个方法,用于操作页面元素的当前值、显示文本和属性。例如,要获取与给定 CSS 或 Dusk 选择器匹配的元素的「值」,可以使用 value 方法:

// 获取值……
$value = $browser->value('selector');

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

你可以使用 inputValue 方法,获取具有给定字段名称的输入元素的「值」:

$value = $browser->inputValue('field');

获取文本

你可以使用 text 方法,获取与给定选择器匹配的元素的显示文本:

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

获取属性

最后,你可以使用 attribute 方法,获取与给定选择器匹配的元素的某个属性值:

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

与表单交互

输入值

Dusk 提供了多种方法,用于操作表单和输入元素。首先,来看一个在输入框中输入文本的示例:

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

请注意,虽然 type 方法在需要时接收 CSS 选择器,但并非必须传入。如果没有提供 CSS 选择器,Dusk 会查找 name 属性与给定名称匹配的 input 或 textarea 字段。

要在不清空字段内容的情况下追加文本,可以使用 append 方法:

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

你可以使用 clear 方法清空输入框的值:

$browser->clear('email');

你可以使用 typeSlowly 方法,让 Dusk 缓慢输入。默认情况下,每次按键之间会暂停 100 毫秒。要自定义按键间隔,可以将所需的毫秒数作为第三个参数传入:

$browser->typeSlowly('mobile', '+1 (202) 555-5555');

$browser->typeSlowly('mobile', '+1 (202) 555-5555', 300);

你可以使用 appendSlowly 方法缓慢追加文本:

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

下拉列表

要选择 select 元素中的某个值,可以使用 select 方法。与 type 方法一样,select 方法不要求提供完整的 CSS 选择器。传入的值应该是选项实际的值,而非显示文本:

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

省略第二个参数时,可以随机选择一个选项:

$browser->select('size');

将数组作为第二个参数传递给 select 方法,可以选择多个选项:

$browser->select('categories', ['Art', 'Music']);

复选框

你可以使用 check 方法勾选复选框。与其他许多输入相关的方法一样,该方法不要求提供完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 会查找 name 属性匹配的复选框:

$browser->check('terms');

你可以使用 uncheck 方法取消勾选复选框:

$browser->uncheck('terms');

单选按钮

你可以使用 radio 方法选中 radio 输入选项。与其他许多输入相关的方法一样,该方法不要求提供完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 会查找 name 和 value 属性均匹配的 radio 输入元素:

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

附加文件

你可以使用 attach 方法,为 file 输入元素附加文件。与其他许多输入相关的方法一样,该方法不要求提供完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 会查找 name 属性匹配的 file 输入元素:

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

[!WARNING]
attach 函数要求服务器已安装并启用 PHP 的 Zip 扩展。

按下按钮

你可以使用 press 方法点击页面上的按钮元素。传入的参数可以是按钮的显示文本,也可以是 CSS / Dusk 选择器:

$browser->press('Login');

提交表单时,许多应用会在提交按钮被点击后禁用它,并在提交表单的 HTTP 请求完成后重新启用。要点击按钮并等待它重新启用,可以使用 pressAndWaitFor 方法:

// 点击按钮,最多等待 5 秒,直到它重新启用……
$browser->pressAndWaitFor('Save');

// 点击按钮,最多等待 1 秒,直到它重新启用……
$browser->pressAndWaitFor('Save', 1);

点击链接

你可以使用浏览器实例的 clickLink 方法点击链接。该方法会点击具有给定显示文本的链接:

$browser->clickLink($linkText);

你可以使用 seeLink 方法,判断页面上是否有具有给定显示文本的可见链接:

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

[!WARNING]
这些方法依赖 jQuery。如果页面中没有 jQuery,Dusk 会自动将其注入页面,以便在测试期间使用。

使用键盘

相比 type 方法,keys 方法允许你向给定元素发送更复杂的输入序列。例如,可以让 Dusk 在输入值时按住修饰键。下例中,向匹配给定选择器的元素输入 taylor 时会按住 shift 键;输入完成后,再不带任何修饰键地输入 swift:

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

keys 方法的另一个实用场景,是向应用的主 CSS 选择器发送「键盘快捷键」组合:

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

[!NOTE]
所有修饰键(如 {command})都用 {} 字符包裹,并对应于 Facebook\WebDriver\WebDriverKeys 类中定义的常量。你可以在 GitHub 上查看这些常量。

链式键盘交互

Dusk 还提供了 withKeyboard 方法,让你通过 Laravel\Dusk\Keyboard 类,以链式调用执行复杂的键盘交互。Keyboard 类提供了 press、release、type 和 pause 方法:

use Laravel\Dusk\Keyboard;

$browser->withKeyboard(function (Keyboard $keyboard) {
    $keyboard->press('c')
        ->pause(1000)
        ->release('c')
        ->type(['c', 'e', 'o']);
});

键盘宏

如果你想定义可在整个测试套件中轻松复用的自定义键盘交互,可以使用 Keyboard 类提供的 macro 方法。通常,应该在服务提供者的 boot 方法中调用该方法:

<?php

namespace App\Providers;

use Facebook\WebDriver\WebDriverKeys;
use Illuminate\Support\ServiceProvider;
use Laravel\Dusk\Keyboard;
use Laravel\Dusk\OperatingSystem;

class DuskServiceProvider extends ServiceProvider
{
    /**
     * 注册 Dusk 浏览器宏。
     */
    public function boot(): void
    {
        Keyboard::macro('copy', function (string $element = null) {
            $this->type([
                OperatingSystem::onMac() ? WebDriverKeys::META : WebDriverKeys::CONTROL, 'c',
            ]);

            return $this;
        });

        Keyboard::macro('paste', function (string $element = null) {
            $this->type([
                OperatingSystem::onMac() ? WebDriverKeys::META : WebDriverKeys::CONTROL, 'v',
            ]);

            return $this;
        });
    }
}

macro 函数的第一个参数是名称,第二个参数是闭包。当你在 Keyboard 实例上将宏作为方法调用时,就会执行这个闭包:

$browser->click('@textarea')
    ->withKeyboard(fn (Keyboard $keyboard) => $keyboard->copy())
    ->click('@another-textarea')
    ->withKeyboard(fn (Keyboard $keyboard) => $keyboard->paste());

使用鼠标

点击元素

你可以使用 click 方法,点击与给定 CSS 或 Dusk 选择器匹配的元素:

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

你可以使用 clickAtXPath 方法,点击与给定 XPath 表达式匹配的元素:

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

你可以使用 clickAtPoint 方法,点击浏览器可视区域内给定坐标处最上层的元素:

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

你可以使用 doubleClick 方法模拟鼠标双击:

$browser->doubleClick();

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

你可以使用 rightClick 方法模拟鼠标右键点击:

$browser->rightClick();

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

你可以使用 clickAndHold 方法模拟按住鼠标按钮。随后调用 releaseMouse 方法,即可结束该操作并释放鼠标按钮:

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

$browser->clickAndHold()
    ->pause(1000)
    ->releaseMouse();

你可以使用 controlClick 方法,模拟浏览器中的 ctrl+click 事件:

$browser->controlClick();

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

你可以使用 clickWhenVisible 或 clickWhenEnabled 方法,等待元素准备就绪后只点击一次:

$browser->clickWhenVisible('@save-button');
$browser->clickWhenEnabled('@submit-button');

鼠标悬停

需要将鼠标移到与给定 CSS 或 Dusk 选择器匹配的元素上时,可以使用 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');

要通过点击「确定」按钮关闭已打开的 JavaScript 对话框,可以调用 acceptDialog 方法:

$browser->acceptDialog();

要通过点击「取消」按钮关闭已打开的 JavaScript 对话框,可以调用 dismissDialog 方法:

$browser->dismissDialog();

与内嵌框架交互

如果你需要与 iframe 中的元素交互,可以使用 withinFrame 方法。传入闭包中的所有元素交互,都会限定在指定 iframe 的上下文中:

$browser->withinFrame('#credit-card-details', function ($browser) {
    $browser->type('input[name="cardnumber"]', '4242424242424242')
        ->type('input[name="exp-date"]', '1224')
        ->type('input[name="cvc"]', '123')
        ->press('Pay');
});

限定选择器作用域

有时,你可能希望在给定选择器的范围内执行多项操作。例如,断言某些文本在表格内存在,然后点击该表格中的按钮。你可以使用 with 方法实现这一点。传入闭包中的所有操作,都会限定在原始选择器的范围内:

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

有时,你可能需要在当前作用域之外执行断言。此时可以使用 elsewhere 和 elsewhereWhenAvailable 方法:

$browser->with('.table', function (Browser $table) {
    // 当前作用域为 `body .table`……

    $browser->elsewhere('.page-title', function (Browser $title) {
        // 当前作用域为 `body .page-title`……
        $title->assertSee('Hello World');
    });

    $browser->elsewhereWhenAvailable('.page-title', function (Browser $title) {
        // 当前作用域为 `body .page-title`……
        $title->assertSee('Hello World');
    });
});

等待元素

测试大量使用 JavaScript 的应用时,通常需要等待某些元素或数据就绪后,才能继续执行测试。Dusk 让这变得很简单。你可以通过多种方法,等待页面上的元素变为可见,甚至等待给定 JavaScript 表达式的结果变为 true。

等待

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

$browser->pause(1000);

如果只想在给定条件为 true 时暂停测试,可以使用 pauseIf 方法:

$browser->pauseIf(App::environment('production'), 1000);

同样,如果想在给定条件不为 true 时暂停测试,可以使用 pauseUnless 方法:

$browser->pauseUnless(App::environment('testing'), 1000);

等待选择器

你可以使用 waitFor 方法暂停测试,直到与给定 CSS 或 Dusk 选择器匹配的元素显示在页面上。默认最多等待五秒,超时后抛出异常。如有需要,可以将自定义超时值作为第二个参数传入:

// 最多等待五秒,直到匹配的元素显示……
$browser->waitFor('.selector');

// 最多等待一秒,直到匹配的元素显示……
$browser->waitFor('.selector', 1);

你也可以等待与给定选择器匹配的元素包含给定文本:

// 最多等待五秒,直到匹配的元素包含给定文本……
$browser->waitForTextIn('.selector', 'Hello World');

// 最多等待一秒,直到匹配的元素包含给定文本……
$browser->waitForTextIn('.selector', 'Hello World', 1);

你也可以等待与给定选择器匹配的元素从页面中消失:

// 最多等待五秒,直到匹配的元素消失……
$browser->waitUntilMissing('.selector');

// 最多等待一秒,直到匹配的元素消失……
$browser->waitUntilMissing('.selector', 1);

或者,你可以等待与给定选择器匹配的元素启用或禁用:

// 最多等待五秒,直到匹配的元素启用……
$browser->waitUntilEnabled('.selector');

// 最多等待一秒,直到匹配的元素启用……
$browser->waitUntilEnabled('.selector', 1);

// 最多等待五秒,直到匹配的元素禁用……
$browser->waitUntilDisabled('.selector');

// 最多等待一秒,直到匹配的元素禁用……
$browser->waitUntilDisabled('.selector', 1);

元素就绪后限定选择器作用域

有时,你可能想先等待与给定选择器匹配的元素出现,再与其交互。例如,等待模态窗口就绪后,点击其中的「确定」按钮。你可以使用 whenAvailable 方法实现这一点。传入闭包中的所有元素操作,都会限定在原始选择器的范围内:

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

等待文本

你可以使用 waitForText 方法,等待给定文本显示在页面上:

// 最多等待五秒,直到文本显示……
$browser->waitForText('Hello World');

// 最多等待一秒,直到文本显示……
$browser->waitForText('Hello World', 1);

你可以使用 waitUntilMissingText 方法,等待已显示的文本从页面中移除:

// 最多等待五秒,直到文本移除……
$browser->waitUntilMissingText('Hello World');

// 最多等待一秒,直到文本移除……
$browser->waitUntilMissingText('Hello World', 1);

等待链接

你可以使用 waitForLink 方法,等待给定的链接文本显示在页面上:

// 最多等待五秒,直到链接显示……
$browser->waitForLink('Create');

// 最多等待一秒,直到链接显示……
$browser->waitForLink('Create', 1);

等待输入框

你可以使用 waitForInput 方法,等待给定输入框在页面上变为可见:

// 最多等待五秒,直到输入框可见……
$browser->waitForInput($field);

// 最多等待一秒,直到输入框可见……
$browser->waitForInput($field, 1);

等待页面地址

执行 $browser->assertPathIs('/home') 这样的路径断言时,如果 window.location.pathname 正在异步更新,断言可能会失败。你可以使用 waitForLocation 方法,等待地址变为给定值:

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

waitForLocation 方法也可用于等待当前窗口地址变为完整的 URL:

$browser->waitForLocation('https://example.com/path');

你也可以等待命名路由对应的地址:

$browser->waitForRoute($routeName, $parameters);

等待页面重新加载

如果你需要在执行某项操作后等待页面重新加载,可以使用 waitForReload 方法:

use Laravel\Dusk\Browser;

$browser->waitForReload(function (Browser $browser) {
    $browser->press('Submit');
})
->assertSee('Success!');

等待页面重新加载通常发生在点击按钮之后,因此你可以使用更便捷的 clickAndWaitForReload 方法:

$browser->clickAndWaitForReload('.selector')
    ->assertSee('something');

等待 JavaScript 表达式

有时,你可能想暂停测试,直到给定 JavaScript 表达式的结果为 true。使用 waitUntil 方法即可轻松实现。向该方法传入表达式时,无需包含 return 关键字或末尾的分号:

// 最多等待五秒,直到表达式的结果为 true……
$browser->waitUntil('App.data.servers.length > 0');

// 最多等待一秒,直到表达式的结果为 true……
$browser->waitUntil('App.data.servers.length > 0', 1);

等待 Vue 表达式

你可以使用 waitUntilVue 和 waitUntilVueIsNot 方法,分别等待 Vue 组件的某个属性等于或不等于给定值:

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

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

等待 JavaScript 事件

你可以使用 waitForEvent 方法暂停测试,直到 JavaScript 事件发生:

$browser->waitForEvent('load');

事件监听器会附加到当前作用域,默认为 body 元素。使用限定作用域的选择器时,事件监听器会附加到匹配的元素:

$browser->with('iframe', function (Browser $iframe) {
    // 等待 iframe 的 load 事件……
    $iframe->waitForEvent('load');
});

你也可以将选择器作为第二个参数传递给 waitForEvent 方法,将事件监听器附加到特定元素:

$browser->waitForEvent('load', '.selector');

你也可以等待 document 和 window 对象上的事件:

// 等待文档滚动……
$browser->waitForEvent('scroll', 'document');

// 最多等待五秒,直到窗口大小发生变化……
$browser->waitForEvent('resize', 'window', 5);

使用回调等待

Dusk 的许多「等待」方法依赖底层的 waitUsing 方法。你可以直接使用该方法,等待给定闭包返回 true。waitUsing 方法接收最大等待秒数、闭包执行的间隔、闭包本身,以及可选的失败消息:

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

将元素滚动到可视范围

有时,你无法点击某个元素,是因为它位于浏览器的可视区域之外。scrollIntoView 方法会滚动浏览器窗口,直到给定选择器对应的元素进入可视区域:

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

可用断言

Dusk 提供了多种用于测试应用的断言。所有可用断言如下所列:

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('/home');

assertPathEndsWith

断言当前 URL 路径以给定路径结尾:

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

assertPathContains

断言当前 URL 路径包含给定路径:

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

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

断言 URL 当前的哈希片段与给定片段匹配:

$browser->assertFragmentIs('anchor');

assertFragmentBeginsWith

断言 URL 当前的哈希片段以给定片段开头:

$browser->assertFragmentBeginsWith('anchor');

assertFragmentIsNot

断言 URL 当前的哈希片段与给定片段不匹配:

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

assertCount

断言与给定选择器匹配的元素出现了指定次数:

$browser->assertCount($selector, $count);

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

断言给定输入字段具有给定值:

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

assertInputValueIsNot

断言给定输入字段的值不等于给定值:

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

assertChecked

断言给定复选框已勾选:

$browser->assertChecked($field);

assertNotChecked

断言给定复选框未勾选:

$browser->assertNotChecked($field);

assertIndeterminate

断言给定复选框处于不确定状态:

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

assertSelectMissingOption

断言给定值不可供选择:

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

assertValue

断言与给定选择器匹配的元素具有给定值:

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

assertValueIsNot

断言与给定选择器匹配的元素的值不等于给定值:

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

assertAttribute

断言与给定选择器匹配的元素,其指定属性具有给定值:

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

assertAttributeMissing

断言与给定选择器匹配的元素不存在指定属性:

$browser->assertAttributeMissing($selector, $attribute);

assertAttributeContains

断言与给定选择器匹配的元素,其指定属性包含给定值:

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

assertAttributeDoesntContain

断言与给定选择器匹配的元素,其指定属性不包含给定值:

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

assertAriaAttribute

断言与给定选择器匹配的元素,其指定的 aria 属性具有给定值:

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

例如,对于标记 <button aria-label="Add"></button>,你可以像下面这样对 aria-label 属性进行断言:

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

assertDataAttribute

断言与给定选择器匹配的元素,其指定的 data 属性具有给定值:

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

例如,对于标记 <tr id="row-1" data-content="attendees"></tr>,你可以像下面这样对 data-content 属性进行断言:

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

assertVisible

断言与给定选择器匹配的元素可见:

$browser->assertVisible($selector);

assertPresent

断言页面源码中存在与给定选择器匹配的元素:

$browser->assertPresent($selector);

assertNotPresent

断言页面源码中不存在与给定选择器匹配的元素:

$browser->assertNotPresent($selector);

assertMissing

断言与给定选择器匹配的元素不可见:

$browser->assertMissing($selector);

assertInputPresent

断言具有给定名称的输入元素存在:

$browser->assertInputPresent($name);

assertInputMissing

断言页面源码中不存在具有给定名称的输入元素:

$browser->assertInputMissing($name);

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

Dusk 甚至允许你对 Vue 组件的数据状态进行断言。例如,假设应用中包含以下 Vue 组件:

// HTML 示例……

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

// 组件定义……

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

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

你可以像下面这样,对 Vue 组件的状态进行断言:

test('vue', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
            ->assertVue('user.name', 'Taylor', '@profile-component');
    });
});
/**
 * 一个基本的 Vue 测试示例。
 */
public function test_vue(): void
{
    $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);

assertVueDoesntContain

断言给定 Vue 组件的数据属性为数组,且不包含给定值:

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

页面

有时,测试需要按顺序执行多项复杂操作,这会让测试难以阅读和理解。Dusk 页面允许你定义清晰易读的操作,随后通过单个方法在指定页面上执行。页面还允许你为整个应用或单个页面的常用选择器定义简写。

生成页面

要生成页面对象,请执行 dusk:page Artisan 命令。所有页面对象都会放在应用的 tests/Browser/Pages 目录中:

php artisan dusk:page Login

配置页面

默认情况下,页面有三个方法:url、assert 和 elements。下面先介绍 url 和 assert 方法,elements 方法会在后文详细介绍。

url 方法

url 方法应该返回该页面对应的 URL 路径。Dusk 在浏览器中导航到该页面时,会使用这个 URL:

/**
 * 获取页面的 URL。
 */
public function url(): string
{
    return '/login';
}

assert 方法

assert 方法可以执行必要的断言,验证浏览器确实位于给定页面上。该方法可以留空,也可以根据需要加入断言。这些断言会在导航到该页面时自动执行:

/**
 * 断言浏览器位于该页面。
 */
public function assert(Browser $browser): void
{
    $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<string, string>
 */
public function elements(): array
{
    return [
        '@email' => 'input[name=email]',
    ];
}

定义简写后,你可以在任何通常使用完整 CSS 选择器的地方使用该简写选择器:

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

全局简写选择器

安装 Dusk 后,tests/Browser/Pages 目录中会生成一个 Page 基类。该类包含 siteElements 方法,可用于定义在整个应用的每个页面中都可用的全局简写选择器:

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

页面方法

除了页面上默认定义的方法,你还可以定义其他方法,供测试复用。例如,假设我们正在构建一个音乐管理应用,某个页面上的常见操作是创建播放列表。你可以在页面类中定义 createPlaylist 方法,避免在每个测试中重复编写创建播放列表的逻辑:

<?php

namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;

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

    /**
     * 创建新的播放列表。
     */
    public function createPlaylist(Browser $browser, string $name): void
    {
        $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 的「页面对象」,但用于在整个应用中复用的界面和功能片段,例如导航栏或通知窗口。因此,组件不绑定特定的 URL。

生成组件

要生成组件,请执行 dusk:component Artisan 命令。新组件会放在 tests/Browser/Components 目录中:

php artisan dusk:component DatePicker

如上所示,「日期选择器」就是一种可能出现在应用多个页面中的组件。如果在整个测试套件的数十个测试中,都手动编写选择日期的浏览器自动化逻辑,会很繁琐。我们可以定义一个代表日期选择器的 Dusk 组件,将这些逻辑封装在组件中:

<?php

namespace Tests\Browser\Components;

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

class DatePicker extends BaseComponent
{
    /**
     * 获取组件的根选择器。
     */
    public function selector(): string
    {
        return '.date-picker';
    }

    /**
     * 断言浏览器页面包含该组件。
     */
    public function assert(Browser $browser): void
    {
        $browser->assertVisible($this->selector());
    }

    /**
     * 获取组件的元素简写。
     *
     * @return array<string, string>
     */
    public function elements(): array
    {
        return [
            '@date-field' => 'input.datepicker-input',
            '@year-list' => 'div > div.datepicker-years',
            '@month-list' => 'div > div.datepicker-months',
            '@day-list' => 'div > div.datepicker-days',
        ];
    }

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

使用组件

定义组件后,我们就可以在任意测试中,轻松地通过日期选择器选择日期。如果选择日期的逻辑发生变化,只需更新组件即可:

<?php

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\Browser\Components\DatePicker;

pest()->use(DatabaseMigrations::class);

test('basic example', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
            ->within(new DatePicker, function (Browser $browser) {
                $browser->selectDate(2019, 1, 30);
            })
            ->assertSee('January');
    });
});
<?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
{
    /**
     * 一个基本的组件测试示例。
     */
    public function test_basic_example(): void
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
                ->within(new DatePicker, function (Browser $browser) {
                    $browser->selectDate(2019, 1, 30);
                })
                ->assertSee('January');
        });
    }
}

你可以使用 component 方法,获取作用域限定在给定组件中的浏览器实例:

$datePicker = $browser->component(new DatePickerComponent);

$datePicker->selectDate(2019, 1, 30);

$datePicker->assertSee('January');

持续集成

[!WARNING]
大多数 Dusk 持续集成配置都要求 Laravel 应用通过 PHP 内置开发服务器在 8000 端口提供服务。因此,继续之前,请确保持续集成环境的 APP_URL 环境变量值为 http://127.0.0.1:8000。

Heroku CI

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

{
  "environments": {
    "test": {
      "buildpacks": [
        { "url": "heroku/php" },
        { "url": "https://github.com/heroku/heroku-buildpack-chrome-for-testing" }
      ],
      "scripts": {
        "test-setup": "cp .env.testing .env",
        "test": "nohup bash -c './vendor/laravel/dusk/bin/chromedriver-linux --port=9515 > /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 服务器:

language: php

php:
  - 8.2

addons:
  chrome: stable

install:
  - cp .env.testing .env
  - travis_retry composer install --no-interaction --prefer-dist
  - 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

如果你使用 GitHub Actions 运行 Dusk 测试,可以将以下配置文件作为起点。与 Travis CI 一样,我们将使用 php artisan serve 命令启动 PHP 内置 Web 服务器:

name: CI
on: [push]
jobs:

  dusk-php:
    runs-on: ubuntu-latest
    env:
      APP_URL: "http://127.0.0.1:8000"
      DB_USERNAME: root
      DB_PASSWORD: root
      MAIL_MAILER: log
    steps:
      - uses: actions/checkout@v5
      - 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 --prefer-dist --optimize-autoloader
      - name: Generate Application Key
        run: php artisan key:generate
      - name: Upgrade Chrome Driver
        run: php artisan dusk:chrome-driver --detect
      - name: Start Chrome Driver
        run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=9515 &
      - name: Run Laravel Server
        run: php artisan serve --no-reload &
      - name: Run Dusk Tests
        run: php artisan dusk
      - name: Upload Screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: screenshots
          path: tests/Browser/screenshots
      - name: Upload Console Logs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: console
          path: tests/Browser/console

Chipper CI

如果你使用 Chipper CI 运行 Dusk 测试,可以将以下配置文件作为起点。我们将使用 PHP 内置服务器运行 Laravel,以便监听请求:

# 文件 .chipperci.yml
version: 1

environment:
  php: 8.2
  node: 16

# 在构建环境中包含 Chrome
services:
  - dusk

# 构建所有提交
on:
   push:
      branches: .*

pipeline:
  - name: Setup
    cmd: |
      cp -v .env.example .env
      composer install --no-interaction --prefer-dist --optimize-autoloader
      php artisan key:generate

      # 创建 Dusk 环境文件,确保 APP_URL 使用 BUILD_HOST
      cp -v .env .env.dusk.ci
      sed -i "s@APP_URL=.*@APP_URL=http://$BUILD_HOST:8000@g" .env.dusk.ci

  - name: Compile Assets
    cmd: |
      npm ci --no-audit
      npm run build

  - name: Browser Tests
    cmd: |
      php -S [::0]:8000 -t public 2>server.log &
      sleep 2
      php artisan dusk:chrome-driver $CHROME_DRIVER
      php artisan dusk --env=ci

要进一步了解如何在 Chipper CI 上运行 Dusk 测试(包括如何使用数据库),请参阅 Chipper CI 官方文档。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/13.x/du...

译文地址:https://learnku.com/docs/laravel/13.x/du...

上一篇 下一篇
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
贡献者:1
讨论数量: 2
发起讨论 只看当前版本


Leokey
Laradock使用dusk测试报错
0 个点赞 | 2 个回复 | 问答 | 课程版本 9.x
Mr-houzi
PHP artisan dusk 是空页面如何解决?
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.8