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. It also works with RTL layouts.
It also optionally tests pages from the Theming Tools contrib module when it's installed.
| Command | Description | Options |
|---|---|---|
ddev vrt |
Run visual regression tests against baselines | --normal — narrow + wide, LTR only (skip prompt) |
--full — all viewports and RTL (skip prompt) |
||
--no-bail — don't stop after 5 failures |
||
--project=<name> — run a single viewport (narrow, mid, wide, rtl-narrow, rtl-mid, rtl-wide) |
||
--debug — run with Playwright inspector |
||
ddev vrt-update |
Capture or update baseline screenshots | --normal — narrow + wide, LTR only (skip prompt) |
--full — all viewports and RTL (skip prompt) |
||
--project=<name> — update a single viewport |
||
ddev vrt-report |
Serve the HTML diff report | — |
This add-on:
- Screenshots admin pages at three viewport widths (narrow 375px, mid 768px, wide 1280px) in both LTR and RTL
- Two test modes: Normal (narrow + wide, LTR only) for fast feedback, or Full (all viewports + RTL)
- 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
- 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)
Run the following to install the default_admin theme, set it as both the default and admin theme, and clear the cache:
ddev drush theme:install default_admin -y
ddev drush config:set system.theme default default_admin -y
ddev drush config:set system.theme admin default_admin -y
ddev drush config:set system.performance css.preprocess 0 -y
ddev drush config:set system.performance js.preprocess 0 -y
ddev drush cr# 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 vrtYou'll be prompted to choose a test mode:
- Normal (default) — runs narrow + wide viewports, LTR only. Faster for everyday development.
- Full — runs all viewports (narrow, mid, wide) and RTL variants.
To skip the prompt, pass --normal or --full:
ddev vrt --normal # Skip prompt, run normal mode
ddev vrt --full # Skip prompt, run full modeThe prompt is also skipped when you pass --project directly (e.g., ddev vrt --project=narrow).
If all screenshots match, you'll see all tests pass. If there are visual differences, the tests will fail and Playwright will generate diff images.
By default, the run stops early after 5 test failures to save time. Use --no-bail to run all tests regardless of failures.
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-updateLike ddev vrt, this prompts for normal/full mode. Use --normal or --full to skip the prompt.
# 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.
The add-on optionally tests pages from the Theming Tools contrib module — a suite of test submodules that each exercise a specific UI component (buttons, dialogs, tables, form widgets, etc.).
When theming_tools is installed and its submodules are enabled, the VRT suite automatically screenshots their test pages alongside the core admin pages. If theming_tools is not installed, these tests are silently skipped.
Install the module in your Drupal project:
ddev composer require 'drupal/theming_tools:1.0.x-dev@dev'
ddev drush en theming_toolsThen enable whichever test submodules you want to screenshot:
# Enable a few specific ones
ddev drush en button dialog table dropbutton textform
# Or enable all of them at once via the dashboard
# Visit /admin/modules/theming-tools after enabling theming_toolsSome submodules (like checkboxradio, textarea, textform, select) depend on the contact module. Install it if needed:
ddev composer require drupal/contact
ddev drush en contactOnce the submodules are enabled, capture baselines and run tests as usual:
ddev vrt-update
ddev vrtTests for submodules that aren't enabled will be automatically skipped — you don't need to do anything special.
| Component | Pages |
|---|---|
| Action Link | Action link variants |
| Autocomplete | Autocomplete widget |
| Buttons | Button variants, disabled state |
| Checkbox & Radio | Checkbox/radio contact form |
| Dialog | Dialog page + modal interaction |
| Dropbutton | Dropbutton, operations, Views |
| Field Cardinality | Multi-value field widgets |
| Fieldset | Fieldset component |
| Image & File | File and image upload widgets |
| Message | Short and long messages |
| Pager | Pager component |
| Password | Password confirm widget |
| Prefix/Suffix | Text and number prefix/suffix |
| Progress | Progress indicators |
| Select | Select widgets |
| Tabs | Local task tabs |
| Table | Table component |
| Tabledrag | Draggable tables, nested |
| Textarea | Plain and formatted textareas |
| Text Form | Text-like form items |
The page definitions are in .ddev/drupal-admin-vrt/page-definitions/theming-tools-pages.ts.
ddev vrt tests/vrt/theming-tools.spec.ts
ddev vrt-update tests/vrt/theming-tools.spec.tsTo 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 admin pages to test
│ └── theming-tools-pages.ts # Theming Tools module pages (optional)
└── 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
└── theming-tools.spec.ts # Theming Tools tests (auto-skipped if not installed)
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.