Project Vandal: UI Mutation Testing Engine
Your E2E suite is green, but would it catch a broken button? Vandal injects realistic UI faults (unclickable buttons, hidden elements, junk data, swallowed click handlers) into a live Playwright page and reports which "mutants" your tests failed to kill.
Project Vandal is a lightweight, non-LLM UI mutation testing engine integrated with Playwright. It allows you to sabotage your UI in subtle ways to verify if your end-to-end tests are resilient and capable of detecting regressions.
Learn more: Project Vandal: UI mutation testing · Project page · Tester's Guide · Follow @dkkkkd on X
Installation
Ensure you have Playwright installed:
# Install the engine from PyPI
pip install project-vandal
# Or clone for local development
git clone https://github.com/godhiraj-code/vandal.git
cd vandal
pip install .
# Setup Playwright
playwright install chromium
Usage
You can use the Vandal class as a context manager in your Playwright tests (Vandal uses Playwright's async API):
from vandal import Vandal
async with Vandal(page) as v:
# Apply a mutation to an element
await v.apply_mutation("stealth_disable", "#login-btn")
# Run your test logic
await page.click("#login-btn")
# Assert the outcome
assert await page.inner_text("#msg") == "Success"
If the test fails inside the async with block, the mutant is recorded as killed (your test noticed the sabotage). If it passes, the mutant survived, which points to a gap in your assertions. Call v.report() to print the results or v.save_report() to write vandal_report.json and vandal_report.html.
Features
- Mutation Strategies:
stealth_disable: Disables pointer events on elements, making them unclickable while remaining visually present.ghost_element: Makes elements invisible and unclickable.data_sabotage: Replaces text or input values with junk data.logic_sabotage: Prevents event propagation (e.g., stopping click handlers from firing).ui_shift: Moves elements 100px right and down with a CSS transform.slow_load: Hides elements for 5 seconds, then shows them again.
- Playwright Integration: Easy-to-use context manager (
Vandal) for wrapping test logic. - Shadow DOM and navigation: Mutations reach elements inside open shadow roots. By default (
persistent=True) they are also registered as an init script, so they are re-applied on every page loaded afterwards, including to elements added dynamically on those pages. - Reporting: Automated mutation report showing which "mutants" were killed (detected by tests) or survived.
- Detailed Guide: See TESTER_GUIDE.md for advanced usage and customization.
Project Structure
core.py: The heart of the mutation engine (JavaScript-based strategies).playwright_ext.py: Playwright wrapper and context manager.demo_vandal.py: A demonstration script showing Vandal in action.vandal_demo_app.html: A simple demo application for testing.
Running the Demo
The project includes a demo script that tests a simple login button.
Headless Mode
PYTHONPATH="." python demo_vandal.py --headless
Headed Mode
PYTHONPATH="." python demo_vandal.py --headed
In headed mode, the script includes a slow_mo delay and holds the browser open for 5 seconds after completion so you can see the results.
When to use Vandal (vs code mutation testing)
| Tool | What it mutates | What it tells you |
|---|---|---|
| mutmut, cosmic-ray | Your Python source code (operators, constants, conditions) | Whether your unit/integration tests notice code changes |
| Project Vandal | The rendered UI in a live browser page (pointer events, visibility, text/values, click handlers, position) | Whether your Playwright end-to-end tests notice a broken UI |
They answer different questions and work well together. Use Vandal when:
- you want evidence that a critical E2E test (login, checkout, a primary CTA) actually fails when that UI breaks
- you suspect tests are "too loose", for example they click with
force=Trueor never assert on the outcome - you are reviewing a new critical test before trusting it in CI
Vandal is not a good fit if you use Selenium or Cypress (only Playwright for Python is supported today), or if you want to measure unit-test quality (use a source-code mutation tool for that).
FAQ and Troubleshooting
Does it work with pytest?
Yes, with an async Playwright setup (for example pytest-asyncio). The Tester's Guide has a fixture pattern.
Does it support Playwright's sync API?
No. Vandal is an async context manager and apply_mutation() is a coroutine, so use playwright.async_api.
Why did my test raise an exception when the mutant was killed?
Vandal records the result but does not swallow the exception: a killed mutant means your test failed, and the failure propagates. Catch it around the async with block if you want the run to continue, as demo_vandal.py does.
"Warning: No elements found for selector"
The selector matched nothing on the current page when the mutation was applied. On the current page a mutation is applied once, immediately, so wait for the element first (await page.wait_for_selector(selector)). Alternatively, apply the mutation before page.goto(): persistent mutations then watch the new page and also hit elements that appear later.
My mutant survived but the UI was clearly broken.
Check for force=True on clicks. Playwright's actionability checks would otherwise catch stealth_disable and ghost_element.
Can I undo a mutation?
revert_mutation() reverts stealth_disable, ghost_element, data_sabotage and ui_shift on the current page. logic_sabotage and slow_load have no revert script. Persistent mutations are registered with page.add_init_script(), which Playwright can't remove, so use a fresh page or context for the next test.
Why "Vandal"?
Unlike traditional mutation testing that modifies source code, Project Vandal sabotages the UI layer directly in the browser. This is particularly effective for catching:
- CSS regressions that block interactions.
- Event listener bugs.
- Data binding failures.
License
MIT. See LICENSE.
Metadata
Release files for project-vandal 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| project_vandal-0.2.1.tar.gz | 13.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| project_vandal-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.8 kB
Release files / project_vandal-0.2.1.tar.gz
| Download URL | project_vandal-0.2.1.tar.gz |
|---|---|
| Size | 13.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0070b29f94eec342ef8f13c674ad1d760ff5a3b729f28202179992ef0c3d33eb
|
|
BLAKE2b-256 checksum How to use checksums |
5283161798fff407bec5d1e091bd68fecdcccff9f12b82f412931f393fbfb13b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / project_vandal-0.2.1-py3-none-any.whl
| Download URL | project_vandal-0.2.1-py3-none-any.whl |
|---|---|
| Size | 10.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1e23a34bd1f5c9206699eb59425a0d6378319549ca316ff90e8c6088be225f25
|
|
BLAKE2b-256 checksum How to use checksums |
17c688e599abbbabe4900b445a5df8cea1c7088b292d8535b8db5d655e98b6bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Security for project-vandal 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.