Install
$ agentstack add skill-trebormc-drupal-ai-agents-drupal-functionaljs-test ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Drupal FunctionalJavascript Test
What It Is
Extends BrowserTestBase but uses a real Chrome browser via WebDriver. JavaScript executes completely. AJAX works. CSS animations are disabled.
Speed: 10-170 seconds per class. Use ONLY when there is no alternative.
Critical Difference from BrowserTestBase
statusCodeEquals() does NOT work in WebDriverTestBase. The Selenium2 driver does not have access to HTTP status codes. Verify access with pageTextContains() or elementExists().
Base Template
adminUser = $this->drupalCreateUser(['access content', 'administer MODULE']);
}
public function testAjaxInteraction(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('route');
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$page->selectFieldOption('field', 'value');
$assert->assertWaitOnAjaxRequest();
$element = $assert->waitForElementVisible('css', '.result');
$this->assertNotEmpty($element);
}
}
GOLDEN RULE: Never sleep(), Always Waits
The #1 cause of flaky tests. NEVER sleep(). ALWAYS use waits:
$assert = $this->assertSession();
// Wait for AJAX (most used)
$assert->assertWaitOnAjaxRequest();
// Wait for element in DOM
$element = $assert->waitForElement('css', '.my-element');
$this->assertNotEmpty($element);
// Wait for VISIBLE element
$element = $assert->waitForElementVisible('css', '.dropdown');
// Wait for element to disappear
$assert->waitForElementRemoved('css', '.spinner');
// Specific waits
$assert->waitForButton('Submit');
$assert->waitForLink('Next');
$assert->waitForField('field_name');
$assert->waitForId('my-id');
$assert->waitForText('Done');
// Autocomplete
$assert->waitOnAutocomplete();
// Custom JS condition
$this->getSession()->wait(5000, 'jQuery("#el").is(":visible")');
Pattern: Form with AJAX
public function testDependentSelect(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/article');
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$page->selectFieldOption('field_country', 'ES');
$assert->assertWaitOnAjaxRequest();
$cityField = $assert->waitForElementVisible('css', '#edit-field-city');
$this->assertNotEmpty($cityField);
$options = $cityField->findAll('css', 'option');
$values = array_map(fn($o) => $o->getValue(), $options);
$this->assertContains('madrid', $values);
}
Pattern: Autocomplete (Entity Reference)
public function testAutocomplete(): void {
$this->drupalCreateNode(['type' => 'article', 'title' => 'Drupal Testing Guide']);
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/page');
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$field = $page->findField('field_related[0][target_id]');
$field->setValue('Drupal');
$assert->waitOnAutocomplete();
$suggestions = $page->findAll('css', '.ui-autocomplete li');
$this->assertGreaterThanOrEqual(1, count($suggestions));
$suggestions[0]->click();
$this->assertStringContainsString('Drupal Testing Guide', $field->getValue());
}
Pattern: Modal / Dialog
public function testModal(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/structure/block');
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$page->clickLink('Place block');
$modal = $assert->waitForElementVisible('css', '.ui-dialog');
$this->assertNotEmpty($modal);
$modal->fillField('Filter', 'Powered by');
$assert->waitForText('Powered by Drupal');
$modal->pressButton('Close');
$assert->waitForElementRemoved('css', '.ui-dialog');
}
Pattern: Visibility with #states
public function testConditionalVisibility(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('my-module/settings');
$page = $this->getSession()->getPage();
$field = $page->findField('api_key');
$this->assertFalse($field->isVisible());
$page->checkField('enable_api');
$this->assertTrue($field->isVisible());
}
Extra Capabilities
// Verify visibility (not possible in BrowserTestBase)
$this->assertFalse($element->isVisible());
// Execute JS
$this->getSession()->executeScript('document.title = "Test"');
// Evaluate JS (returns value)
$result = $this->getSession()->evaluateScript('return document.title');
// Browser drupalSettings
$settings = $this->getDrupalSettings();
// Screenshot for debug
$this->createScreenshot('/tmp/debug.png');
ChromeDriver Config
Drupal 10.3+ and 11:
Version guard: Drupal 10 (PHPUnit 9) still accepts plain chromeOptions (deprecated since 10.3); in Drupal 11 goog:chromeOptions is MANDATORY (without the goog: prefix it does not work). When in doubt, use goog:chromeOptions — it works on both.
Anti-Patterns
- Using
sleep(). NEVER. - Using
statusCodeEquals(). DOES NOT WORK in WebDriverTestBase. - Not waiting after AJAX. Will pass locally and fail in CI.
- Tests with 15+ interactions. That is an E2E flow -> Behat or Playwright.
- Testing things that do not need JS. Use Functional test.
Running Tests
ChromeDriver is already available inside the DDEV web container. Do not start it from the agent container (it would not be reachable by PHPUnit in web).
First pick the config form ONCE per session: ssh web test -f phpunit.xml && echo "ROOT" || echo "CORE"
# Form ROOT (project phpunit.xml exists — it must define MINK_DRIVER_ARGS_WEBDRIVER):
ssh web ./vendor/bin/phpunit $DDEV_DOCROOT/modules/custom/MODULE/tests/src/FunctionalJavascript
# Form CORE (no project phpunit.xml — pass ALL env vars explicitly, including the webdriver):
ssh web env SIMPLETEST_DB=mysql://db:db@db/db SIMPLETEST_BASE_URL=http://localhost \
MINK_DRIVER_ARGS_WEBDRIVER='["chrome", {"browserName":"chrome","goog:chromeOptions":{"args":["--disable-gpu","--headless","--no-sandbox","--disable-dev-shm-usage"]}}, "http://127.0.0.1:9515"]' \
./vendor/bin/phpunit -c $DDEV_DOCROOT/core $DDEV_DOCROOT/modules/custom/MODULE/tests/src/FunctionalJavascript
Troubleshooting
| Error | Cause | Fix | |-------|-------|-----| | Connection refused to ChromeDriver | Driver URL wrong or driver not running | Check the URL in MINKDRIVERARGS_WEBDRIVER; verify the driver runs in the web container | | Test flaky (passes sometimes) | Race condition after AJAX/JS | Replace any sleep() with assertWaitOnAjaxRequest() or waitForElementVisible() | | statusCodeEquals() fails/undefined | Not supported in WebDriverTestBase | Assert on page text/elements instead | | Chrome capability errors on D11 | Plain chromeOptions used | Use goog:chromeOptions |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: trebormc
- Source: trebormc/drupal-ai-agents
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.