A standard form submission is easy to test: the whole page reloads, and you assert on the result. An AJAX-driven field is different — nothing reloads, a small XHR request fires, and only a fragment of the page changes. Drupal's testing framework has a dedicated way to simulate exactly that, without needing a real browser or real JavaScript engine.
What you'll learn in this lesson
- Why AJAX interactions need a different test method than regular submission
- How to trigger an AJAX callback in a test with
submitForm()'s AJAX-aware sibling - What actually gets returned to a test versus what a real browser's JavaScript would do with it
- The limits of functional AJAX testing — and when you genuinely need a full browser instead
Triggering an AJAX-enabled element in a test
public function testColorDependsOnTemperature() {
$this->drupalGet('/examples/form-api-example/ajax-color-demo');
$assert = $this->assertSession();
$assert->fieldNotExists('color');
// Simulate selecting "Warm" from the Temperature select,
// which fires its #ajax callback.
$page = $this->getSession()->getPage();
$page->selectFieldOption('temperature', 'warm');
$this->assertSession()->assertWaitOnAjaxRequest();
// Now the color_wrapper's contents should have arrived via AJAX.
$assert->fieldExists('color');
}
This drives an actual headless browser session (via the Mink/WebDriver stack that BrowserTestBase's JavaScript-capable variant, WebDriverTestBase, sets up) — selectFieldOption() really changes the select element and really fires the change event, exactly like a person would. assertWaitOnAjaxRequest() pauses the test until Drupal's AJAX request-response cycle genuinely completes, so the assertion after it is checking real, settled DOM state.
BrowserTestBase vs. WebDriverTestBase
Plain BrowserTestBase (used in earlier lessons) doesn't execute JavaScript at all — it only sees the initial HTML response. That's fine for testing non-AJAX form submission, but an AJAX callback is triggered by client-side JavaScript, so testing it for real requires WebDriverTestBase, which drives a real headless Chrome instance. It's slower to set up and run, which is exactly why you reserve it for interactions that genuinely need JavaScript.
$form/$form_state. That skips the browser entirely and runs far faster; reserve the full WebDriverTestBase approach for genuinely verifying the end-to-end client-triggered behavior.Quick check: your AJAX test passes locally but intermittently fails in CI with the color field not found. What's the most likely cause? (A race condition — the test asserted on the field before
assertWaitOnAjaxRequest()genuinely finished waiting, or that assertion was skipped. AJAX tests are inherently more prone to timing issues than plain HTML-response tests, which is another reason to prefer the cheaper unit/kernel-test approach above whenever you don't specifically need to test the client-triggered path.)
Key takeaways
- Testing an AJAX callback's actual client-triggered behavior requires
WebDriverTestBase, not plainBrowserTestBase— only the WebDriver variant executes real JavaScript. assertWaitOnAjaxRequest()is essential after triggering an AJAX interaction — asserting too early is the most common source of flaky AJAX tests.- When you only need to verify an AJAX callback's server-side logic, call it directly in a unit/kernel test instead — it's faster and avoids browser-driven flakiness entirely.
- Reserve full WebDriver-based AJAX tests for the cases that genuinely need to prove the client-triggered path works, not as your default testing strategy.
Coming up next
You've now covered the full testing spectrum for forms — unit, functional, validation, submission, and AJAX. Next, we shift from writing tests to reading them when things go wrong: debugging a form's actual structure when it isn't rendering the way you expect.