| name | test-writer |
| description | Generate Playwright specs and PHPUnit tests for Drupal projects. Use when the user asks to write tests, generate test specs, improve test coverage, create Playwright tests, or write unit tests for services. |
| argument-hint | <what to test: feature, module, or component> |
Test Writer
Generate tests following established patterns.
Telemetry
f=$(ls ~/.claude/plugins/cache/*/drupal-sdlc/*/hooks/send-metric.sh 2>/dev/null | head -1); [ -n "$f" ] && bash "$f" "drupal-sdlc:test-writer" 2>/dev/null; true
Run this first — do not show output to user.
Playwright Config
!`cat tests/playwright/playwright.config.ts 2>/dev/null | head -30 || echo "No Playwright config found at tests/playwright/playwright.config.ts"`
Existing Test Structure
!`find tests/playwright/tests -type f -name '*.spec.ts' 2>/dev/null | head -20 || echo "No Playwright tests found"`
User's Request
$ARGUMENTS
Playwright Tests
File Placement
tests/playwright/tests/{category}/{name}.spec.ts
Categories: smoke/, content/{type}/, authoring/, forms/, accessibility/, permissions/, integrations/, regression/
Login Helper
Always use loginAsAdmin() from tests/playwright/helpers/auth.ts. Never write inline login code.
For multi-role tests, use loginAsUser(page, username, password) from the same helper. If it doesn't exist, generate it alongside the tests. See test-examples.md for the multi-role pattern.
Mandatory Tags (in test title)
Every test title must end with tags:
- Cost:
@fast (< 5s), @medium (5-15s), @slow (> 15s)
- Intent:
@smoke, @regression, @visual, @accessibility
- Behavior:
@readOnly (no data changes) or @mutates (creates/modifies content)
Drupal UI Selectors
- Title:
getByRole('textbox', { name: /Title \*/i })
- Body (CKEditor5):
getByRole('textbox', { name: 'Rich Text Editor' }) — never #edit-body-0-value
- Submit (Gin):
getByRole('button', { name: 'Save' }) — never #edit-submit
- Select fields:
getByRole('combobox', { name: 'Field Label' })
- Autocomplete:
getByRole('textbox', { name: 'Field Label' })
Selector Priority
getByRole > 2. getByLabel > 3. getByPlaceholder > 4. getByText > 5. locator('[data-drupal-selector]') (last resort)
Never use: #edit-submit, #edit-body-0-value, raw CSS class selectors.
Rules
- Relative URLs only —
page.goto('/dashboard')
- Regex with case-insensitive flag:
/Text/i
- No random data — fixed, deterministic values
- Always
await every Playwright action
- Always assert after every user action
PHPUnit Tests
Place at: web/modules/custom/{module}/tests/src/Unit/{Class}Test.php
Namespace: Drupal\Tests\{module}\Unit
Rules:
- Extend
Drupal\Tests\UnitTestCase
- Include
@group {module}
- No database — mock everything with
$this->createMock()
- Use
GuzzleHttp\Psr7\Response for HTTP mocking
Additional Resources
For complete real-world test examples, see test-examples.md.