Files
pretix_original/src/tests/e2e/conftest.py
2026-03-17 14:17:15 +01:00

1258 lines
36 KiB
Python

#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
"""
E2E Test Configuration for Pretix Widget with Playwright
This module provides pytest fixtures for end-to-end testing of the pretix widget
using Playwright. It integrates Playwright with Django's test infrastructure.
"""
# TODO dev server websocket does not work, but is this relevant?
import os
import subprocess
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from urllib.error import URLError
from urllib.request import urlopen
import pytest
from django_scopes import scopes_disabled
from playwright.sync_api import ( # noqa: F401
Browser, BrowserContext, Page, expect,
)
from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, SubEvent, Voucher,
)
# Allow Django ORM operations in async context (required for Playwright integration)
os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true")
def _future_dt(days=30, hour=10, minute=0):
"""Build a future UTC datetime with a fixed time-of-day.
Uses a relative date so tests don't expire, but pins the time
component so results are deterministic regardless of when tests run.
"""
d = date.today() + timedelta(days=days)
return datetime(d.year, d.month, d.day, hour, minute, tzinfo=timezone.utc)
# ============================================================================
# Widget Asset Configuration (old Vue2 / new Vite / Vite dev server)
# ============================================================================
PROJECT_ROOT = os.path.join(
os.path.dirname(__file__),
'../../..'
)
VITE_DEV_PORT = 5180
@pytest.fixture(scope="session", autouse=True)
def _widget_assets():
"""
Build or check the widget JS depending on env vars.
- Default: old Vue2 widget (no build needed)
- PRETIX_WIDGET_VITE=1: run vite build, Django serves the output
- PRETIX_WIDGET_VITE_DEV=1: uses your already-running vite dev server
"""
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
try:
urlopen(f'http://localhost:{VITE_DEV_PORT}/', timeout=2)
except (URLError, OSError):
raise RuntimeError(
f'PRETIX_WIDGET_VITE_DEV is set but no Vite dev server found on port {VITE_DEV_PORT}. '
f'Start it with: npm run dev:widget'
)
yield
elif os.environ.get("PRETIX_WIDGET_VITE"):
subprocess.check_call(['npm', 'run', 'build:widget'], cwd=PROJECT_ROOT)
yield
else:
yield # Old widget, no build needed
# ============================================================================
# Playwright Configuration
# ============================================================================
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
"""Configure browser context for all tests."""
return {
**browser_context_args,
"viewport": {"width": 1280, "height": 720},
"locale": "en-US",
"timezone_id": "Europe/Berlin",
# Enable video recording for debugging (optional)
# "record_video_dir": "test-results/videos/",
}
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
"""Configure browser launch arguments."""
return {
**browser_type_launch_args,
# Uncomment for debugging
# "headless": False,
# "slow_mo": 500, # Slow down operations by 500ms
}
# ============================================================================
# Django Live Server Fixtures
# ============================================================================
@pytest.fixture
def live_server_url(live_server, settings):
"""
Get the live server URL.
Uses pytest-django's built-in live_server fixture which starts
a Django development server for E2E tests.
"""
# Enable django-compressor for on-the-fly SCSS compilation
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = False # Compile on-the-fly, not from cache
# Re-enable SCSS precompilers (disabled in test settings)
from pretix.testutils.settings import COMPRESS_PRECOMPILERS_ORIGINAL
settings.COMPRESS_PRECOMPILERS = COMPRESS_PRECOMPILERS_ORIGINAL
# Fix cache backend for compression
settings.COMPRESS_CACHE_BACKEND = 'default'
# Add testcache to CACHES if needed (for compatibility)
if 'testcache' not in settings.CACHES:
settings.CACHES['testcache'] = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
settings.SITE_URL = live_server.url
# Enable Vite widget if requested via env var
if os.environ.get("PRETIX_WIDGET_VITE") or os.environ.get("PRETIX_WIDGET_VITE_DEV"):
settings.PRETIX_WIDGET_VITE = True
return live_server.url
# ============================================================================
# Test Data Fixtures - Organizers and Events
# ============================================================================
@pytest.fixture
@scopes_disabled()
def organizer(db):
"""
Create an organizer for widget tests.
Reuses the same pattern as existing API tests.
"""
return Organizer.objects.create(
name='Test Organizer',
slug='testorg',
plugins='pretix.plugins.banktransfer,pretix.plugins.stripe'
)
@pytest.fixture
@scopes_disabled()
def event(organizer):
"""Create a basic event for widget tests."""
event = Event.objects.create(
organizer=organizer,
name='Test Event',
slug='testevent',
date_from=_future_dt(days=30, hour=10),
date_to=_future_dt(days=30, hour=18),
currency='EUR',
live=True,
testmode=False,
plugins='pretix.plugins.banktransfer',
)
event.set_defaults()
event.settings.set('timezone', 'Europe/Berlin')
event.settings.set('locale', 'en')
event.settings.set('locales', ['en'])
return event
@pytest.fixture
@scopes_disabled()
def items(event):
"""Create basic test items/products."""
from pretix.base.models import ItemCategory
items = []
# Create a proper category
category = ItemCategory.objects.create(
event=event,
name='Tickets',
position=0
)
# General Admission ticket
item1 = Item.objects.create(
event=event,
category=category,
name='General Admission',
default_price=Decimal('50.00'),
description='Standard entry ticket',
active=True,
)
items.append(item1)
# VIP ticket
item2 = Item.objects.create(
event=event,
category=category,
name='VIP Ticket',
default_price=Decimal('150.00'),
description='VIP access with special perks',
active=True,
)
items.append(item2)
# Create quotas for each item
for item in items:
quota = Quota.objects.create(
event=event,
name=f'{item.name} Quota',
size=100,
)
quota.items.add(item)
return items
# ============================================================================
# Test Data Fixtures - Items with Variations
# ============================================================================
@pytest.fixture
@scopes_disabled()
def item_with_variations(event):
"""Create an item with size variations (S, M, L, XL)."""
from pretix.base.models import ItemCategory
# Create category for the item
category = ItemCategory.objects.create(
event=event,
name='Merchandise',
position=1
)
item = Item.objects.create(
event=event,
category=category,
name='Event T-Shirt',
default_price=Decimal('25.00'),
description='Official event t-shirt',
active=True,
)
variations = []
sizes_and_prices = [
('Small', Decimal('20.00')),
('Medium', Decimal('25.00')),
('Large', Decimal('25.00')),
('X-Large', Decimal('30.00')),
]
for size, price in sizes_and_prices:
var = ItemVariation.objects.create(
item=item,
value=size,
default_price=price,
)
variations.append(var)
# Create quota for all variations
quota = Quota.objects.create(
event=event,
name='T-Shirt Quota',
size=50,
)
# Add both the item AND the variations to the quota
quota.items.add(item)
for var in variations:
quota.variations.add(var)
return item, variations
@pytest.fixture
@scopes_disabled()
def item_single_select(event):
"""Create an item with max_per_order=1 (should show checkbox)."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='VIP',
position=2
)
item = Item.objects.create(
event=event,
category=category,
name='VIP Pass',
default_price=Decimal('500.00'),
description='Limited VIP pass - one per customer',
active=True,
max_per_order=1,
)
quota = Quota.objects.create(
event=event,
name='VIP Quota',
size=10,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_free_price(event):
"""Create an item with pay-what-you-want pricing."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Donations',
position=3
)
item = Item.objects.create(
event=event,
category=category,
name='Donation',
default_price=Decimal('10.00'),
description='Support our cause',
active=True,
free_price=True,
)
quota = Quota.objects.create(
event=event,
name='Donation Quota',
size=999,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_sold_out(event):
"""Create a sold out item."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Early Bird',
position=4
)
item = Item.objects.create(
event=event,
category=category,
name='Early Bird Ticket',
default_price=Decimal('30.00'),
description='Sold out!',
active=True,
)
# Create quota with size=0 (sold out)
quota = Quota.objects.create(
event=event,
name='Early Bird Quota',
size=0,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_free(event):
"""Create a free item (price = 0.00)."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Free Stuff',
position=10
)
item = Item.objects.create(
event=event,
category=category,
name='Free Gift',
default_price=Decimal('0.00'),
active=True,
)
quota = Quota.objects.create(
event=event,
name='Free Gift Quota',
size=100,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_with_decimals(event):
"""Create an item with non-zero decimal price."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Test Category',
position=11
)
item = Item.objects.create(
event=event,
category=category,
name='Half Price Item',
default_price=Decimal('12.50'),
active=True,
)
quota = Quota.objects.create(
event=event,
name='Half Price Quota',
size=50,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_with_tax(event):
"""Create an item with tax rule."""
from pretix.base.models import ItemCategory, TaxRule
# Create tax rule
tax_rule = TaxRule.objects.create(
event=event,
name='VAT',
rate=Decimal('19.00'), # 19% VAT
)
category = ItemCategory.objects.create(
event=event,
name='Taxed Items',
position=12
)
item = Item.objects.create(
event=event,
category=category,
name='Taxed Product',
default_price=Decimal('100.00'),
tax_rule=tax_rule,
active=True,
)
quota = Quota.objects.create(
event=event,
name='Taxed Product Quota',
size=50,
)
quota.items.add(item)
return item
# ============================================================================
# Test Data Fixtures - Edge Cases
# ============================================================================
@pytest.fixture
@scopes_disabled()
def item_min_order(event):
"""Create an item with min_per_order=2."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Group Tickets',
position=13
)
item = Item.objects.create(
event=event,
category=category,
name='Group Pass',
default_price=Decimal('40.00'),
active=True,
min_per_order=2,
)
quota = Quota.objects.create(
event=event,
name='Group Pass Quota',
size=50,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_special_chars(event):
"""Create an item with special characters in the name."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Spezial',
position=14
)
item = Item.objects.create(
event=event,
category=category,
name='Böhm & Söhne Konzert',
default_price=Decimal('55.00'),
active=True,
)
quota = Quota.objects.create(
event=event,
name='Special Quota',
size=50,
)
quota.items.add(item)
return item
# ============================================================================
# Test Data Fixtures - Categories
# ============================================================================
@pytest.fixture
@scopes_disabled()
def items_with_category_description(event):
"""Create items with a category that has a description."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Tickets',
description='Early bird tickets available',
position=0
)
item = Item.objects.create(
event=event,
category=category,
name='Early Bird',
default_price=Decimal('35.00'),
active=True,
)
quota = Quota.objects.create(
event=event,
name='Early Bird Quota',
size=100,
)
quota.items.add(item)
return [item]
@pytest.fixture
@scopes_disabled()
def items_multiple_categories(event):
"""Create items in multiple categories to test grouping and ordering."""
from pretix.base.models import ItemCategory
cat_music = ItemCategory.objects.create(
event=event,
name='Music',
position=0
)
cat_food = ItemCategory.objects.create(
event=event,
name='Food & Drink',
position=1
)
item1 = Item.objects.create(
event=event,
category=cat_music,
name='Concert Ticket',
default_price=Decimal('75.00'),
active=True,
)
item2 = Item.objects.create(
event=event,
category=cat_food,
name='Food Pass',
default_price=Decimal('25.00'),
active=True,
)
for item in [item1, item2]:
quota = Quota.objects.create(
event=event,
name=f'{item.name} Quota',
size=100,
)
quota.items.add(item)
return [item1, item2]
# ============================================================================
# Test Data Fixtures - Vouchers
# ============================================================================
@pytest.fixture
@scopes_disabled()
def voucher(event, items):
"""Create a voucher for the event."""
voucher = Voucher.objects.create(
event=event,
code='TESTCODE2024',
max_usages=10,
price_mode='none',
)
# Clear the vouchers_exist cache so the widget picks it up
event.get_cache().delete('vouchers_exist')
return voucher
@pytest.fixture
@scopes_disabled()
def voucher_with_item(event, items):
"""Create a voucher tied to a specific item."""
item = items[0]
voucher = Voucher.objects.create(
event=event,
code='ITEMVOUCHER',
max_usages=5,
price_mode='percent',
value=Decimal('20.00'), # 20% off
item=item,
)
event.get_cache().delete('vouchers_exist')
return voucher
# ============================================================================
# Test Data Fixtures - Waiting List
# ============================================================================
@pytest.fixture
@scopes_disabled()
def item_sold_out_with_waitinglist(event):
"""Create a sold out item with waiting list enabled."""
from pretix.base.models import ItemCategory
# Enable waiting list on the event
event.settings.set('waiting_list_enabled', True)
category = ItemCategory.objects.create(
event=event,
name='Sold Out',
position=20
)
item = Item.objects.create(
event=event,
category=category,
name='Sold Out Concert',
default_price=Decimal('80.00'),
active=True,
allow_waitinglist=True,
)
# Create quota with size=0 (sold out)
quota = Quota.objects.create(
event=event,
name='Sold Out Quota',
size=0,
)
quota.items.add(item)
return item
# ============================================================================
# Test Data Fixtures - Event Series
# ============================================================================
@pytest.fixture
@scopes_disabled()
def event_series(organizer):
"""Create an event series with multiple subevents, items, and quotas."""
from pretix.base.models import ItemCategory
event = Event.objects.create(
organizer=organizer,
name='Concert Series',
slug='concert-series',
date_from=_future_dt(days=30, hour=19),
has_subevents=True,
currency='EUR',
live=True,
plugins='pretix.plugins.banktransfer',
)
event.set_defaults()
event.settings.set('timezone', 'Europe/Berlin')
event.settings.set('locale', 'en')
event.settings.set('locales', ['en'])
category = ItemCategory.objects.create(
event=event,
name='Tickets',
position=0
)
item = Item.objects.create(
event=event,
category=category,
name='Concert Ticket',
default_price=Decimal('45.00'),
active=True,
)
subevents = []
base_date = _future_dt(days=30, hour=19)
for i in range(15):
se = SubEvent.objects.create(
event=event,
name=f'Concert Night {i + 1}',
date_from=base_date + timedelta(days=i * 2),
date_to=base_date + timedelta(days=i * 2, hours=2),
active=True,
)
subevents.append(se)
# Each subevent needs its own quota
quota = Quota.objects.create(
event=event,
name=f'Concert {i + 1} Quota',
size=100,
subevent=se,
)
quota.items.add(item)
return event, subevents
# ============================================================================
# Widget Helper Fixtures
# ============================================================================
@pytest.fixture
def widget_page(page):
"""
Enhanced page fixture with widget-specific helper methods.
Provides convenience methods for common widget interactions.
"""
class WidgetPage:
def __init__(self, page: Page):
self.page = page
def goto(
self,
live_server_url: str,
org_slug: str,
event_slug: str,
wait=True,
**widget_attrs
):
"""
Navigate to a test page with widget embedded and wait for it to load.
Uses a Django view that serves an HTML page with the pretix
widget embedded, simulating how it would be used on a customer's
website.
Extra keyword arguments are passed as query params to the view,
which converts them to widget attributes. For boolean attributes
(like disable-vouchers), pass an empty string as value.
Set wait=False to skip waiting for the widget to load (useful for
tests that need to observe loading/error states).
"""
# Navigate to the test view URL
test_url = f"{live_server_url}/widget-test/{org_slug}/{event_slug}/"
if widget_attrs:
from urllib.parse import urlencode
test_url += '?' + urlencode(widget_attrs)
self.page.goto(test_url)
if wait:
self.wait_for_widget_load()
return self
def wait_for_widget_load(self):
"""Wait for widget to finish loading."""
self.page.wait_for_selector('.pretix-widget', timeout=15000)
# Wait for loading spinner to be hidden (widget has rendered content)
self.page.locator('.pretix-widget-loading').wait_for(state='hidden', timeout=15000)
return self
def wait_for_loading_indicator(self, timeout=15000):
"""Wait for the loading indicator to appear and then disappear (display: none)."""
loading = self.page.locator('.pretix-widget-loading')
loading.wait_for(state='visible', timeout=timeout)
loading.wait_for(state='hidden', timeout=timeout)
return self
def select_item_quantity(self, item_name: str, quantity: int):
"""Select quantity for an item by name."""
# Find the item row
item_row = self.page.locator(f'.pretix-widget-item:has-text("{item_name}")')
# Find number input within that row
number_input = item_row.locator('input[type="number"]').first
number_input.wait_for(state='visible', timeout=5000)
if number_input.count() > 0:
number_input.fill(str(quantity))
number_input.dispatch_event('change')
else:
# Maybe it's a checkbox (order_max=1)
checkbox = item_row.locator('input[type="checkbox"]').first
if quantity > 0:
checkbox.check()
return self
def select_variation_quantity(self, item_name: str, variation_name: str, quantity: int):
"""Select quantity for a specific variation."""
# Find item
item = self.page.locator(f'.pretix-widget-item:has-text("{item_name}")')
# Find variation within item using exact text match to avoid
# "Large" matching "X-Large"
variation = item.locator(
f'.pretix-widget-variation:has(strong:text-is("{variation_name}"))'
)
# Find input
input_field = variation.locator('input[type="number"]').first
input_field.fill(str(quantity))
input_field.dispatch_event('change')
return self
def click_buy_button(self):
"""Click the buy/register button."""
buy_button = self.page.locator("""
.pretix-widget-action button:has-text("Buy"),
.pretix-widget-action button:has-text("Register")
""")
buy_button.first.click()
return self
def wait_for_iframe_checkout(self):
"""Wait for checkout iframe to appear."""
self.page.wait_for_selector('.pretix-widget-frame-shown', timeout=15000)
# Wait for iframe to load
self.page.wait_for_function(
"""() => {
const iframe = document.querySelector('iframe[name^="pretix-widget-"]');
return iframe && iframe.src !== 'about:blank';
}""",
timeout=15000
)
iframe = self.page.frame_locator('iframe[name^="pretix-widget-"]')
return iframe
def close_iframe(self):
"""Close the checkout iframe and wait for the widget to reload.
The widget triggers a reload() when the iframe is closed
(without incrementing the loading counter), so we wait for
the XHR response to complete before returning.
"""
close_btn = self.page.locator('.pretix-widget-frame-close button')
# Wait for the reload XHR that fires when the iframe closes
with self.page.expect_response(
lambda r: 'widget/product_list' in r.url,
timeout=15000
):
close_btn.click()
self.page.locator('.pretix-widget-frame-shown').wait_for(
state='detached', timeout=5000
)
return self
def wait_for_view(self, selector: str, timeout=15000):
"""Wait for a specific element to appear after a view switch."""
self.page.locator(selector).first.wait_for(state='visible', timeout=timeout)
return self
def expand_variations(self, item_name: str):
"""Click the 'Show variants' button for an item."""
item = self.page.locator(f'.pretix-widget-item:has-text("{item_name}")')
toggle_btn = item.locator('button:has-text("Show variants"), button:has-text("variants")')
toggle_btn.click()
return self
def goto_button_test_page(
self,
live_server_url: str,
org_slug: str,
event_slug: str,
**query_params
):
"""Navigate to a test page with pretix-button embedded."""
from urllib.parse import urlencode
test_url = f"{live_server_url}/button-test/{org_slug}/{event_slug}/"
if query_params:
test_url += '?' + urlencode(query_params)
self.page.goto(test_url)
return self
return WidgetPage(page)
# ============================================================================
# Test Data Fixtures - Availability States
# ============================================================================
@pytest.fixture
@scopes_disabled()
def item_require_voucher(event):
"""Create an item that requires a voucher to purchase."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Voucher Only',
position=30
)
item = Item.objects.create(
event=event,
category=category,
name='Exclusive Pass',
default_price=Decimal('200.00'),
active=True,
require_voucher=True,
)
quota = Quota.objects.create(
event=event,
name='Exclusive Quota',
size=50,
)
quota.items.add(item)
return item
@pytest.fixture
@scopes_disabled()
def item_low_stock(event):
"""Create an item with low stock (quota_left visible)."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Limited',
position=31
)
item = Item.objects.create(
event=event,
category=category,
name='Last Chance Ticket',
default_price=Decimal('65.00'),
active=True,
)
quota = Quota.objects.create(
event=event,
name='Limited Quota',
size=3,
)
quota.items.add(item)
# Enable "show quota left" on the event
event.settings.set('show_quota_left', True)
return item
@pytest.fixture
@scopes_disabled()
def item_not_yet_available(event):
"""Create an item that is not yet available (future available_from)."""
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Coming Soon',
position=32
)
item = Item.objects.create(
event=event,
category=category,
name='Future Ticket',
default_price=Decimal('45.00'),
active=True,
available_from=_future_dt(days=365),
available_from_mode='info', # Show as "not yet available" instead of hiding
)
quota = Quota.objects.create(
event=event,
name='Future Quota',
size=100,
)
quota.items.add(item)
return item
# ============================================================================
# Test Data Fixtures - Items with Pictures
# ============================================================================
@pytest.fixture
@scopes_disabled()
def item_with_picture(event):
"""Create an item with a product picture."""
import io
from django.core.files.uploadedfile import SimpleUploadedFile
from PIL import Image as PILImage
from pretix.base.models import ItemCategory
category = ItemCategory.objects.create(
event=event,
name='Gallery Items',
position=40
)
# Create a small test image (100x100 red square)
img = PILImage.new('RGB', (100, 100), color='red')
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
picture_file = SimpleUploadedFile(
name='test_product.png',
content=buf.read(),
content_type='image/png'
)
item = Item.objects.create(
event=event,
category=category,
name='Art Print',
default_price=Decimal('35.00'),
description='Limited edition art print',
active=True,
picture=picture_file,
)
quota = Quota.objects.create(
event=event,
name='Art Print Quota',
size=50,
)
quota.items.add(item)
return item
# ============================================================================
# Cross-Browser Testing
# ============================================================================
@pytest.fixture(params=['chromium']) # Add 'firefox', 'webkit' when ready
def cross_browser_page(request, playwright):
"""
Test across multiple browsers.
Usage:
def test_widget_works_everywhere(cross_browser_page):
page = cross_browser_page
page.goto("...")
"""
browser_type = getattr(playwright, request.param)
browser = browser_type.launch()
context = browser.new_context()
page = context.new_page()
yield page
page.close()
context.close()
browser.close()
@pytest.fixture(scope='session', autouse=True)
def _register_widget_test_view():
"""
Register a test view that serves an HTML page with widget embedded.
This allows E2E tests to navigate to a real URL instead of using
set_content, which causes CORS issues.
"""
from django.http import HttpResponse
from django.urls import path
from django.views import View
from pretix.multidomain import maindomain_urlconf as urls
class WidgetTestView(View):
"""Serve HTML page with widget embedded for E2E testing."""
# Widget attributes that can be passed as query params
WIDGET_ATTRS = [
'items', 'categories', 'voucher', 'disable-vouchers',
'disable-iframe', 'subevent', 'list-type',
'display-event-info', 'skip-ssl-check',
]
def get(self, request, organizer, event):
base_url = f"{request.scheme}://{request.get_host()}"
event_url = f"{base_url}/{organizer}/{event}/"
widget_css = f"{base_url}/{organizer}/{event}/widget/v2.css"
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
script_tag = f'<script type="module" src="http://localhost:{VITE_DEV_PORT}/src/main.ts"></script>'
else:
widget_js = f"{base_url}/widget/v2.en.js"
script_tag = f'<script type="text/javascript" src="{widget_js}" async crossorigin></script>'
# Build extra attributes from query params
extra_attrs = ''
for attr in self.WIDGET_ATTRS:
val = request.GET.get(attr)
if val is not None:
if val == '':
# Boolean attribute (e.g., disable-vouchers)
extra_attrs += f' {attr}'
else:
extra_attrs += f' {attr}="{val}"'
# Always add skip-ssl-check so iframe checkout works on HTTP
if 'skip-ssl-check' not in extra_attrs:
extra_attrs += ' skip-ssl-check'
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Widget Test</title>
<link rel="stylesheet" type="text/css" href="{widget_css}" crossorigin>
</head>
<body>
<pretix-widget event="{event_url}"{extra_attrs}></pretix-widget>
{script_tag}
</body>
</html>"""
resp = HttpResponse(html, content_type='text/html')
resp['Content-Security-Policy'] = "script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'"
return resp
class ButtonTestView(View):
"""Serve HTML page with pretix-button element for E2E testing."""
def get(self, request, organizer, event):
base_url = f"{request.scheme}://{request.get_host()}"
event_url = f"{base_url}/{organizer}/{event}/"
widget_css = f"{base_url}/{organizer}/{event}/widget/v2.css"
if os.environ.get("PRETIX_WIDGET_VITE_DEV"):
script_tag = f'<script type="module" src="http://localhost:{VITE_DEV_PORT}/src/main.ts"></script>'
else:
widget_js = f"{base_url}/widget/v2.en.js"
script_tag = f'<script type="text/javascript" src="{widget_js}" async crossorigin></script>'
# Build extra attributes from query params
extra_attrs = ''
for attr in ['items', 'voucher', 'subevent', 'disable-iframe']:
val = request.GET.get(attr)
if val is not None:
if val == '':
extra_attrs += f' {attr}'
else:
extra_attrs += f' {attr}="{val}"'
button_text = request.GET.get('button-text', 'Buy tickets!')
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Button Test</title>
<link rel="stylesheet" type="text/css" href="{widget_css}" crossorigin>
</head>
<body>
<pretix-button event="{event_url}"{extra_attrs} skip-ssl-check>{button_text}</pretix-button>
{script_tag}
</body>
</html>"""
resp = HttpResponse(html, content_type='text/html')
resp['Content-Security-Policy'] = "script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'"
return resp
# Add URL patterns
test_pattern = path(
'widget-test/<str:organizer>/<str:event>/',
WidgetTestView.as_view()
)
button_pattern = path(
'button-test/<str:organizer>/<str:event>/',
ButtonTestView.as_view()
)
# Insert at beginning of URL patterns
if hasattr(urls, 'urlpatterns'):
urls.urlpatterns.insert(0, test_pattern)
urls.urlpatterns.insert(0, button_pattern)