Laravel 黄昏

Introduction

Laravel 黄昏 提供了一个富有表现力、易于使用的浏览器自动化和测试 API。默认情况下,Dusk 不要求您在本地计算机上安装 JDK 或 Selenium。相反,Dusk 使用独立的ChromeDriver 安装。但是,您可以随意使用任何其他您希望的 Selenium 兼容驱动程序。

Installation

要开始,您应该安装谷歌浏览器 并添加laravel/dusk Composer 对您的项目的依赖:

composer require --dev laravel/dusk

Warning 如果你手动注册 Dusk 的服务提供者,你应该never 在您的生产环境中注册它,因为这样做可能会导致任意用户能够通过您的应用程序进行身份验证。

安装 Dusk 包后,执行dusk:install 工匠命令。这dusk:install 命令将创建一个tests/Browser 目录,一个 Dusk 测试示例,并为您的操作系统安装 Chrome 驱动程序二进制文件:

php artisan dusk:install

接下来,设置APP_URL 应用程序中的环境变量.env 文件。此值应与您用于在浏览器中访问应用程序的 URL 相匹配。

Note 如果您正在使用Laravel 风帆 要管理您的本地开发环境,请参阅 Sail 文档配置和运行 Dusk 测试.

管理 ChromeDriver 安装

如果您想安装与 Laravel Dusk 通过以下方式安装的不同版本的 ChromeDriverdusk:install 命令,你可以使用dusk:chrome-driver 命令:

# Install the latest version of ChromeDriver for your OS...
php artisan dusk:chrome-driver

# Install a given version of ChromeDriver for your OS...
php artisan dusk:chrome-driver 86

# Install a given version of ChromeDriver for all supported OSs...
php artisan dusk:chrome-driver --all

# Install the version of ChromeDriver that matches the detected version of Chrome / Chromium for your OS...
php artisan dusk:chrome-driver --detect

Warning 黄昏需要chromedriver 二进制文件是可执行的。如果你在运行 Dusk 时遇到问题,你应该使用以下命令确保二进制文件是可执行的:chmod -R 0755 vendor/laravel/dusk/bin/.

使用其他浏览器

默认情况下,Dusk 使用谷歌浏览器和一个独立的ChromeDriver 安装以运行浏览器测试。但是,您可以启动自己的 Selenium 服务器并针对您希望的任何浏览器运行测试。

首先,打开你的tests/DuskTestCase.php 文件,这是应用程序的基本 Dusk 测试用例。在此文件中,您可以删除对startChromeDriver 方法。这将阻止 Dusk 自动启动 ChromeDriver:

/**
 * Prepare for Dusk test execution.
 *
 * @beforeClass
 */
public static function prepare(): void
{
    // static::startChromeDriver();
}

接下来,您可以修改driver 连接到您选择的 URL 和端口的方法。此外,您可以修改应传递给 WebDriver 的“所需功能”:

use Facebook\WebDriver\Remote\RemoteWebDriver;

/**
 * Create the RemoteWebDriver instance.
 */
protected function driver(): RemoteWebDriver
{
    return RemoteWebDriver::create(
        'http://localhost:4444/wd/hub', DesiredCapabilities::phantomjs()
    );
}

入门

生成测试

要生成 Dusk 测试,请使用dusk:make 工匠命令。生成的测试将被放置在tests/Browser 目录:

php artisan dusk:make LoginTest

每次测试后重置数据库

您编写的大多数测试将与从应用程序数据库中检索数据的页面进行交互;然而,你的 Dusk 测试不应该使用RefreshDatabase特征。这RefreshDatabase trait 利用了在 HTTP 请求中不适用或不可用的数据库事务。相反,您有两个选择:DatabaseMigrations 特质和DatabaseTruncation 特征。

使用数据库迁移

DatabaseMigrations trait 将在每次测试之前运行您的数据库迁移。但是,为每个测试删除并重新创建数据库表通常比截断表慢:

<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;
}

Warning 执行 Dusk 测试时可能不会使用 SQLite 内存数据库。由于浏览器在自己的进程中执行,因此无法访问其他进程的内存数据库。

使用数据库截断

在使用之前DatabaseTruncation 特质,你必须安装doctrine/dbal 使用 Composer 包管理器包:

composer require --dev doctrine/dbal

DatabaseTruncation trait 将在第一次测试时迁移您的数据库,以确保您的数据库表已正确创建。然而,在随后的测试中,数据库的表将被简单地截断——与重新运行所有数据库迁移相比提供了速度提升:

<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseTruncation;
}

默认情况下,此特征将截断除migrations 桌子。如果你想自定义应该被截断的表,你可以定义一个$tablesToTruncate 测试类的属性:

/**
 * Indicates which tables should be truncated.
 *
 * @var array
 */
protected $tablesToTruncate = ['users'];

或者,您可以定义一个$exceptTables 测试类上的属性以指定应从截断中排除哪些表:

/**
 * Indicates which tables should be excluded from truncation.
 *
 * @var array
 */
protected $exceptTables = ['users'];

要指定应截断其表的数据库连接,您可以定义一个$connectionsToTruncate 测试类的属性:

/**
 * Indicates which connections should have their tables truncated.
 *
 * @var array
 */
protected $connectionsToTruncate = ['mysql'];

运行测试

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

php artisan dusk

如果上次运行时测试失败dusk 命令,您可以通过首先使用dusk:fails 命令:

php artisan dusk:fails

dusk 命令接受 PHPUnit 测试运行器通常接受的任何参数,例如允许您只运行给定的测试group:

php artisan dusk --group=foo

Note 如果您正在使用Laravel 风帆 要管理您的本地开发环境,请查阅 Sail 文档配置和运行 Dusk 测试.

手动启动 ChromeDriver

默认情况下,Dusk 会自动尝试启动 ChromeDriver。如果这对您的特定系统不起作用,您可以在运行之前手动启动 ChromeDriverdusk 命令。如果您选择手动启动 ChromeDriver,您应该注释掉您的以下行tests/DuskTestCase.php 文件:

/**
 * Prepare for Dusk test execution.
 *
 * @beforeClass
 */
public static function prepare(): void
{
    // static::startChromeDriver();
}

另外,如果你在9515以外的端口启动ChromeDriver,你应该修改driver 反映正确端口的同类方法:

use Facebook\WebDriver\Remote\RemoteWebDriver;

/**
 * Create the RemoteWebDriver instance.
 */
protected function driver(): RemoteWebDriver
{
    return RemoteWebDriver::create(
        'http://localhost:9515', DesiredCapabilities::chrome()
    );
}

环境处理

要强制 Dusk 在运行测试时使用它自己的环境文件,创建一个.env.dusk.{environment} 文件在项目的根目录中。例如,如果您要启动dusk 从你的命令local 环境,你应该创建一个.env.dusk.local 文件。

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

浏览器基础

创建浏览器

首先,让我们编写一个测试来验证我们可以登录到我们的应用程序。生成测试后,我们可以修改它以导航到登录页面,输入一些凭据,然后单击“登录”按钮。要创建浏览器实例,您可以调用browse Dusk 测试中的方法:

<?php

namespace Tests\Browser;

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

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

    /**
     * A basic browser test example.
     */
    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 自动传递给这个闭包,它是用于与您的应用程序交互和对您的应用程序进行断言的主要对象。

创建多个浏览器

有时您可能需要多个浏览器才能正确执行测试。例如,可能需要多个浏览器来测试与 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');
});

Navigation

visit 方法可用于导航到应用程序中的给定 URI:

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

您可以使用visitRoute 方法导航到命名路线:

$browser->visitRoute('login');

您可以使用导航“后退”和“前进”backforward 方法:

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

浏览器宏

如果您想定义一个可以在各种测试中重复使用的自定义浏览器方法,您可以使用macro 上的方法Browser 班级。通常,您应该从服务供应商 boot 方法:

<?php

namespace App\Providers;

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

class DuskServiceProvider extends ServiceProvider
{
    /**
     * Register Dusk's browser macros.
     */
    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');
});

Authentication

通常,您将测试需要身份验证的页面。你可以使用 Dusk 的loginAs 方法以避免在每次测试期间与应用程序的登录屏幕进行交互。这loginAs 方法接受与您的可验证模型或可验证模型实例关联的主键:

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

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

Warning 使用后loginAs 方法,将为文件中的所有测试维护用户会话。

Cookies

您可以使用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');

执行脚本

您可以使用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');

将控制台输出存储到磁盘

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

$browser->storeConsoleLog('filename');

将页面源存储到磁盘

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

$browser->storeSource('filename');

与元素交互

黄昏选择器

选择好的 CSS 选择器来与元素交互是编写 Dusk 测试最困难的部分之一。随着时间的推移,前端更改可能会导致如下所示的 CSS 选择器破坏您的测试:

// HTML...

<button>Login</button>

// Test...

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

Dusk 选择器让你专注于编写有效的测试而不是记住 CSS 选择器。要定义一个选择器,添加一个dusk 属性到您的 HTML 元素。然后,当与 Dusk 浏览器交互时,在选择器前面加上@ 在测试中操作附加元素:

// HTML...

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

// Test...

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

文本、值和属性

检索和设置值

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', 'taylor@laravel.com');

请注意,尽管该方法在必要时接受一个,但我们不需要将 CSS 选择器传递给type 方法。如果没有提供 CSS 选择器,Dusk 将搜索一个input 或者textarea 给定的字段name 属性。

要将文本附加到字段而不清除其内容,您可以使用append 方法:

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

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

$browser->clear('email');

你可以指示 Dusk 使用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');

Dropdowns

选择一个可用的值select 元素,你可以使用select 方法。像type 方法,select 方法不需要完整的 CSS 选择器。当传递一个值给select 方法,您应该传递基础选项值而不是显示文本:

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

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

$browser->select('size');

通过提供一个数组作为第二个参数select 方法,您可以指示该方法选择多个选项:

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

Checkboxes

要“选中”复选框输入,您可以使用check 方法。与许多其他输入相关的方法一样,不需要完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 将搜索具有匹配项的复选框name 属性:

$browser->check('terms');

uncheck 方法可用于“取消选中”复选框输入:

$browser->uncheck('terms');

单选按钮

“选择”一个radio 输入选项,您可以使用radio 方法。与许多其他输入相关的方法一样,不需要完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 会搜索一个radio 输入匹配namevalue 属性:

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

附加文件

attach 方法可用于将文件附加到file 输入元素。与许多其他输入相关的方法一样,不需要完整的 CSS 选择器。如果找不到匹配的 CSS 选择器,Dusk 会搜索一个file 输入匹配name 属性:

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

Warning 附加功能需要Zip 要在您的服务器上安装和启用的 PHP 扩展。

按下按钮

press 方法可用于单击页面上的按钮元素。给的论据press method 可以是按钮的显示文本或 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)) {
    // ...
}

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

使用键盘

keys 方法允许您向给定元素提供比通常允许的更复杂的输入序列type 方法。例如,您可以指示 Dusk 在输入值时按住修改键。在这个例子中,shift 键将同时举行taylor 被输入到与给定选择器匹配的元素中。后taylor 被键入,swift 将在没有任何修改键的情况下键入:

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

另一个有价值的用例keys 方法是将“键盘快捷方式”组合发送到应用程序的主要 CSS 选择器:

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

Note 所有修改键,例如{command} 被包裹在{} 字符,并匹配中定义的常量Facebook\WebDriver\WebDriverKeys 类,可以是在 GitHub 上找到.

使用鼠标

单击元素

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

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

clickAtXPath 方法可用于单击与给定 XPath 表达式匹配的元素:

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

clickAtPoint 方法可用于点击相对于浏览器可视区域的给定坐标对处的最顶层元素:

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

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

$browser->doubleClick();

rightClick 方法可用于模拟鼠标右键单击:

$browser->rightClick();

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

clickAndHold 方法可用于模拟鼠标按钮被单击和按住。随后调用releaseMouse 方法将撤消此行为并释放鼠标按钮:

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

Mouseover

mouseover 当您需要将鼠标移动到与给定 CSS 或 Dusk 选择器匹配的元素上时,可以使用方法:

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

作用域选择器

有时您可能希望执行多个操作,同时在给定选择器中确定所有操作的范围。例如,您可能希望断言某些文本仅存在于表格中,然后单击该表格中的按钮。您可以使用with 方法来实现这一点。在给定的闭包内执行的所有操作with 方法将作用于原始选择器:

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

您可能偶尔需要在当前范围之外执行断言。您可以使用elsewhereelsewhereWhenAvailable 实现此目的的方法:

 $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 的应用程序时,通常需要“等待”某些元素或数据可用,然后才能继续进行测试。黄昏使这成为小事一桩。使用多种方法,您可以等待元素在页面上可见,甚至可以等到给定的 JavaScript 表达式计算为true.

Waiting

如果您只需要将测试暂停给定的毫秒数,请使用pause 方法:

$browser->pause(1000);

如果仅在给定条件满足时才需要暂停测试true, 使用pauseIf 方法:

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

同样,如果您需要暂停测试,除非给定条件是true, 你可以使用pauseUnless 方法:

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

等待选择器

waitFor 方法可用于暂停测试的执行,直到与给定 CSS 或 Dusk 选择器匹配的元素显示在页面上。默认情况下,这将在抛出异常之前暂停测试最多五秒钟。如有必要,您可以将自定义超时阈值作为第二个参数传递给该方法:

// Wait a maximum of five seconds for the selector...
$browser->waitFor('.selector');

// Wait a maximum of one second for the selector...
$browser->waitFor('.selector', 1);

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

// Wait a maximum of five seconds for the selector to contain the given text...
$browser->waitForTextIn('.selector', 'Hello World');

// Wait a maximum of one second for the selector to contain the given text...
$browser->waitForTextIn('.selector', 'Hello World', 1);

您也可以等到页面中缺少与给定选择器匹配的元素:

// Wait a maximum of five seconds until the selector is missing...
$browser->waitUntilMissing('.selector');

// Wait a maximum of one second until the selector is missing...
$browser->waitUntilMissing('.selector', 1);

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

// Wait a maximum of five seconds until the selector is enabled...
$browser->waitUntilEnabled('.selector');

// Wait a maximum of one second until the selector is enabled...
$browser->waitUntilEnabled('.selector', 1);

// Wait a maximum of five seconds until the selector is disabled...
$browser->waitUntilDisabled('.selector');

// Wait a maximum of one second until the selector is disabled...
$browser->waitUntilDisabled('.selector', 1);

范围选择器可用时

有时,您可能希望等待与给定选择器匹配的元素出现,然后与该元素进行交互。例如,您可能希望等到模态窗口可用,然后按模态中的“确定”按钮。这whenAvailable 方法可以用来完成这个。在给定闭包中执行的所有元素操作都将限定在原始选择器中:

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

等待文本

waitForText 方法可用于等待给定文本显示在页面上:

// Wait a maximum of five seconds for the text...
$browser->waitForText('Hello World');

// Wait a maximum of one second for the text...
$browser->waitForText('Hello World', 1);

您可以使用waitUntilMissingText 等到显示的文本从页面中删除的方法:

// Wait a maximum of five seconds for the text to be removed...
$browser->waitUntilMissingText('Hello World');

// Wait a maximum of one second for the text to be removed...
$browser->waitUntilMissingText('Hello World', 1);

等待链接

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

// Wait a maximum of five seconds for the link...
$browser->waitForLink('Create');

// Wait a maximum of one second for the link...
$browser->waitForLink('Create', 1);

等待输入

waitForInput 方法可用于等待给定的输入字段在页面上可见:

// Wait a maximum of five seconds for the input...
$browser->waitForInput($field);

// Wait a maximum of one second for the input...
$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 关键字或结束分号:

// Wait a maximum of five seconds for the expression to be true...
$browser->waitUntil('App.data.servers.length > 0');

// Wait a maximum of one second for the expression to be true...
$browser->waitUntil('App.data.servers.length > 0', 1);

等待 Vue 表达式

waitUntilVuewaitUntilVueIsNot 方法可以用来等到Vue 组件 属性具有给定值:

// Wait until the component attribute contains the given value...
$browser->waitUntilVue('user.name', 'Taylor', '@user');

// Wait until the component attribute doesn't contain the given value...
$browser->waitUntilVueIsNot('user.name', null, '@user');

等待 JavaScript 事件

waitForEvent 方法可用于暂停测试的执行,直到发生 JavaScript 事件:

$browser->waitForEvent('load');

事件侦听器附加到当前范围,即body 默认情况下的元素。使用范围选择器时,事件监听器将附加到匹配元素:

$browser->with('iframe', function (Browser $iframe) {
    // Wait for the iframe's load event...
    $iframe->waitForEvent('load');
});

您还可以提供一个选择器作为第二个参数waitForEvent 将事件侦听器附加到特定元素的方法:

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

您也可以等待事件documentwindow 对象:

// Wait until the document is scrolled...
$browser->waitForEvent('scroll', 'document');

// Wait a maximum of five seconds until the window is resized...
$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');

assertPathIs

断言当前路径与给定路径匹配:

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

assertPathIsNot

断言当前路径与给定路径不匹配:

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

assertRouteIs

断言当前 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);

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

assertAttributeContains

断言与给定选择器匹配的元素在提供的属性中包含给定值:

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

assertAriaAttribute

断言与给定选择器匹配的元素在提供的 aria 属性中具有给定值:

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

例如,给定标记<button aria-label="Add"></button>,你可以断言反对aria-label 像这样的属性:

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

assertDataAttribute

断言匹配给定选择器的元素在提供的数据属性中具有给定值:

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

例如,给定标记<tr id="row-1" data-content="attendees"></tr>,你可以断言反对data-label 像这样的属性:

$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 组件的状态:

/**
 * 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);

assertVueDoesNotContain

断言给定的 Vue 组件数据属性是一个数组并且不包含给定值:

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

Pages

有时,测试需要按顺序执行几个复杂的操作。这会使您的测试更难阅读和理解。 Dusk Pages 允许您定义富有表现力的动作,然后可以通过单一方法在给定页面上执行这些动作。页面还允许您为您的应用程序或单个页面定义通用选择器的快捷方式。

生成页面

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

php artisan dusk:page Login

配置页面

默认情况下,页面有三种方法:url,assert, 和elements.我们将讨论urlassert 现在的方法。这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 选择器定义快速、易于记忆的快捷方式。例如,让我们为应用程序登录页面的“电子邮件”输入字段定义一个快捷方式:

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

定义快捷方式后,您可以在通常使用完整 CSS 选择器的任何地方使用速记选择器:

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

全局速记选择器

安装 Dusk 后,一个 basePage 类将被放置在你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;

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

Components

组件类似于 Dusk 的“页面对象”,但适用于在整个应用程序中重复使用的 UI 和功能,例如导航栏或通知窗口。因此,组件不会绑定到特定的 URL。

生成组件

要生成组件,请执行dusk:component 工匠命令。新组件被放置在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

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

持续集成

Warning 大多数 Dusk 持续集成配置都希望你的 Laravel 应用程序使用内置的 PHP 开发服务器在端口 8000 上提供服务。因此,在继续之前,你应该确保你的持续集成环境有一个APP_URL 的环境变量值http://127.0.0.1:8000.

Heroku CI

运行 Dusk 测试Heroku CI,将以下 Google Chrome buildpack 和脚本添加到您的 Herokuapp.json 文件:

{
  "environments": {
    "test": {
      "buildpacks": [
        { "url": "heroku/php" },
        { "url": "https://github.com/heroku/heroku-buildpack-google-chrome" }
      ],
      "scripts": {
        "test-setup": "cp .env.testing .env",
        "test": "nohup bash -c './vendor/laravel/dusk/bin/chromedriver-linux > /dev/null 2>&1 &' && nohup bash -c 'php artisan serve --no-reload > /dev/null 2>&1 &' && php artisan dusk"
      }
    }
  }
}

特拉维斯CI

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

language: php

php:
  - 7.3

addons:
  chrome: stable

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

如果您正在使用GitHub 操作 要运行你的 Dusk 测试,你可以使用以下配置文件作为起点。像 TravisCI 一样,我们将使用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@v3
      - 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 &
      - 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@v2
        with:
          name: screenshots
          path: tests/Browser/screenshots
      - name: Upload Console Logs
        if: failure()
        uses: actions/upload-artifact@v2
        with:
          name: console
          path: tests/Browser/console
豫ICP备18041297号-2