Browser Tests (Laravel Dusk)

Introduction

Laravel Dusk provides an expressive, easy-to-use browser automation and testing API. By default, Dusk does not require you to install JDK or Selenium on your machine. Instead, Dusk uses a standalone ChromeDriver installation. However, you are free to utilize any other Selenium compatible driver you wish.

Installation

To get started, you should add the laravel/dusk Composer dependency to your project:

  1. composer require --dev laravel/dusk:"^1.0"

Once Dusk is installed, you should register the Laravel\Dusk\DuskServiceProvider service provider. You should register the provider within the register method of your AppServiceProvider in order to limit the environments in which Dusk is available, since it exposes the ability to log in as other users:

  1. use Laravel\Dusk\DuskServiceProvider;
  2. /**
  3. * Register any application services.
  4. *
  5. * @return void
  6. */
  7. public function register()
  8. {
  9. if ($this->app->environment('local', 'testing')) {
  10. $this->app->register(DuskServiceProvider::class);
  11. }
  12. }

Next, run the dusk:install Artisan command:

  1. php artisan dusk:install

A Browser directory will be created within your tests directory and will contain an example test. Next, set the APP_URL environment variable in your .env file. This value should match the URL you use to access your application in a browser.

To run your tests, use the dusk Artisan command. The dusk command accepts any argument that is also accepted by the phpunit command:

  1. php artisan dusk

Using Other Browsers

By default, Dusk uses Google Chrome and a standalone ChromeDriver installation to run your browser tests. However, you may start your own Selenium server and run your tests against any browser you wish.

To get started, open your tests/DuskTestCase.php file, which is the base Dusk test case for your application. Within this file, you can remove the call to the startChromeDriver method. This will stop Dusk from automatically starting the ChromeDriver:

  1. /**
  2. * Prepare for Dusk test execution.
  3. *
  4. * @beforeClass
  5. * @return void
  6. */
  7. public static function prepare()
  8. {
  9. // static::startChromeDriver();
  10. }

Next, you may simply modify the driver method to connect to the URL and port of your choice. In addition, you may modify the "desired capabilities" that should be passed to the WebDriver:

  1. /**
  2. * Create the RemoteWebDriver instance.
  3. *
  4. * @return \Facebook\WebDriver\Remote\RemoteWebDriver
  5. */
  6. protected function driver()
  7. {
  8. return RemoteWebDriver::create(
  9. 'http://localhost:4444/wd/hub', DesiredCapabilities::phantomjs()
  10. );
  11. }

ChromeDriver Options

To customize the ChromeDriver session, you may modify the driver method of the DuskTestCase class:

  1. use Facebook\WebDriver\Chrome\ChromeOptions;
  2. /**
  3. * Create the RemoteWebDriver instance.
  4. *
  5. * @return \Facebook\WebDriver\Remote\RemoteWebDriver
  6. */
  7. protected function driver()
  8. {
  9. $options = (new ChromeOptions)->addArguments(['--headless']);
  10. return RemoteWebDriver::create(
  11. 'http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
  12. ChromeOptions::CAPABILITY, $options
  13. )
  14. );
  15. }

Getting Started

Generating Tests

To generate a Dusk test, use the dusk:make Artisan command. The generated test will be placed in the tests/Browser directory:

  1. php artisan dusk:make LoginTest

Running Tests

To run your browser tests, use the dusk Artisan command:

  1. php artisan dusk

The dusk command accepts any argument that is normally accepted by the PHPUnit test runner, allowing you to only run the tests for a given group, etc:

  1. php artisan dusk --group=foo

Manually Starting ChromeDriver

By default, Dusk will automatically attempt to start ChromeDriver. If this does not work for your particular system, you may manually start ChromeDriver before running the dusk command. If you choose to start ChromeDriver manually, you should comment out the following line of your tests/DuskTestCase.php file:

  1. /**
  2. * Prepare for Dusk test execution.
  3. *
  4. * @beforeClass
  5. * @return void
  6. */
  7. public static function prepare()
  8. {
  9. // static::startChromeDriver();
  10. }

In addition, if you start ChromeDriver on a port other than 9515, you should modify the driver method of the same class:

  1. /**
  2. * Create the RemoteWebDriver instance.
  3. *
  4. * @return \Facebook\WebDriver\Remote\RemoteWebDriver
  5. */
  6. protected function driver()
  7. {
  8. return RemoteWebDriver::create(
  9. 'http://localhost:9515', DesiredCapabilities::chrome()
  10. );
  11. }

Environment Handling

To force Dusk to use its own environment file when running tests, create a .env.dusk.{environment} file in the root of your project. For example, if you will be initiating the dusk command from your local environment, you should create a .env.dusk.local file.

When running tests, Dusk will back-up your .env file and rename your Dusk environment to .env. Once the tests have completed, your .env file will be restored.

Creating Browsers

To get started, let's write a test that verifies we can log into our application. After generating a test, we can modify it to navigate to the login page, enter some credentials, and click the "Login" button. To create a browser instance, call the browse method:

  1. <?php
  2. namespace Tests\Browser;
  3. use App\User;
  4. use Tests\DuskTestCase;
  5. use Laravel\Dusk\Chrome;
  6. use Illuminate\Foundation\Testing\DatabaseMigrations;
  7. class ExampleTest extends DuskTestCase
  8. {
  9. use DatabaseMigrations;
  10. /**
  11. * A basic browser test example.
  12. *
  13. * @return void
  14. */
  15. public function testBasicExample()
  16. {
  17. $user = factory(User::class)->create([
  18. 'email' => '[email protected]',
  19. ]);
  20. $this->browse(function ($browser) use ($user) {
  21. $browser->visit('/login')
  22. ->type('email', $user->email)
  23. ->type('password', 'secret')
  24. ->press('Login')
  25. ->assertPathIs('/home');
  26. });
  27. }
  28. }

As you can see in the example above, the browse method accepts a callback. A browser instance will automatically be passed to this callback by Dusk and is the main object used to interact with and make assertions against your application.

{tip} This test can be used to test the login screen generated by the make:auth Artisan command.

Creating Multiple Browsers

Sometimes you may need multiple browsers in order to properly carry out a test. For example, multiple browsers may be needed to test a chat screen that interacts with websockets. To create multiple browsers, simply "ask" for more than one browser in the signature of the callback given to the browse method:

  1. $this->browse(function ($first, $second) {
  2. $first->loginAs(User::find(1))
  3. ->visit('/home')
  4. ->waitForText('Message');
  5. $second->loginAs(User::find(2))
  6. ->visit('/home')
  7. ->waitForText('Message')
  8. ->type('message', 'Hey Taylor')
  9. ->press('Send');
  10. $first->waitForText('Hey Taylor')
  11. ->assertSee('Jeffrey Way');
  12. });

Authentication

Often, you will be testing pages that require authentication. You can use Dusk's loginAs method in order to avoid interacting with the login screen during every test. The loginAs method accepts a user ID or user model instance:

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

{note} After using the loginAs method, the user session will be maintained for all tests within the file.

Interacting With Elements

To click a link, you may use the clickLink method on the browser instance. The clickLink method will click the link that has the given display text:

  1. $browser->clickLink($linkText);

{note} This method interacts with jQuery. If jQuery is not available on the page, Dusk will automatically inject it into the page so it is available for the test's duration.

Text, Values, & Attributes

Retrieving & Setting Values

Dusk provides several methods for interacting with the current display text, value, and attributes of elements on the page. For example, to get the "value" of an element that matches a given selector, use the value method:

  1. // Retrieve the value...
  2. $value = $browser->value('selector');
  3. // Set the value...
  4. $browser->value('selector', 'value');

Retrieving Text

The text method may be used to retrieve the display text of an element that matches the given selector:

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

Retrieving Attributes

Finally, the attribute method may be used to retrieve an attribute of an element matching the given selector:

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

Using Forms

Typing Values

Dusk provides a variety of methods for interacting with forms and input elements. First, let's take a look at an example of typing text into an input field:

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

Note that, although the method accepts one if necessary, we are not required to pass a CSS selector into the type method. If a CSS selector is not provided, Dusk will search for an input field with the given name attribute. Finally, Dusk will attempt to find a textarea with the given name attribute.

You may "clear" the value of an input using the clear method:

  1. $browser->clear('email');

Dropdowns

To select a value in a dropdown selection box, you may use the select method. Like the type method, the select method does not require a full CSS selector. When passing a value to the select method, you should pass the underlying option value instead of the display text:

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

You may select a random option by omitting the second parameter:

  1. $browser->select('size');

Checkboxes

To "check" a checkbox field, you may use the check method. Like many other input related methods, a full CSS selector is not required. If an exact selector match can't be found, Dusk will search for a checkbox with a matching name attribute:

  1. $browser->check('terms');
  2. $browser->uncheck('terms');

Radio Buttons

To "select" a radio button option, you may use the radio method. Like many other input related methods, a full CSS selector is not required. If an exact selector match can't be found, Dusk will search for a radio with matching name and value attributes:

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

Attaching Files

The attach method may be used to attach a file to a file input element. Like many other input related methods, a full CSS selector is not required. If an exact selector match can't be found, Dusk will search for a file input with matching name attribute:

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

Using The Keyboard

The keys method allows you to provide more complex input sequences to a given element than normally allowed by the type method. For example, you may hold modifier keys entering values. In this example, the shift key will be held while taylor is entered into the element matching the given selector. After taylor is typed, otwell will be typed without any modifier keys:

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

You may even send a "hot key" to the primary CSS selector that contains your application:

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

{tip} All modifier keys are wrapped in {} characters, and match the constants defined in the Facebook\WebDriver\WebDriverKeys class, which can be found on GitHub.

Using The Mouse

Clicking On Elements

The click method may be used to "click" on an element matching the given selector:

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

Mouseover

The mouseover method may be used when you need to move the mouse over an element matching the given selector:

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

Drag & Drop

The drag method may be used to drag an element matching the given selector to another element:

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

Or, you may drag an element in a single direction:

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

Scoping Selectors

Sometimes you may wish to perform several operations while scoping all of the operations within a given selector. For example, you may wish to assert that some text exists only within a table and then click a button within that table. You may use the with method to accomplish this. All operations performed within the callback given to the with method will be scoped to the original selector:

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

Waiting For Elements

When testing applications that use JavaScript extensively, it often becomes necessary to "wait" for certain elements or data to be available before proceeding with a test. Dusk makes this a cinch. Using a variety of methods, you may wait for elements to be visible on the page or even wait until a given JavaScript expression evaluates to true.

Waiting

If you need to pause the test for a given number of milliseconds, use the pause method:

  1. $browser->pause(1000);

Waiting For Selectors

The waitFor method may be used to pause the execution of the test until the element matching the given CSS selector is displayed on the page. By default, this will pause the test for a maximum of five seconds before throwing an exception. If necessary, you may pass a custom timeout threshold as the second argument to the method:

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

You may also wait until the given selector is missing from the page:

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

Scoping Selectors When Available

Occasionally, you may wish to wait for a given selector and then interact with the element matching the selector. For example, you may wish to wait until a modal window is available and then press the "OK" button within the modal. The whenAvailable method may be used in this case. All element operations performed within the given callback will be scoped to the original selector:

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

Waiting For Text

The waitForText method may be used to wait until the given text is displayed on the page:

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

The waitForLink method may be used to wait until the given link text is displayed on the page:

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

Waiting On The Page Location

When making a path assertion such as $browser->assertPathIs('/home'), the assertion can fail if window.location.pathname is being updated asynchronously. You may use the waitForLocation method to wait for the location to be a given value:

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

Waiting for Page Reloads

If you need to make assertions after a page has been reloaded, use the waitForReload method:

  1. $browser->click('.some-action')
  2. ->waitForReload()
  3. ->assertSee('something');

Waiting On JavaScript Expressions

Sometimes you may wish to pause the execution of a test until a given JavaScript expression evaluates to true. You may easily accomplish this using the waitUntil method. When passing an expression to this method, you do not need to include the return keyword or an ending semi-colon:

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

Waiting With A Callback

Many of the "wait" methods in Dusk rely on the underlying waitUsing method. You may use this method directly to wait for a given callback to return true. The waitUsing method accepts the maximum number of seconds to wait, the interval at which the Closure should be evaluated, the Closure, and an optional failure message:

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

Available Assertions

Dusk provides a variety of assertions that you may make against your application. All of the available assertions are documented in the table below:

AssertionDescription
$browser->assertTitle($title)Assert the page title matches the given text.
$browser->assertTitleContains($title)Assert the page title contains the given text.
$browser->assertPathBeginsWith($path)Assert that the current URL path begins with given path.
$browser->assertPathIs('/home')Assert the current path matches the given path.
$browser->assertPathIsNot('/home')Assert the current path does not match the given path.
$browser->assertRouteIs($name, $parameters)Assert the current URL matches the given named route's URL.
$browser->assertQueryStringHas($name, $value)Assert the given query string parameter is present and has a given value.
$browser->assertQueryStringMissing($name)Assert the given query string parameter is missing.
$browser->assertHasQueryStringParameter($name)Assert that the given query string parameter is present.
$browser->assertHasCookie($name)Assert the given cookie is present.
$browser->assertCookieValue($name, $value)Assert a cookie has a given value.
$browser->assertPlainCookieValue($name, $value)Assert an unencrypted cookie has a given value.
$browser->assertSee($text)Assert the given text is present on the page.
$browser->assertDontSee($text)Assert the given text is not present on the page.
$browser->assertSeeIn($selector, $text)Assert the given text is present within the selector.
$browser->assertDontSeeIn($selector, $text)Assert the given text is not present within the selector.
$browser->assertSourceHas($code)Assert that the given source code is present on the page.
$browser->assertSourceMissing($code)Assert that the given source code is not present on the page.
$browser->assertSeeLink($linkText)Assert the given link is present on the page.
$browser->assertDontSeeLink($linkText)Assert the given link is not present on the page.
$browser->assertInputValue($field, $value)Assert the given input field has the given value.
$browser->assertInputValueIsNot($field, $value)Assert the given input field does not have the given value.
$browser->assertChecked($field)Assert the given checkbox is checked.
$browser->assertNotChecked($field)Assert the given checkbox is not checked.
$browser->assertRadioSelected($field, $value)Assert the given radio field is selected.
$browser->assertRadioNotSelected($field, $value)Assert the given radio field is not selected.
$browser->assertSelected($field, $value)Assert the given dropdown has the given value selected.
$browser->assertNotSelected($field, $value)Assert the given dropdown does not have the given value selected.
$browser->assertSelectHasOptions($field, $values)Assert that the given array of values are available to be selected.
$browser->assertSelectMissingOptions($field, $values)Assert that the given array of values are not available to be selected.
$browser->assertSelectHasOption($field, $value)Assert that the given value is available to be selected on the given field.
$browser->assertValue($selector, $value)Assert the element matching the given selector has the given value.
$browser->assertVisible($selector)Assert the element matching the given selector is visible.
$browser->assertMissing($selector)Assert the element matching the given selector is not visible.
$browser->assertDialogOpened($message)Assert that a JavaScript dialog with given message has been opened.

Pages

Sometimes, tests require several complicated actions to be performed in sequence. This can make your tests harder to read and understand. Pages allow you to define expressive actions that may then be performed on a given page using a single method. Pages also allow you to define short-cuts to common selectors for your application or a single page.

Generating Pages

To generate a page object, use the dusk:page Artisan command. All page objects will be placed in the tests/Browser/Pages directory:

  1. php artisan dusk:page Login

Configuring Pages

By default, pages have three methods: url, assert, and elements. We will discuss the url and assert methods now. The elements method will be discussed in more detail below.

The url Method

The url method should return the path of the URL that represents the page. Dusk will use this URL when navigating to the page in the browser:

  1. /**
  2. * Get the URL for the page.
  3. *
  4. * @return string
  5. */
  6. public function url()
  7. {
  8. return '/login';
  9. }

The assert Method

The assert method may make any assertions necessary to verify that the browser is actually on the given page. Completing this method is not necessary; however, you are free to make these assertions if you wish. These assertions will be run automatically when navigating to the page:

  1. /**
  2. * Assert that the browser is on the page.
  3. *
  4. * @return void
  5. */
  6. public function assert(Browser $browser)
  7. {
  8. $browser->assertPathIs($this->url());
  9. }

Navigating To Pages

Once a page has been configured, you may navigate to it using the visit method:

  1. use Tests\Browser\Pages\Login;
  2. $browser->visit(new Login);

Sometimes you may already be on a given page and need to "load" the page's selectors and methods into the current test context. This is common when pressing a button and being redirected to a given page without explicitly navigating to it. In this situation, you may use the on method to load the page:

  1. use Tests\Browser\Pages\CreatePlaylist;
  2. $browser->visit('/dashboard')
  3. ->clickLink('Create Playlist')
  4. ->on(new CreatePlaylist)
  5. ->assertSee('@create');

Shorthand Selectors

The elements method of pages allows you to define quick, easy-to-remember shortcuts for any CSS selector on your page. For example, let's define a shortcut for the "email" input field of the application's login page:

  1. /**
  2. * Get the element shortcuts for the page.
  3. *
  4. * @return array
  5. */
  6. public function elements()
  7. {
  8. return [
  9. '@email' => 'input[name=email]',
  10. ];
  11. }

Now, you may use this shorthand selector anywhere you would use a full CSS selector:

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

Global Shorthand Selectors

After installing Dusk, a base Page class will be placed in your tests/Browser/Pages directory. This class contains a siteElements method which may be used to define global shorthand selectors that should be available on every page throughout your application:

  1. /**
  2. * Get the global element shortcuts for the site.
  3. *
  4. * @return array
  5. */
  6. public static function siteElements()
  7. {
  8. return [
  9. '@element' => '#selector',
  10. ];
  11. }

Page Methods

In addition to the default methods defined on pages, you may define additional methods which may be used throughout your tests. For example, let's imagine we are building a music management application. A common action for one page of the application might be to create a playlist. Instead of re-writing the logic to create a playlist in each test, you may define a createPlaylist method on a page class:

  1. <?php
  2. namespace Tests\Browser\Pages;
  3. use Laravel\Dusk\Browser;
  4. class Dashboard extends Page
  5. {
  6. // Other page methods...
  7. /**
  8. * Create a new playlist.
  9. *
  10. * @param \Laravel\Dusk\Browser $browser
  11. * @param string $name
  12. * @return void
  13. */
  14. public function createPlaylist(Browser $browser, $name)
  15. {
  16. $browser->type('name', $name)
  17. ->check('share')
  18. ->press('Create Playlist');
  19. }
  20. }

Once the method has been defined, you may use it within any test that utilizes the page. The browser instance will automatically be passed to the page method:

  1. use Tests\Browser\Pages\Dashboard;
  2. $browser->visit(new Dashboard)
  3. ->createPlaylist('My Playlist')
  4. ->assertSee('My Playlist');

Continuous Integration

Travis CI

To run your Dusk tests on Travis CI, we will need to use the "sudo-enabled" Ubuntu 14.04 (Trusty) environment. Since Travis CI is not a graphical environment, we will need to take some extra steps in order to launch a Chrome browser. In addition, we will use php artisan serve to launch PHP's built-in web server:

  1. sudo: required
  2. dist: trusty
  3. before_script:
  4. - export DISPLAY=:99.0
  5. - sh -e /etc/init.d/xvfb start
  6. - ./vendor/laravel/dusk/bin/chromedriver-linux &
  7. - cp .env.testing .env
  8. - php artisan serve > /dev/null 2>&1 &
  9. script:
  10. - php artisan dusk

CircleCI

CircleCI 1.0

If you are using CircleCI 1.0 to run your Dusk tests, you may use this configuration file as a starting point. Like TravisCI, we will use the php artisan serve command to launch PHP's built-in web server:

  1. test:
  2. pre:
  3. - "./vendor/laravel/dusk/bin/chromedriver-linux":
  4. background: true
  5. - cp .env.testing .env
  6. - "php artisan serve":
  7. background: true
  8. override:
  9. - php artisan dusk

CircleCI 2.0

If you are using CircleCI 2.0 to run your Dusk tests, you may add these steps to your build:

  1. version: 2
  2. jobs:
  3. build:
  4. steps:
  5. - run:
  6. name: Start Chrome Driver
  7. command: ./vendor/laravel/dusk/bin/chromedriver-linux
  8. background: true
  9. - run:
  10. name: Run Laravel Server
  11. command: php artisan serve
  12. background: true
  13. - run:
  14. name: Run Laravel Dusk Tests
  15. command: php artisan dusk