A DDEV add-on that provides visual regression testing for Drupal's default_admin theme using Playwright. It screenshots 21 admin pages across 3 viewport sizes and compares them against baseline images, highlighting any visual differences.
This add-on:
- Screenshots admin pages at three viewport widths (narrow 375px, mid 768px, wide 1280px)
- Compares screenshots against baseline images using Playwright's built-in
toHaveScreenshot() - Reports visual differences with an interactive HTML report showing side-by-side diffs
- Handles authentication automatically via
drush uli(no manual login needed) - Runs inside the DDEV container so screenshots are consistent across macOS, Linux, and Windows — no "works on my machine" issues
- DDEV v1.24.0 or later
- A Drupal project running in DDEV with the
default_admintheme installed and set as the admin theme - An installed Drupal site with the database set up (run
ddev drush site:installif starting fresh) - Drush installed (
ddev composer require drush/drushif not already present) - The
default_admintheme installed and set as the admin theme:ddev drush theme:install default_admin -y ddev drush config:set system.theme admin default_admin -y
# Install the add-on
ddev add-on install https://github.com/mherchel/ddev-drupal-admin-vrt/tarball/main
# Restart DDEV to pick up the new docker-compose config
ddev restart
# Install Node.js dependencies and Chromium browser
ddev exec -d /var/www/html/.ddev/drupal-admin-vrt npm install
ddev exec -d /var/www/html/.ddev/drupal-admin-vrt npx playwright install --with-deps chromiumThe list of admin pages to screenshot is defined in:
.ddev/drupal-admin-vrt/page-definitions/admin-pages.ts
Edit this file to add, remove, or modify pages. See Adding pages for details and examples.
Run this on your reference branch (usually main) to establish the visual baseline:
git checkout main
ddev vrt-updateThis creates PNG screenshots in the __screenshots__/ directory at the project root, organized by viewport:
__screenshots__/
├── narrow/ # 375px viewport
├── mid/ # 768px viewport
└── wide/ # 1280px viewport
Switch to your feature branch and run the comparison:
git checkout feature/my-theme-change
ddev vrtIf all screenshots match, you'll see all tests pass. If there are visual differences, the tests will fail and Playwright will generate diff images.
After a failed comparison, view the interactive HTML report:
ddev vrt-reportThis serves the report at https://<projectname>.ddev.site:9324. The report shows:
- Side-by-side comparison (expected vs actual)
- A diff overlay highlighting changes
- A slider to toggle between versions
- Pass/fail status per test
You can also view raw diff images directly in test-results/ — for each failure Playwright writes three PNGs:
*-expected.png— the baseline*-actual.png— what was captured*-diff.png— differences highlighted
If your feature branch intentionally changes the UI, update the baselines:
ddev vrt-update# Run only the narrow viewport
ddev vrt --project=narrow
# Run only content section tests
ddev vrt tests/vrt/content.spec.ts
# Combine: wide viewport, structure section only
ddev vrt --project=wide tests/vrt/structure.spec.ts
# Update baselines for a specific section
ddev vrt-update tests/vrt/people.spec.tsThe add-on tests 21 admin pages grouped into 6 sections:
| Section | Pages |
|---|---|
| Content | Content overview, Add article, Add page |
| Structure | Overview, Content types, Block layout, Views, Taxonomy, Menus |
| Appearance | Theme list |
| Config | Overview, Site information, Performance, Text formats, File system |
| People | User list, Permissions, Roles |
| Reports | Status report, Recent log messages, Available updates |
Each page is screenshotted at 3 viewports = 63 total screenshots per run.
To add a new admin page to the test suite, edit .ddev/drupal-admin-vrt/page-definitions/admin-pages.ts and add an entry to the adminPages array:
{
id: 'config-logging', // Unique ID (used in screenshot filenames)
path: '/admin/config/development/logging', // URL path
section: 'config', // Section grouping
},Then capture its baseline:
ddev vrt-update| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier for screenshot filenames |
path |
Yes | URL path relative to the site root |
section |
Yes | Grouping: content, structure, appearance, config, people, or reports |
fullPage |
No | Capture the full scrollable page instead of just the viewport |
waitFor |
No | CSS selector to wait for before taking the screenshot |
maskSelectors |
No | Array of CSS selectors to mask (hide) in the screenshot |
timeout |
No | Custom timeout in ms for screenshot stability check (default: 5000) |
interactions |
No | Array of actions to perform before taking additional screenshots |
To screenshot a page after an interaction (opening a modal, clicking a button, etc.), use the interactions field:
{
id: 'structure-block-layout',
path: '/admin/structure/block',
section: 'structure',
interactions: [
{
label: 'place-block-modal', // Used in screenshot filename
action: async (page) => {
await page.getByRole('link', { name: 'Place block' }).first().click();
await page.locator('#drupal-modal').waitFor({ state: 'visible' });
await page.waitForTimeout(300); // Let animation settle
},
},
],
}Each interaction generates an additional screenshot named <id>--<label>.png.
-
Authentication: Before any tests run, the
auth-setupproject runsdrush uliinside the container to get a one-time login URL. It navigates to that URL and saves the authenticated session cookies. All subsequent tests reuse this session. -
Viewport projects: Playwright runs three projects in parallel (
narrow,mid,wide), each with a different viewport size. All three depend onauth-setupcompleting first. -
Screenshot comparison: Each test navigates to an admin page, waits for the page to load, and calls
toHaveScreenshot(). Playwright takes two screenshots 100ms apart to ensure stability, then compares against the baseline using pixel diffing. -
Dynamic content handling: A global CSS stylesheet (
fixtures/hide-dynamic.css) hides elements that change between runs (timestamps, CSRF tokens, etc.) to prevent false positives.
.ddev/
├── commands/web/
│ ├── vrt # ddev vrt command
│ ├── vrt-update # ddev vrt-update command
│ └── vrt-report # ddev vrt-report command
├── docker-compose.vrt-report.yaml # Exposes port 9324 for the report
└── drupal-admin-vrt/
├── playwright.config.ts # Playwright configuration
├── package.json
├── fixtures/
│ ├── auth.setup.ts # Automatic admin login
│ └── hide-dynamic.css # Hides timestamps, tokens, etc.
├── page-definitions/
│ └── admin-pages.ts # Central registry of pages to test
└── tests/vrt/
├── generate-vrt-tests.ts # Test generator (shared logic)
├── content.spec.ts # Content section tests
├── structure.spec.ts # Structure section tests
├── appearance.spec.ts # Appearance section tests
├── config.spec.ts # Config section tests
├── people.spec.ts # People section tests
└── reports.spec.ts # Reports section tests
Baselines and test output live in the project root:
project-root/
├── __screenshots__/ # Baseline PNGs
└── test-results/ # Diff output (gitignored)
The default comparison settings in playwright.config.ts:
maxDiffPixelRatio: 0.01— allows up to 1% of pixels to differ before failingthreshold: 0.2— per-pixel color sensitivity (0 = exact match, 1 = any color)animations: 'disabled'— disables CSS animations for stable screenshotscaret: 'hide'— hides the text cursor
To adjust these, edit .ddev/drupal-admin-vrt/playwright.config.ts.
To add more elements that should be hidden during screenshots (to prevent false positives), edit .ddev/drupal-admin-vrt/fixtures/hide-dynamic.css:
/* Example: hide a widget that shows random content */
.my-dynamic-widget {
visibility: hidden !important;
}For CI environments where drush uli may not be available, set environment variables for form-based login:
DRUPAL_ADMIN_USER=admin
DRUPAL_ADMIN_PASS=adminThe BASE_URL environment variable can override the default https://localhost if the Drupal site is at a different address in CI.
Drupal is not installed. Run ddev drush site:install (or your project's site install command) before running VRT tests.
You need to capture baselines before running comparisons. Run ddev vrt-update first.
Some pages (like the status report) contain content that changes between runs. Options:
- Add selectors to
hide-dynamic.cssto hide the changing elements - Add
maskSelectorsto the specific page definition inadmin-pages.ts - Increase
maxDiffPixelRatioin the config if small differences are acceptable
Large pages (like Permissions) may need more time for Playwright to confirm the screenshot is stable. Add a timeout to the page definition:
{
id: 'people-permissions',
path: '/admin/people/permissions',
section: 'people',
fullPage: true,
timeout: 30000,
},Run ddev restart to ensure the docker-compose port mapping is loaded, then try ddev vrt-report again.
| Command | Description |
|---|---|
ddev vrt |
Run visual regression tests against baselines |
ddev vrt-update |
Capture or update baseline screenshots |
ddev vrt-report |
Serve the HTML diff report |
ddev vrt --project=narrow |
Run only narrow (375px) viewport |
ddev vrt --project=mid |
Run only mid (768px) viewport |
ddev vrt --project=wide |
Run only wide (1280px) viewport |
ddev vrt tests/vrt/content.spec.ts |
Run only content section |
ddev vrt --debug |
Run with Playwright inspector |