Skip to content

PHPUnit FunctionalJavascript Tests

When to Use

Write FunctionalJavascript tests only when testing features that require JavaScript execution: AJAX form elements, autocomplete fields, modal dialogs, drag-and-drop interfaces, or client-side validation.

Decision

If you need... Use FunctionalJavascript Why
Test AJAX interactions Yes Real browser executes JavaScript
Test autocomplete fields Yes Requires JavaScript event handling
Test modal dialogs Yes JavaScript-driven UI components
Test drag-and-drop Yes Browser DOM manipulation needed
Test server-rendered forms No, use Functional Faster without browser overhead
Test pure logic No, use Unit/Kernel No UI needed

Pattern

<?php
namespace Drupal\Tests\my_module\FunctionalJavascript;

use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests the my_module AJAX form interactions.
 */
#[Group('my_module')]
#[RunTestsInSeparateProcesses]
class AjaxFormTest extends WebDriverTestBase {

  protected $defaultTheme = 'stark';
  protected static $modules = ['my_module', 'node'];

  public function testAjaxDependentDropdown(): void {
    $this->drupalLogin($this->drupalCreateUser(['access content']));

    $session = $this->getSession();
    $page = $session->getPage();
    $assert_session = $this->assertSession();

    $this->drupalGet('my-module/ajax-form');

    // Trigger AJAX request by selecting option
    $page->selectFieldOption('category', 'electronics');

    // CRITICAL: Wait for AJAX to complete
    $assert_session->assertWaitOnAjaxRequest();

    // Verify AJAX-loaded content
    $assert_session->elementExists('css', '#subcategory-wrapper');
    $assert_session->fieldExists('subcategory');
    $assert_session->optionExists('subcategory', 'smartphones');

    // Test cascading AJAX
    $page->selectFieldOption('subcategory', 'smartphones');
    $assert_session->assertWaitOnAjaxRequest();

    $assert_session->elementExists('css', '#brand-wrapper');
    $assert_session->fieldExists('brand');
  }

  public function testModalDialog(): void {
    $this->drupalLogin($this->drupalCreateUser(['access content']));

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

    $this->drupalGet('my-module/content-list');

    // Open modal via AJAX
    $page->clickLink('Add New Item');

    // Wait for modal to appear
    $assert_session->waitForElement('css', '.ui-dialog');
    $assert_session->elementExists('css', '.ui-dialog-title');
    $assert_session->elementTextContains('css', '.ui-dialog-title', 'Add Item');

    // Fill and submit modal form
    $page->fillField('title', 'Test Item');
    $page->fillField('description', 'Test description');
    $page->pressButton('Save');

    // Wait for AJAX submission
    $assert_session->assertWaitOnAjaxRequest();

    // Verify modal closed and item added
    $assert_session->elementNotExists('css', '.ui-dialog');
    $assert_session->pageTextContains('Test Item');
  }

  public function testAutocomplete(): void {
    $this->drupalLogin($this->drupalCreateUser(['access content']));

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

    $this->drupalGet('my-module/autocomplete-form');

    // Type to trigger autocomplete
    $page->fillField('user_search', 'adm');

    // Wait for autocomplete results
    $assert_session->waitForElement('css', '.ui-autocomplete');
    $assert_session->elementExists('css', '.ui-autocomplete li');
    $assert_session->elementTextContains('css', '.ui-autocomplete', 'admin');

    // Select autocomplete result
    $page->find('css', '.ui-autocomplete li:first-child')->click();

    // Verify selection
    $this->assertEquals('admin', $page->findField('user_search')->getValue());
  }
}

File Location: modules/my_module/tests/src/FunctionalJavascript/AjaxFormTest.php

Common Mistakes

  • Not calling assertWaitOnAjaxRequest() after AJAX triggers → Race conditions, flaky tests
  • Using drupalGet() and immediately checking for AJAX content → Content not loaded yet
  • Not configuring ChromeDriver/WebDriver → Tests fail with connection errors
  • Writing FunctionalJavascript tests for non-JavaScript features → Unnecessary slowness
  • Using sleep() instead of proper waits → Unreliable timing, slow tests

WHY these are mistakes: JavaScript execution is asynchronous. Tests must explicitly wait for AJAX requests to complete or elements to appear. Using fixed sleep() calls creates flaky tests that sometimes pass and sometimes fail based on system load. FunctionalJavascript tests are the slowest - only use them when JavaScript is required. ChromeDriver must be running as a separate process before tests execute.

See Also