简介

Pest 4 现在包含了自动化浏览器测试功能,相较于 Laravel Dusk,它在性能和易用性方面有了显著提升。对于新项目,我们建议使用 Pest 进行浏览器测试。

Laravel Dusk 提供了一个直观、易于使用的浏览器自动化与测试 API。默认情况下,Dusk 无需你在本地计算机上安装 JDK 或 Selenium。相反,Dusk 使用的是独立的 ChromeDriver 安装。不过,你也可以自由选用任何其他兼容 Selenium 的驱动程序。

安装

若要开始使用,你应安装 Google Chrome,并将 laravel/dusk Composer 依赖添加到你的项目中:

composer require laravel/dusk --dev

如果你手动注册 Dusk 的服务提供者,那么绝不能在生产环境中注册它,否则可能会导致任意用户能够通过你的应用程序进行身份验证。

安装完 Dusk 包后,执行 dusk:install Artisan 命令。dusk:install 命令会创建 tests/Browser 目录、一个 Dusk 测试示例,并为你所使用的操作系统安装 Chrome Driver 二进制文件:

php artisan dusk:install

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

如果你使用 Laravel Sail 来管理本地开发环境,请同时查阅关于配置和运行 Dusk 测试的 Sail 文档。

管理 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

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 的“期望 capabilities”:

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。RefreshDatabase trait 利用了数据库事务,而在 HTTP 请求之间,这些事务不适用或不可用。因此,你有两种选择:DatabaseMigrations trait 和 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;

    //
}

执行 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 属性:

如果你使用的是 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

如果你使用 Laravel Sail 来管理本地开发环境,请查阅关于配置和运行 Dusk 测试的 Sail 文档。

手动启动 ChromeDriver

默认情况下,Dusk 会自动尝试启动 ChromeDriver。如果你的系统无法自动启动,你可以在运行 dusk 命令之前手动启动 ChromeDriver。如果你选择手动启动 ChromeDriver,应注释掉 tests/DuskTestCase.php 文件中的以下一行:

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

此外,如果你在非 9515 端口启动 ChromeDriver,应修改同一类的 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 文件将被恢复。

浏览器基础

创建浏览器

首先,让我们编写一个测试来验证我们可以登录应用程序。生成测试后,我们可以对其进行修改,以导航到登录页面、输入一些凭据并点击“登录”按钮。要创建浏览器实例,你可以在 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' => '[email protected]',
    ]);

    $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' => '[email protected]',
        ]);

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

如上面的示例所示,browse 方法接受一个闭包。Dusk 会自动将一个浏览器实例传递给该闭包,它是用于与你的应用程序进行交互并对其做出断言的主要对象。

创建多个浏览器

有时你可能需要多个浏览器才能正确执行测试。例如,测试与 websockets 交互的聊天屏幕可能需要多个浏览器。要创建多个浏览器,只需在传递给 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 方法,以避免在每次测试时都与应用程序的登录界面进行交互。loginAs 方法接受与你的身份验证模型关联的主键或身份验证模型实例:

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

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

使用 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>
// 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');

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

use Laravel\Dusk\Dusk;

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

文本、值与属性

获取与设置值

Dusk 提供了多种方法,用于与页面上元素的当前值、显示文本和属性交互。例如,要获取匹配给定 CSS 或 Dusk 选择器的元素的值,请使用 value 方法:

// Retrieve the value...
$value = $browser->value('selector');

// Set the value...
$browser->value('selector', 'value');

你可以使用 inputValue 方法来获取具有指定字段名的输入元素的值:

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

获取文本

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

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

获取属性

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

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

与表单交互

输入值

Dusk 提供了多种与表单及输入元素交互的方法。首先,让我们看一下在输入字段中输入文本的示例:

$browser->type('email', '[email protected]');

请注意,尽管该方法在必要时可以接受 CSS 选择器,但并非必须向 type 方法传入 CSS 选择器。如果未提供 CSS 选择器,Dusk 将查找具有给定 name 属性的 input 或 textarea 字段。

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

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

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

$browser->clear('email');

你可以使用 typeSlowly 方法指示 Dusk 缓慢输入。默认情况下,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 选择器。向 select 方法传值时,应传入底层选项的值而非显示文本:

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

附加功能需要服务器上已安装并启用 PHP 的 Zip 扩展。

按下按钮

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

$browser->press('Login');

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

// Press the button and wait a maximum of 5 seconds for it to be enabled...
$browser->pressAndWaitFor('Save');

// Press the button and wait a maximum of 1 second for it to be enabled...
$browser->pressAndWaitFor('Save', 1);

点击链接

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

$browser->clickLink($linkText);

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

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

这些方法需要与 jQuery 交互。如果页面上没有 jQuery,Dusk 会自动将其注入到页面中,以便在测试期间可用。

使用键盘

通过 keys 方法,你可以向给定元素提供比 type 方法通常允许的更为复杂的输入序列。例如,你可以指示 Dusk 在输入值时按住修饰键。在本示例中,在向匹配给定选择器的元素输入 taylor 时会按住 shift 键。输入 taylor 后,将不使用任何修饰键输入 swift:

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

keys 方法的另一个常见用例是向应用的主要 CSS 选择器发送“键盘快捷键”组合:

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

所有修饰键(如 {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
{
    /**
     * Register Dusk's browser macros.
     */
    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 方法。传递给 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 方法来实现这一点。传递给 with 方法的闭包内执行的所有操作,都将限定在原始选择器的范围内:

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

你偶尔可能需要在当前作用域之外执行断言。可以使用 elsewhere 和 elsewhereWhenAvailable 方法来实现这一点:

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

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

    $browser->elsewhereWhenAvailable('.page-title', function (Browser $title) {
        // Current scope is `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 assertTitleContains assertUrlIs assertSchemeIs assertSchemeIsNot assertHostIs assertHostIsNot assertPortIs assertPortIsNot assertPathBeginsWith assertPathEndsWith assertPathContains assertPathIs assertPathIsNot assertRouteIs assertQueryStringHas assertQueryStringMissing assertFragmentIs assertFragmentBeginsWith assertFragmentIsNot assertHasCookie assertHasPlainCookie assertCookieMissing assertPlainCookieMissing assertCookieValue assertPlainCookieValue assertSee assertDontSee assertSeeIn assertDontSeeIn assertSeeAnythingIn assertSeeNothingIn assertCount assertScript assertSourceHas assertSourceMissing assertSeeLink assertDontSeeLink assertInputValue assertInputValueIsNot assertChecked assertNotChecked assertIndeterminate assertRadioSelected assertRadioNotSelected assertSelected assertNotSelected assertSelectHasOptions assertSelectMissingOptions assertSelectHasOption assertSelectMissingOption assertValue assertValueIsNot assertAttribute assertAttributeMissing assertAttributeContains assertAttributeDoesntContain assertAriaAttribute assertDataAttribute assertVisible assertPresent assertNotPresent assertMissing assertInputPresent assertInputMissing assertDialogOpened assertEnabled assertDisabled assertButtonEnabled assertButtonDisabled assertFocused assertNotFocused assertAuthenticated assertGuest assertAuthenticatedAs assertVue assertVueIsNot assertVueContains assertVueDoesntContain

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>

// Component Definition...

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');
    });
});
/**
 * A basic Vue test example.
 */
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);

页面(Pages)

有时,测试需要按顺序执行若干复杂操作。这会使你的测试代码更难阅读和理解。Dusk 页面(Pages)允许你定义具有表现力的操作,随后可通过单个方法在给定页面上执行这些操作。页面还允许你为应用程序或单个页面定义常用选择器的快捷方式。

生成页面

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

php artisan dusk:page Login

配置页面

默认情况下,页面具有三个方法:url、assert 和 elements。我们现在将讨论 url 和 assert 方法。elements 方法将在下文更详细地讨论。

url 方法

url 方法应返回代表该页面的 URL 路径。Dusk 在浏览器中导航至该页面时会使用此 URL:

/**
 * Get the URL for the page.
 */
public function url(): string
{
    return '/login';
}

assert 方法

assert 方法可执行必要的断言,以验证浏览器确实处于给定页面。该方法中并非必须放入任何内容;不过,你可以按需进行这些断言。导航至该页面时,这些断言将自动运行:

/**
 * Assert that the browser is on the page.
 */
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”输入字段定义一个快捷方式:

/**
 * Get the element shortcuts for the page.
 *
 * @return array<string, string>
 */
public function elements(): array
{
    return [
        '@email' => 'input[name=email]',
    ];
}

一旦定义了该快捷方式,你可以在任何通常使用完整 CSS 选择器的地方使用该快捷选择器:

$browser->type('@email', '[email protected]');

全局快捷选择器

安装 Dusk 后,一个基础的 Page 类会被放置到 tests/Browser/Pages 目录中。该类包含一个 siteElements 方法,可用于定义全局快捷选择器,这些选择器将在整个应用程序的所有页面中可用:

/**
 * Get the global element shortcuts for the site.
 *
 * @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
{
    // Other page methods...

    /**
     * Create a new playlist.
     */
    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 的“页面对象”,但适用于在应用程序中重复使用的 UI 片段和功能,例如导航栏或通知窗口。因此,组件并不绑定到特定的 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
{
    /**
     * Get the root selector for the component.
     */
    public function selector(): string
    {
        return '.date-picker';
    }

    /**
     * Assert that the browser page contains the component.
     */
    public function assert(Browser $browser): void
    {
        $browser->assertVisible($this->selector());
    }

    /**
     * Get the element shortcuts for the component.
     *
     * @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',
        ];
    }

    /**
     * Select the given date.
     */
    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
{
    /**
     * A basic component test example.
     */
    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');

持续集成

大多数 Dusk 持续集成配置要求使用端口 8000 的 PHP 内置开发服务器来托管 Laravel 应用程序。因此,在继续之前,请确保你的持续集成环境中 APP_URL 环境变量的值为 http://127.0.0.1:8000。

Heroku CI

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

{
  "environments": {
    "test": {
      "buildpacks": [
        { "url": "heroku/php" },
        { "url": "https://github.com/heroku/heroku-buildpack-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: 准备环境
        run: cp .env.example .env
      - name: 创建数据库
        run: |
          sudo systemctl start mysql
          mysql --user="root" --password="root" -e "CREATE DATABASE \`my-database\` character set UTF8mb4 collate utf8mb4_bin;"
      - name: 安装 Composer 依赖
        run: composer install --no-progress --prefer-dist --optimize-autoloader
      - name: 生成应用密钥
        run: php artisan key:generate
      - name: 升级 Chrome 驱动
        run: php artisan dusk:chrome-driver --detect
      - name: 启动 Chrome 驱动
        run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=9515 &
      - name: 启动 Laravel 服务器
        run: php artisan serve --no-reload &
      - name: 运行 Dusk 测试
        run: php artisan dusk
      - name: 上传截图
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: screenshots
          path: tests/Browser/screenshots
      - name: 上传控制台日志
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: console
          path: tests/Browser/console

Chipper CI

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

# file .chipperci.yml
version: 1

environment:
  php: 8.2
  node: 16

# Include Chrome in the build environment
services:
  - dusk

# Build all commits
on:
   push:
      branches: .*

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

      # Create a dusk env file, ensuring APP_URL uses 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 官方文档。