Drupal & DDEV Patterns
When to Use
Use these patterns for functional E2E against Drupal sites under DDEV. For DDEV plumbing (baseURL, ignoreHTTPSErrors, storage state), see the VR guide's Drupal section.
Decision
| Scenario | Approach |
|---|---|
| Cache rebuild after config change | execSync('ddev drush cr') then re-navigate; assert with expect(...).toContainText(...) |
| Deferred fields / lazy builders | Same as Big Pipe — assert against final content |
| Search index reindex | expect.toPass({ timeout: 60_000 }) polling search results |
| Entity CRUD | Create via JSON:API, drive UI for the feature under test, verify via JSON:API |
Pattern
Form API specifics
// Entity reference autocomplete — wait for AJAX dropdown
await page.getByLabel('Author').fill('admin');
await page.getByRole('option', { name: /^admin\s/ }).click();
Drupal forms include hidden fields (form_token, form_id, form_build_id). Submit via the actual button — Playwright fills visible inputs, hidden fields ride along.
AJAX-driven forms
// Wait for the response that matters
await Promise.all([
page.waitForResponse(r => r.url().includes('/system/ajax') && r.ok()),
page.getByRole('button', { name: 'Add another item' }).click(),
]);
// Wait for the throbber to disappear
await expect(page.locator('.ajax-progress, .throbber')).toHaveCount(0);
Big Pipe placeholders
// Reliable: wait for real content, not the placeholder
await expect(page.getByRole('region', { name: 'User account menu' }))
.toContainText(/Log out/);
// For elements that don't exist until Big Pipe replaces the placeholder:
await expect(myLocator).toBeAttached();
Session cookies (DDEV)
- Cookie name is
SESS<32-char-hash>over HTTP,SSESS<…>over HTTPS - DDEV defaults to HTTPS — expect
SSESS… - Don't hard-code the name — use
context.cookies()and filter by prefix
Drush from tests for setup
import { execSync } from 'child_process';
test.beforeAll(() => {
execSync('ddev drush en my_module -y', { stdio: 'inherit' });
execSync('ddev drush cr', { stdio: 'inherit' });
});
Wrap in a worker-scoped fixture if the cost is significant.
Entity CRUD — recommended pattern
- Create the node via JSON:API or Drush (fast, deterministic)
- Drive UI for the thing under test (publishing, paragraph editing, moderation transition)
- Verify via JSON:API
UI-creating a node touches dozens of fields, alters, and behaviors irrelevant to most tests.
Common Mistakes
- Wrong: Hardcoded
waitForTimeoutafter AJAX — usewaitForResponseor assertion auto-retry - Wrong: Not handling Big Pipe — first assertion fails on placeholder content
- Wrong:
ddev drush crbetween every test — kills suite time; use sparingly
See Also
- API Testing — the JSON:API + UI hybrid pattern in detail
- Authentication — Drupal session cookies and
storageState - ATK Integration — Drush-backed DB reset with Testor
- Reference: Lullabot/playwright-drupal — parallel SQLite per worker for full DB isolation