# Drupal Functional Test

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-trebormc-drupal-ai-agents-drupal-functional-test`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [trebormc](https://agentstack.voostack.com/s/trebormc)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [trebormc](https://github.com/trebormc)
- **Source:** https://github.com/trebormc/drupal-ai-agents/tree/main/.claude/skills/drupal-functional-test

## Install

```sh
agentstack add skill-trebormc-drupal-ai-agents-drupal-functional-test
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Drupal Functional Test

## What It Is

Installs a complete Drupal site and simulates HTTP requests with Mink + BrowserKit.
It is NOT a real browser. JavaScript does NOT execute.

Speed: 4-120 seconds per class (installs Drupal from scratch per class).

## When to Use

- Module configuration forms (admin forms)
- Content creation/editing/deletion via UI
- Verify permissions: without permission -> 403, with permission -> 200
- Verify HTML output of pages
- Verify that blocks appear where they should
- Test redirects and routes
- Verify status/error messages

## When NOT to Use

- If you can test the same logic with Kernel test -> use Kernel (10x faster)
- If you need JavaScript/AJAX -> FunctionalJavascript
- If it is pure PHP logic -> Unit test

## Required Properties

```php
protected $defaultTheme = 'stark';       // REQUIRED. Use 'stark' for speed.
protected static $modules = ['my_module']; // Required modules.
// protected $profile = 'testing';        // Default. Only use 'standard' if you need its config.
```

## Base Template

NOTE: if the flow needs JavaScript/AJAX, switch to the **drupal-functionaljs-test** skill. `assertSession()->statusCodeEquals()` works HERE but does NOT work in FunctionalJavascript tests.

```php
drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
    $this->adminUser = $this->drupalCreateUser(['administer MODULE']);
  }

  public function testSomething(): void {
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/config/MODULE/settings');
    $this->assertSession()->statusCodeEquals(200);
  }

}
```

## Pattern: Configuration Form Test

```php
adminUser = $this->drupalCreateUser(['administer my_module']);
  }

  public function testFormSavesValues(): void {
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/config/my-module/settings');

    $assert = $this->assertSession();
    $assert->statusCodeEquals(200);
    $assert->fieldExists('max_items');

    $this->submitForm([
      'max_items' => '25',
      'cache_enabled' => TRUE,
    ], 'Save configuration');

    $assert->statusMessageContains('The configuration options have been saved', 'status');

    $config = $this->config('my_module.settings');
    $this->assertSame(25, $config->get('max_items'));
    $this->assertTrue($config->get('cache_enabled'));

    // Verify the form displays saved values
    $this->drupalGet('admin/config/my-module/settings');
    $assert->fieldValueEquals('max_items', '25');
    $assert->checkboxChecked('edit-cache-enabled');
  }

  public function testFormValidation(): void {
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/config/my-module/settings');

    $this->submitForm(['max_items' => '-5'], 'Save configuration');

    $this->assertSession()->statusMessageExists('error');
  }

  public function testFormRequiresPermission(): void {
    $user = $this->drupalCreateUser(['access content']);
    $this->drupalLogin($user);
    $this->drupalGet('admin/config/my-module/settings');
    $this->assertSession()->statusCodeEquals(403);
  }

}
```

## Pattern: Node Creation and Editing Test

```php
/**
 * @group my_module
 */
class ArticleFormTest extends BrowserTestBase {

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

  protected function setUp(): void {
    parent::setUp();
    $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
    $this->author = $this->drupalCreateUser([
      'access content',
      'create article content',
      'edit own article content',
      'delete own article content',
    ]);
  }

  public function testCreateArticle(): void {
    $this->drupalLogin($this->author);
    $this->drupalGet('node/add/article');
    $this->assertSession()->statusCodeEquals(200);

    $this->submitForm([
      'title[0][value]' => 'Test Article',
      'body[0][value]' => 'Body content.',
    ], 'Save');

    $assert = $this->assertSession();
    $assert->statusMessageContains('has been created', 'status');
    $assert->pageTextContains('Test Article');

    $node = $this->drupalGetNodeByTitle('Test Article');
    $this->assertNotNull($node);
    $this->assertSame('article', $node->bundle());
  }

  public function testEditArticle(): void {
    $this->drupalLogin($this->author);
    $node = $this->drupalCreateNode([
      'type' => 'article',
      'title' => 'Original',
      'uid' => $this->author->id(),
    ]);

    $this->drupalGet('node/' . $node->id() . '/edit');
    $this->assertSession()->fieldValueEquals('title[0][value]', 'Original');

    $this->submitForm(['title[0][value]' => 'Updated'], 'Save');
    $this->assertSession()->statusMessageContains('has been updated', 'status');
  }

  public function testDeleteArticle(): void {
    $this->drupalLogin($this->author);
    $node = $this->drupalCreateNode([
      'type' => 'article',
      'title' => 'To Delete',
      'uid' => $this->author->id(),
    ]);

    $this->drupalGet('node/' . $node->id() . '/delete');
    $this->submitForm([], 'Delete');
    $this->assertSession()->statusMessageContains('has been deleted', 'status');
  }

  public function testOtherUserCannotEdit(): void {
    $other = $this->drupalCreateUser(['access content', 'edit own article content']);
    $node = $this->drupalCreateNode([
      'type' => 'article',
      'uid' => $this->author->id(),
    ]);

    $this->drupalLogin($other);
    $this->drupalGet('node/' . $node->id() . '/edit');
    $this->assertSession()->statusCodeEquals(403);
  }

}
```

## Pattern: Page Output and Blocks Test

```php
public function testBlockAppears(): void {
  $this->drupalLogin($this->adminUser);
  $this->drupalPlaceBlock('my_module_stats_block', [
    'region' => 'content',
    'label' => 'Stats',
    'label_display' => TRUE,
  ]);

  $this->drupalGet('');
  $assert = $this->assertSession();
  $assert->pageTextContains('Stats');
  $assert->elementExists('css', '.block-my-module-stats-block');
}
```

## Assertion Reference -- $this->assertSession()

```php
$assert = $this->assertSession();

// HTTP status
$assert->statusCodeEquals(200);

// Visible text (no HTML)
$assert->pageTextContains('Welcome');
$assert->pageTextNotContains('Access denied');

// Raw HTML
$assert->responseContains('');

// Form fields
$assert->fieldExists('title[0][value]');
$assert->fieldValueEquals('title[0][value]', 'Expected');
$assert->checkboxChecked('edit-status-value');

// CSS elements
$assert->elementExists('css', '.my-class');
$assert->elementNotExists('css', '.should-not-exist');
$assert->elementTextContains('css', 'h1', 'Title');
$assert->elementsCount('css', '.item', 5);

// Links and buttons
$assert->linkExists('Log out');
$assert->linkByHrefExists('/node/add');
$assert->buttonExists('Save');

// Status messages (Drupal 9.3+)
$assert->statusMessageContains('has been created', 'status');
$assert->statusMessageExists('error');
$assert->statusMessageNotExists('error');

// URL
$assert->addressEquals('node/1');
```

## Navigation Methods

```php
$this->drupalGet('admin/config');
$this->drupalLogin($account);
$this->drupalLogout();
$this->submitForm(['field' => 'value'], 'Save');
$this->clickLink('Edit');
$node = $this->drupalCreateNode(['type' => 'article', 'title' => 'Test']);
$node = $this->drupalGetNodeByTitle('Test');
```

## Anti-Patterns

1. Do not use `drupalPostForm()` -- it is deprecated. Use `submitForm()`.
2. Do not verify business logic with Functional tests if a Kernel test works.
3. Do not forget `$defaultTheme`. Without it the test fails.
4. Do not depend on theme markup. Search by text or classes you control.
5. Do not use `assertSession()->statusCodeEquals()` if you will later migrate to FunctionalJS (it does not support it).
6. Data provider methods must be `public static function` (Drupal 10+11 compatibility).

## Execution Command

First pick the config form ONCE per session: `ssh web test -f phpunit.xml && echo "ROOT" || echo "CORE"`

```bash
# Form ROOT (project phpunit.xml exists):
ssh web ./vendor/bin/phpunit $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Functional
ssh web ./vendor/bin/phpunit --filter testFormSavesValues $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Functional

# Form CORE (no project phpunit.xml — pass env vars explicitly):
ssh web env SIMPLETEST_DB=mysql://db:db@db/db SIMPLETEST_BASE_URL=http://localhost \
  ./vendor/bin/phpunit -c $DDEV_DOCROOT/core $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Functional
```

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| Unexpected 403 in test | Test user lacks permission | Pass the exact permissions array to `drupalCreateUser([...])` |
| "Theme not found" | Missing `$defaultTheme` | Add `protected $defaultTheme = 'stark';` |
| "Test site directory exists already" | Stale test site from aborted run | `ssh web rm -rf $DDEV_DOCROOT/sites/simpletest/` then retry |
| "Failed to connect" / base URL errors | SIMPLETEST_BASE_URL not set | Use Form CORE with explicit env vars (above) |
| `Class not found` | Stale autoloader | `ssh web composer dump-autoload` |

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [trebormc](https://github.com/trebormc)
- **Source:** [trebormc/drupal-ai-agents](https://github.com/trebormc/drupal-ai-agents)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-trebormc-drupal-ai-agents-drupal-functional-test
- Seller: https://agentstack.voostack.com/s/trebormc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
