Creating FunctionalJavascript tests (real browser)

Last updated on
28 February 2025

This documentation needs work. See "Help improve this page" in the sidebar.

Unlike Unit, Kernel, and Functional tests, FunctionalJavascript tests are executed in a real browser. JavaScript behaviours are executed, meaning the tests will have the same experience AND requirements that you have in a normal browser. The main disadvantage is that they require more tooling and take longer to execute; if what you are testing does not require JavaScript interactions, consider writing a Unit, Kernel or Functional test instead.

See Running PHPUnit JavaScript tests for configuration related to executing FunctionalJavascript tests.

Scaffold a simple test

  1. Add a FunctionalJavascript folder inside mymodule/tests/src and create a MyModuleTest.php file.
  2. This PHP class should extend Drupal\FunctionalJavascriptTests\WebDriverTestBase. The namespace for your test will be \Drupal\Tests\[MODULENAME]\FunctionalJavascript.
  3. Scaffold a class method to run a test. The method name must start with "test," as shown in "testMyModule()" below:
<?php

namespace Drupal\Tests\mymodule\FunctionalJavascript;

use Drupal\FunctionalJavascriptTests\WebDriverTestBase;

/**
 * Tests the MyModule functionality.
 *
 * @group mymodule
 */
class MymoduleTest extends WebDriverTestBase {

 /**
   * Tests mymodule in a real browser.
   */
  public function testMyModule(): void {
    // ...
  }

}

Make an assertion

The following example navigates to `/node/add/page`, then asserts that the "Summary" field is not visible by default.

  public function testMyModule(): void {
    $this->drupalGet('node/add/page');
    $widget = $this->getSession()->getPage()->findById('edit-body-wrapper');
    $summary_field = $widget->findField('edit-body-0-summary');
    $this->assertEquals(FALSE, $summary_field->isVisible(), 'Summary field is hidden by default.');
  }

Navigating pages

In there you can do ordinary HTTP requests using drupalGet, but also click on links/press buttons. When pressing a submit button (that is not Ajaxified) the page will submit and load the resulting page. After the page is loaded the test will continue.

NOTE: The test continues on the load event. If AJAX behaviors are triggered on load (for example in behaviors) you should add a $this->assertSession()->assertWaitOnAjaxRequest() after the load or, better yet, if you are waiting for elements to appear, you can use a waitForX method, see Waiting.

Finding and using elements

After loading a page you can use various methods of finding elements on the page. The most common methods for this are (there are other methods but these are the most used):

$page = $this->getSession()->getPage();

// Find the submit button labeled 'Save'
$button = $page->findButton('Save');

// Find the field with the name 'test'
$field = $page->findField('field_test[0][value]');

// Find links
$link = $page->findLink('Link text');

// Find using css
$element = $page->find('css', 'css selector');

Before using an element you should always check if it was found:

$this->assertNotEmpty($field);

After this, you can start changing/using the element. For example, you can now check the visibility of an element, see the following code examples:

$page = $this->getSession()->getPage();
$content = $page->findLink('Content');
$this->assertTrue($content->isVisible());

AJAX form interactions are also supported using classes Session, DocumentElement, and NodeElement. The following will run the AJAX behaviors if there are any attached:

$this->getSession()->getPage()->find('css', '#somebutton')->click();

Waiting for execution (AJAX)

When clicking a link/button with AJAX behavior attached, you need to keep in mind that the underlying browser might need a while to deliver changes to the HTML. This can also be true for other JavaScript interactions, like when you choose a radio button that makes other parts of the UI open or click on a vertical tab opener link and then needs to interact with the part of the form that is in that tab. The preferred method for this is to wait until an expected piece of UI is available. There are various methods available for this in WebDriverWebAssert:

  • ::waitForElement()
  • ::waitForElementVisible()
  • ::waitForButton()
  • ::waitForLink()
  • ::waitForField()
  • ::waitForId()

The methods work similarly to the find...() methods in that they return an element when found.

Here is some code that will wait up to 10 seconds for an element to appear, and return the element if it does, FALSE if it doesn't:

// $type can be either 'css' or 'xpath' here, and $selector will need to change accordingly
function waitForAppearance($type, $selector) {
  $page = $this->getSession()->getPage();
  return $page->waitFor(10,
      function() use ($type, $selector, $page) {
        return $page->find($type, $selector);
     });
}

There are edge cases where you have to directly wait for the AJAX request to finish. Use $this->assertSession()->assertWaitOnAjaxRequest() to wait for that.

As a last resort you can use a custom JavaScript snippet code that needs to pass:

$this->assertJsCondition('JavaScript condition that should equal TRUE');

You should always avoid waiting for a specific number of (milli)seconds. This would cause random failures in tests as sometimes the test will run longer than you'd expect, or you will wait too long causing the test to take more time than necessary.

Debugging

Review the HTML of each page load

You can output the HTML that the test browser has after an AJAX request. This will be added to the test HTML output that is produced by normal page requests.

// Get the Mink page element.
$page = $this->getSession()->getPage();

// Change a form element that will react with AJAX, and wait for the request to
// complete.
$page->selectFieldOption('my-field', '1');
$this->assertSession()->assertWaitOnAjaxRequest();

// Output the new HTML.
$this->htmlOutput($page->getHtml());

Take screenshots

If you want to see a screenshot to work out what is going on you can do this! (introduced in 8.1.9)

$this->createScreenshot('PATH/TO/screenshot.png');

Don't put the screenshot in a path inside the test environment like public://test.jpg as this will be cleaned up at the end of the test.

Read more about the context of these changes at #2807237: PHPUnit initiative.

Screenshots on Drupal CI

If you want to see a screenshot to work out what is going on when the Drupal CI testbot runs your tests, you can also do this! To achieve this, you have to write the screenshot to a directory which will be in the build artifacts. The folder sites/default/files/simpletest is such a folder.

Example code:

$this->createScreenshot(\Drupal::root() . '/sites/default/files/simpletest/screen.png');

You can then inspect your screenshot by clicking your way to this file:

  • Click the test result on the issue
  • Click View results on the dispatcher
  • Expand the following path under Build Artifacts: simpletest.js (or run_tests.js) -> phpunit-xml

Log output

Errors during AJAX calls produce no visible error output. The log_stdout module allows errors to show in the normal output.

Help improve this page

Page status: Needs work

You can: