wagtail-scenario-test
E2E scenario testing framework for Wagtail applications using Playwright.
Features
- WagtailAdmin Facade: Simple, intuitive API for Wagtail admin operations
- Page Object Pattern: Maintainable abstractions for Wagtail admin UI
- StreamFieldHelper: Fluent API for StreamField block manipulation
- PageAdminPage: Complete page lifecycle management (create, edit, publish, delete)
- Pytest Fixtures: Ready-to-use fixtures for authenticated browser sessions
- Factory Support: Base factories for test data creation
- Video Recording: Record and convert test videos to GIF
Installation
pip install wagtail-scenario-test
# Install Playwright browsers
playwright install
Quick Start
1. Write your first E2E test
import pytest
from wagtail_scenario_test import WagtailAdmin
@pytest.mark.e2e
@pytest.mark.django_db(transaction=True)
def test_create_snippet(authenticated_page, server_url):
"""Test creating a snippet through the admin UI."""
admin = WagtailAdmin(authenticated_page, server_url)
# Get snippet admin for your model
snippet = admin.snippet("myapp.mymodel")
# Create a new snippet
snippet.create(name="My New Item")
# Verify success
snippet.assert_success_message()
assert snippet.item_exists_in_list("My New Item")
2. Configure Django settings
# tests/settings.py (or conftest.py)
DJANGO_SETTINGS_MODULE = "tests.settings"
# Required settings
LANGUAGE_CODE = "en"
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "testserver"]
WAGTAIL_CONTENT_LANGUAGES = [("en", "English")]
3. Run tests
# Headless (for CI)
pytest tests/ -m e2e
# With browser visible
pytest tests/ -m e2e --headed
# With slow motion (for debugging)
pytest tests/ -m e2e --headed --slowmo=500
WagtailAdmin Facade
The WagtailAdmin class is the main entry point for E2E testing:
from wagtail_scenario_test import WagtailAdmin
def test_admin_operations(authenticated_page, server_url):
admin = WagtailAdmin(authenticated_page, server_url)
# Navigate
admin.go_to_dashboard()
# Search
admin.global_search("my query")
# Work with snippets
snippet = admin.snippet("blog.category")
snippet.create(name="Technology")
# Assertions
admin.assert_success_message()
admin.assert_success_message(contains="created")
Snippet Operations
snippet = admin.snippet("myapp.mymodel")
# Navigation
snippet.go_to_list() # Go to list view
snippet.go_to_add() # Go to add form
snippet.go_to_edit(123) # Go to edit form for ID 123
# CRUD
snippet.create(name="New Item") # Create with name field
snippet.create(name="Item", id_slug="item") # Create with additional fields
snippet.save() # Click Save button
snippet.delete() # Delete with confirmation
# List operations
count = snippet.get_item_count() # Count items in list
items = snippet.get_list_items() # Get all item titles
exists = snippet.item_exists_in_list("Name") # Check if item exists
snippet.click_item_in_list("Name") # Click to edit
# Assertions
snippet.assert_success_message()
snippet.assert_item_created("New Item")
snippet.assert_validation_error("This field is required")
Page Operations
from wagtail_scenario_test import PageAdminPage
page_admin = PageAdminPage(page, base_url)
# Navigation
page_admin.navigate_to_explorer() # Open page explorer panel
page_admin.edit_page(5) # Edit page by ID
# Page lifecycle
page_admin.save_draft() # Save as draft
page_admin.publish() # Publish the page
page_admin.unpublish() # Unpublish the page
page_admin.delete_page(5) # Delete page with confirmation
# Preview and live URLs
page_admin.visit_preview(5) # Navigate to preview
page_admin.visit_live(5) # Navigate to live page
url = page_admin.get_live_url() # Get live URL from editor
StreamField Operations
from wagtail_scenario_test import StreamFieldHelper, PageAdminPage
# Use within a page edit context
page_admin = PageAdminPage(page, base_url)
page_admin.edit_page(page_id)
# Initialize with field name (default: "body")
sf = StreamFieldHelper(page, "body")
# Add blocks
index = sf.add_block("Heading") # Returns new block index
sf.block(index).fill("My Heading") # Fill block content
# Work with StructBlocks
index = sf.add_block("Hero Section")
sf.block(index).struct("title").fill("Welcome")
sf.block(index).struct("subtitle").fill("To our site")
# Work with ListBlocks
index = sf.add_block("Links")
sf.block(index).item(0).struct("title").fill("Google")
sf.block(index).item(0).struct("url").fill("https://google.com")
# Block management
sf.delete_block(1) # Delete block at index
sf.move_block_up(1) # Move block up
sf.move_block_down(0) # Move block down
sf.reorder_blocks(0, 2) # Move block from position 0 to 2
# Query blocks
count = sf.get_block_count() # Get number of blocks
block_type = sf.get_block_type(0) # Get block type
is_deleted = sf.is_block_deleted(0) # Check if block is deleted
order = sf.get_block_order(0) # Get block order position
Fixtures
All fixtures are automatically available when the package is installed:
| Fixture | Scope | Description |
|---|---|---|
authenticated_page |
function | Playwright page logged into Wagtail admin |
authenticated_page_fast |
function | Faster auth using saved storage state |
authenticated_browser_context |
function | Browser context with pre-auth state |
server_url |
function | Base URL of the live test server |
admin_user_e2e |
function | Creates admin user for E2E testing |
admin_credentials |
function | Default admin credentials |
wagtail_site |
function | Creates Wagtail site with root page |
home_page |
function | Returns the root page (HomePage) |
test_page |
function | Creates a test page under home_page |
storage_state_path |
session | Path for storing auth state |
Usage Example
@pytest.mark.e2e
@pytest.mark.django_db(transaction=True)
def test_with_fixtures(authenticated_page, server_url):
# authenticated_page is already logged in
# server_url is the test server URL (e.g., "http://localhost:12345")
admin = WagtailAdmin(authenticated_page, server_url)
admin.go_to_dashboard()
Customizing Credentials
# conftest.py
import pytest
@pytest.fixture
def admin_credentials():
return {"username": "custom_admin", "password": "custom_password"}
Authentication Persistence (Storage State)
For faster test execution, use authenticated_page_fast which saves and reuses
login state across tests:
@pytest.mark.e2e
@pytest.mark.django_db(transaction=True)
def test_fast_auth(authenticated_page_fast, server_url):
# Uses saved storage state - no login required after first test
authenticated_page_fast.goto(f"{server_url}/admin/")
For multiple pages with shared auth:
def test_multi_page(authenticated_browser_context, server_url):
page1 = authenticated_browser_context.new_page()
page2 = authenticated_browser_context.new_page()
# Both pages are already logged in
You can also save storage state manually:
from wagtail_scenario_test import WagtailAdmin
def test_save_state(authenticated_page, server_url):
admin = WagtailAdmin(authenticated_page, server_url)
admin.save_storage_state("auth_state.json")
Page Objects
BasePage
Base class with common browser interactions:
from wagtail_scenario_test.page_objects import BasePage
class MyPage(BasePage):
def do_something(self):
self.goto("/my-path/")
self.click_button("Submit")
self.wait_for_navigation()
self.assert_visible("Success!")
Available Methods:
| Method | Description |
|---|---|
goto(path) |
Navigate to path |
reload() |
Reload current page |
current_path() |
Get current URL path |
wait_for_navigation() |
Wait for page load |
wait_for_element(selector) |
Wait for element visibility |
click_button(name) |
Click button by name |
click_link(name) |
Click link by name |
fill_field(label, value) |
Fill textbox by label |
fill_field_by_id(id, value) |
Fill field by ID |
select_option(label, value) |
Select dropdown option |
check_checkbox(label) |
Check checkbox |
assert_visible(text) |
Assert text is visible |
assert_url_contains(path) |
Assert URL contains path |
screenshot(name) |
Take screenshot |
get_page_content() |
Get page HTML |
Extending Page Objects
Create custom Page Objects for your specific models:
from wagtail_scenario_test.page_objects import SnippetAdminPage
class CategoryAdmin(SnippetAdminPage):
"""Page Object for Category snippet."""
def __init__(self, page, base_url):
super().__init__(
page, base_url,
app_name="blog",
model_name="category",
)
def create_with_color(self, name: str, color: str):
"""Create category with color field."""
self.go_to_add()
self.page.locator("#id_name").fill(name)
self.page.locator("#id_color").fill(color)
self.save()
Factory Support
from wagtail_scenario_test.utils import WagtailUserFactory, WagtailSuperUserFactory
# Create regular user
user = WagtailUserFactory()
# Create superuser
admin = WagtailSuperUserFactory()
# Custom attributes
admin = WagtailSuperUserFactory(username="custom_admin")
Video Recording & GIF Conversion
Record test videos using pytest-playwright's built-in --video option and automatically convert them to GIF format for documentation and debugging.
Record Videos
# Record videos for failed tests only
pytest tests/ -m e2e --video=retain-on-failure
# Record all test videos
pytest tests/ -m e2e --video=on
# Videos are saved to test-results/ directory
Convert Videos to GIF
Use the --gif option to automatically convert recorded videos to GIF format:
# Record and convert to GIF
pytest tests/ -m e2e --video=on --gif
# Customize GIF quality
pytest tests/ -m e2e --video=on --gif --gif-fps=15 --gif-width=1024
GIF Options:
| Option | Default | Description |
|---|---|---|
--gif |
off | Enable automatic GIF conversion |
--gif-fps |
10 | Frames per second (lower = smaller file) |
--gif-width |
800 | Width in pixels (height auto-scaled) |
Requirements: ffmpeg must be installed for GIF conversion.
# macOS
brew install ffmpeg
# Ubuntu/Debian
apt-get install ffmpeg
Programmatic Conversion
from wagtail_scenario_test.utils import convert_video_to_gif, is_ffmpeg_available
if is_ffmpeg_available():
gif_path = convert_video_to_gif("test-results/video.webm")
print(f"Created: {gif_path}")
Best Practices
1. Use the Facade
Prefer WagtailAdmin facade over direct Page Object usage:
# Good
admin = WagtailAdmin(page, url)
admin.snippet("myapp.mymodel").create(name="Test")
# Avoid (unless extending)
snippet = SnippetAdminPage(page, url, app_name="myapp", model_name="mymodel")
2. Mark E2E Tests
Always mark E2E tests for selective running:
@pytest.mark.e2e
@pytest.mark.django_db(transaction=True)
def test_something(authenticated_page, server_url):
...
3. Use Headed Mode for Debugging
# See what's happening
pytest tests/ -m e2e --headed --slowmo=500
# Pause on failure
pytest tests/ -m e2e --headed -x
4. Create Data via UI
For editing tests, create data through the UI to avoid transaction issues:
def test_edit_snippet(authenticated_page, server_url):
admin = WagtailAdmin(authenticated_page, server_url)
snippet = admin.snippet("myapp.mymodel")
# Create first, then edit
snippet.create(name="Original Name")
snippet.click_item_in_list("Original Name")
# Edit
snippet.page.locator("#id_name").fill("Updated Name")
snippet.save()
snippet.assert_success_message()
Requirements
- Python 3.10+
- Django 4.2+
- Wagtail 5.0+
- pytest 8.0+
- pytest-django 4.8+
- pytest-playwright 0.6.2+
- factory-boy 3.3+
License
MIT License
Metadata
Release files for wagtail-scenario-test 0.2.0
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 | Actions |
|---|---|---|---|---|
| wagtail_scenario_test-0.2.0.tar.gz | 69.0 kB | With provenance | Details for wagtail_scenario_test-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| wagtail_scenario_test-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for wagtail_scenario_test-0.2.0-py3-none-any.whl |
Total release size: 99.6 kB
Release files / wagtail_scenario_test-0.2.0.tar.gz
| Download URL | wagtail_scenario_test-0.2.0.tar.gz |
|---|---|
| Size | 69.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ac5fb1154e6307d2d12924b3bc4d80f78c747151cbdef7c04017315f7d0990bb
|
|
BLAKE2b-256 checksum How to use checksums |
ca0ec34aa52eb7e60d4a6bf667b47668b43cad79ee819766a7b55e4a18cb7273
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 24, 2025.
Transparency logRelease files / wagtail_scenario_test-0.2.0-py3-none-any.whl
| Download URL | wagtail_scenario_test-0.2.0-py3-none-any.whl |
|---|---|
| Size | 30.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff850deeab2aa9e39dd97ebac90c2b8b6ae93ccdc2df49d3f19d0e6fd0d1253e
|
|
BLAKE2b-256 checksum How to use checksums |
ea24b69da0e08dce1420afd707f9b466c3032ddb09021dfcdd9442827b7e9873
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 24, 2025.
Transparency logSecurity for wagtail-scenario-test 0.2.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for wagtail_scenario_test-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 24, 2025.
Transparency logA 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.