diff --git a/src/tests/e2e/README.md b/src/tests/e2e/README.md deleted file mode 100644 index a53c6b527c..0000000000 --- a/src/tests/e2e/README.md +++ /dev/null @@ -1,480 +0,0 @@ -# Pretix Widget E2E Tests - -End-to-end tests for the pretix widget using Python Playwright and pytest. - -## Overview - -These tests verify the complete widget experience from a user's perspective, including: -- Widget embedding and initialization -- Product browsing and variations -- Quantity controls and free price inputs -- Cart management and checkout flows -- Iframe/new tab integration -- Responsive behavior and accessibility - -## Installation - -### Prerequisites - -- Python 3.11+ -- Virtual environment activated -- Django test database configured - -### Setup - -1. **Activate the virtual environment:** - ```bash - source env/bin/activate - ``` - -2. **Install pytest-playwright:** - ```bash - pip install pytest-playwright - ``` - -3. **Install browser binaries:** - ```bash - # Install all browsers (Chromium, Firefox, WebKit) - python -m playwright install - - # Or install specific browsers only - python -m playwright install chromium - python -m playwright install firefox - ``` - -4. **Verify installation:** - ```bash - pytest src/tests/e2e/ --collect-only - ``` - -## Running Tests - -### Basic Usage - -```bash -# Run all E2E tests -pytest src/tests/e2e/ - -# Run specific test file -pytest src/tests/e2e/test_widget_embedding.py - -# Run specific test -pytest src/tests/e2e/test_widget_cart.py::TestCartBasics::test_add_to_cart_and_open_checkout -``` - -### Browser Selection - -```bash -# Run with specific browser (default: chromium) -pytest src/tests/e2e/ --browser firefox -pytest src/tests/e2e/ --browser webkit - -# Run with multiple browsers -pytest src/tests/e2e/ --browser chromium --browser firefox -``` - -### Debugging Options - -```bash -# Show browser UI (headed mode) -pytest src/tests/e2e/ --headed - -# Slow down operations for observation (milliseconds) -pytest src/tests/e2e/ --headed --slowmo 1000 - -# Generate video recordings -pytest src/tests/e2e/ --video on - -# Keep browser open on failure -pytest src/tests/e2e/ --headed --pdb - -# Generate screenshots on failure -pytest src/tests/e2e/ --screenshot on -``` - -### Parallel Execution - -```bash -# Run tests in parallel (requires pytest-xdist) -pip install pytest-xdist -pytest src/tests/e2e/ -n 4 -``` - -### Verbose Output - -```bash -# Show detailed output -pytest src/tests/e2e/ -v - -# Show print statements -pytest src/tests/e2e/ -s - -# Show all captured output even for passing tests -pytest src/tests/e2e/ -v -s --capture=no -``` - -## Test Organization - -``` -src/tests/e2e/ -├── conftest.py # Shared fixtures and configuration -├── test_widget_embedding.py # Widget loading and initialization -├── test_widget_variations.py # Product variations and expansion -├── test_widget_quantity_controls.py # Quantity inputs, checkboxes, free price -└── test_widget_cart.py # Cart management and checkout flow -``` - -### Key Fixtures - -Defined in `conftest.py`: - -- **`widget_organizer`** - Creates test organizer (testorg) -- **`widget_event`** - Creates test event (testevent) -- **`widget_items`** - Creates General Admission ($50) and VIP Ticket ($150) -- **`widget_item_with_variations`** - Creates t-shirt with S/M/L/XL sizes -- **`widget_item_single_select`** - Creates item with order_max=1 (checkbox) -- **`widget_item_free_price`** - Creates pay-what-you-want donation item -- **`widget_item_sold_out`** - Creates sold out item -- **`widget_event_series`** - Creates event series with 15 subevents -- **`widget_page`** - Enhanced page object with helper methods -- **`live_server_url`** - Django live server URL - -### WidgetPage Helper Methods - -The `widget_page` fixture provides convenient methods: - -```python -def test_example(widget_page, live_server_url, widget_event, widget_items): - # Navigate to event and wait for widget to load - widget_page.goto(live_server_url, 'testorg', 'testevent') - - # Select item quantity - widget_page.select_item_quantity('General Admission', 2) - - # Expand variations - widget_page.expand_variations('Event T-Shirt') - - # Select variation quantity - widget_page.select_variation_quantity('Event T-Shirt', 'Medium', 1) - - # Click buy button - widget_page.click_buy_button() - - # Wait for iframe checkout - iframe = widget_page.wait_for_iframe_checkout() - - # Close iframe - widget_page.close_iframe() -``` - -## Common Test Patterns - -### Basic Widget Load Test - -```python -@pytest.mark.django_db -def test_widget_loads(page, live_server_url, widget_organizer, widget_event, widget_items, widget_page): - widget_page.goto(live_server_url, widget_organizer.slug, widget_event.slug) - - # Verify content - expect(page.locator(f'text="{widget_event.name}"')).to_be_visible() -``` - -### Variation Interaction Test - -```python -@pytest.mark.django_db -def test_variations(page, live_server_url, widget_organizer, widget_event, widget_item_with_variations, widget_page): - item, variations = widget_item_with_variations - - widget_page.goto(live_server_url, widget_organizer.slug, widget_event.slug) - - # Expand variations - widget_page.expand_variations(item.name) - - # Verify all variations visible - for variation in variations: - expect(page.locator(f'text="{variation.value}"')).to_be_visible() -``` - -### Cart Flow Test - -```python -@pytest.mark.django_db -def test_checkout(page, context, live_server_url, widget_organizer, widget_event, widget_items, widget_page): - widget_page.goto(live_server_url, widget_organizer.slug, widget_event.slug) - - # Add items - widget_page.select_item_quantity(widget_items[0].name, 2) - widget_page.click_buy_button() - - # Wait for checkout to open - page.wait_for_timeout(2000) - - # Verify cookies set - cookies = context.cookies() - assert len(cookies) > 0 -``` - -## Debugging - -### View Browser in Action - -Run tests with `--headed` flag to see the browser: - -```bash -pytest src/tests/e2e/test_widget_embedding.py::TestWidgetEmbedding::test_widget_loads_successfully --headed -``` - -### Slow Down for Observation - -Use `--slowmo` to slow operations (milliseconds): - -```bash -pytest src/tests/e2e/ --headed --slowmo 1000 -``` - -### Interactive Debugging - -Use `--pdb` to drop into debugger on failure: - -```bash -pytest src/tests/e2e/test_widget_cart.py --headed --pdb -``` - -### Record Videos - -Generate video recordings of test runs: - -```bash -pytest src/tests/e2e/ --video on - -# Videos saved to: test-results/ -``` - -### Generate Trace Files - -For detailed debugging with Playwright Inspector: - -```bash -pytest src/tests/e2e/ --tracing on - -# View traces with: -playwright show-trace test-results/.../trace.zip -``` - -## Troubleshooting - -### Browser binaries not found - -**Error:** `playwright._impl._errors.Error: Executable doesn't exist` - -**Solution:** -```bash -python -m playwright install chromium -``` - -### Django database errors - -**Error:** `django.db.utils.OperationalError: no such table` - -**Solution:** -```bash -# Run migrations in test settings -DJANGO_SETTINGS_MODULE=tests.settings python manage.py migrate -``` - -### Live server not starting - -**Error:** `OSError: [Errno 98] Address already in use` - -**Solution:** Kill processes using the port or let pytest assign random ports automatically (default behavior). - -### Widget not loading - -**Issue:** Widget displays loading spinner indefinitely - -**Debug steps:** -1. Check browser console for JavaScript errors: - ```python - page.on("console", lambda msg: print(f"Console: {msg.text}")) - ``` - -2. Verify widget JavaScript is built: - ```bash - ls -l src/pretix/static/pretixpresale/widget/dist/ - ``` - -3. Check Django static files are served: - ```bash - DJANGO_SETTINGS_MODULE=tests.settings python manage.py collectstatic --noinput - ``` - -### Timeout errors - -**Error:** `playwright._impl._errors.TimeoutError: Timeout 10000ms exceeded` - -**Solution:** Increase timeout for specific operations: -```python -page.wait_for_selector('.pretix-widget', timeout=30000) -``` - -Or globally in conftest.py: -```python -@pytest.fixture -def page(context): - page = context.new_page() - page.set_default_timeout(30000) # 30 seconds - yield page - page.close() -``` - -### Cookie issues - -**Issue:** Cart cookies not being set - -**Debug:** -```python -# Print all cookies -cookies = context.cookies() -for cookie in cookies: - print(f"{cookie['name']}: {cookie['value']}") -``` - -Check domain/path settings match your test server. - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: E2E Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pytest-playwright - python -m playwright install --with-deps chromium - - - name: Run E2E tests - run: | - pytest src/tests/e2e/ --browser chromium --video on - - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-results - path: test-results/ -``` - -## Configuration - -### setup.cfg - -Playwright configuration is in `/home/rash/Projects/pretix/src/setup.cfg`: - -```ini -[tool:pytest] -DJANGO_SETTINGS_MODULE = tests.settings -addopts = -rw -# Uncomment for debugging: -# addopts = -rw --headed --slowmo 500 -# --browser chromium -``` - -### Browser Context Args - -Customize in `conftest.py`: - -```python -@pytest.fixture(scope="session") -def browser_context_args(browser_context_args): - return { - **browser_context_args, - "viewport": {"width": 1280, "height": 720}, - "locale": "en-US", - "timezone_id": "America/New_York", - } -``` - -## Writing New Tests - -### Test Structure - -```python -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestFeatureName: - """Test description.""" - - def test_specific_behavior( - self, - page: Page, - live_server_url: str, - widget_organizer, - widget_event, - widget_items, - widget_page - ): - """ - Test should description. - - Given: initial state - When: action occurs - Then: expected outcome - """ - # Arrange - widget_page.goto(live_server_url, widget_organizer.slug, widget_event.slug) - - # Act - widget_page.select_item_quantity(widget_items[0].name, 1) - - # Assert - expect(page.locator('.some-element')).to_be_visible() -``` - -### Best Practices - -1. **Use descriptive test names** - `test_variation_quantity_updates_on_input` vs `test_var` -2. **Use WidgetPage helpers** - Encapsulate common interactions -3. **Wait for elements** - Use `expect().to_be_visible()` instead of `wait_for_timeout()` -4. **Mark Django tests** - Always use `@pytest.mark.django_db` when accessing database -5. **Clean test data** - Use fixtures, let pytest handle cleanup -6. **Verify user-visible behavior** - Test what users see, not implementation details - -## Related Documentation - -- [Playwright Python Docs](https://playwright.dev/python/) -- [pytest-playwright Plugin](https://github.com/microsoft/playwright-pytest) -- [Pretix Widget Documentation](https://docs.pretix.eu/guides/widget/) -- [Plan Document](/home/rash/.claude/plans/snazzy-wishing-horizon.md) - Complete test specification - -## Test Coverage - -Current test files cover Phase 1 (Critical Path): - -- ✅ Widget embedding & initialization (6 tests) -- ✅ Product variations (7 tests) -- ✅ Quantity controls & free price (11 tests) -- ✅ Cart management & checkout (7 tests) - -**Total: 31 tests implemented** - -See plan document for complete 130-test specification covering all widget features. diff --git a/src/tests/e2e/test_css_check.py b/src/tests/e2e/test_css_check.py deleted file mode 100644 index 82c3a51dbb..0000000000 --- a/src/tests/e2e/test_css_check.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Test to verify CSS is properly compiled without SCSS syntax. -""" -import pytest -from playwright.sync_api import Page - - -@pytest.mark.django_db -def test_css_contains_no_scss_syntax( - page: Page, - live_server_url: str, - organizer, - event, - items -): - """Verify CSS is compiled and doesn't contain SCSS syntax.""" - # Fetch the CSS directly - css_url = f"{live_server_url}/{organizer.slug}/{event.slug}/widget/v2.css" - - print(f"\nFetching CSS from: {css_url}") - response = page.request.get(css_url) - - print(f"Status: {response.status}") - assert response.status == 200, f"CSS returned {response.status}" - - css_content = response.text() - - # Check that CSS doesn't contain SCSS syntax - scss_indicators = [ - '@include', # SCSS mixins - '@extend', # SCSS extends - '$', # SCSS variables (though $ can appear in selectors, check more carefully) - ] - - print(f"\nCSS length: {len(css_content)} characters") - print(f"\nFirst 500 chars of CSS:\n{css_content[:500]}") - - has_scss = False - for indicator in scss_indicators: - if indicator in css_content: - # For $, be more specific - check if it's actually a variable - if indicator == '$': - # Look for variable patterns like $variable-name or $variable_name - import re - if re.search(r'\$[a-zA-Z_]', css_content): - has_scss = True - print(f"\n⚠️ Found SCSS syntax: {indicator}") - # Find and print examples - matches = re.findall(r'\$[a-zA-Z_][a-zA-Z0-9_-]*', css_content) - print(f"Examples: {matches[:5]}") - else: - has_scss = True - print(f"\n⚠️ Found SCSS syntax: {indicator}") - # Find line with the indicator - for i, line in enumerate(css_content.split('\n')[:100]): - if indicator in line: - print(f"Line {i+1}: {line}") - break - - if not has_scss: - print("\n✅ CSS is properly compiled - no SCSS syntax found!") - - assert not has_scss, "CSS contains SCSS syntax - not properly compiled!" diff --git a/src/tests/e2e/test_views.py b/src/tests/e2e/test_views.py deleted file mode 100644 index b128af7c30..0000000000 --- a/src/tests/e2e/test_views.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Test views for E2E widget testing. - -These views serve HTML pages with embedded widgets for testing purposes. -""" -from django.http import HttpResponse -from django.template import Template, Context -from django.views import View - - -class WidgetTestPageView(View): - """ - Serves a simple HTML page with the pretix widget embedded. - - Used for E2E testing to simulate how the widget would be embedded - in a customer's website. - """ - - def get(self, request, organizer, event): - """Render test page with widget embedded.""" - # Build event URL - event_url = request.build_absolute_uri( - f"/{organizer}/{event}/" - ) - - # Build widget script URL (old version) - widget_script_url = request.build_absolute_uri( - f"/widget/v2.en.js" - ) - - html_content = f""" - - - - - Widget Test Page - - -

Pretix Widget E2E Test Page

- - - - - - - - -""" - - return HttpResponse(html_content, content_type="text/html") diff --git a/src/tests/e2e/test_widget_accessibility.py b/src/tests/e2e/test_widget_accessibility.py deleted file mode 100644 index e44adae165..0000000000 --- a/src/tests/e2e/test_widget_accessibility.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -E2E Tests for Accessibility - -Tests that verify: -- ARIA labels on main widget wrapper -- Heading roles and levels -- Voucher input labeling -- Buy button aria-describedby -- Keyboard navigation -- Quantity control labels -- Calendar table accessibility -- Variations toggle aria-expanded/aria-controls -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestWidgetAriaLabels: - """Test ARIA attributes on the widget structure.""" - - def test_widget_wrapper_has_role_article( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Main widget wrapper should have role="article" - and aria-label with event name. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - wrapper = page.locator('.pretix-widget-wrapper') - expect(wrapper).to_have_attribute('role', 'article') - expect(wrapper).to_have_attribute('aria-label', event.name) - - def test_widget_wrapper_is_focusable( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget wrapper should have tabindex="0" for keyboard access. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - wrapper = page.locator('.pretix-widget-wrapper') - expect(wrapper).to_have_attribute('tabindex', '0') - - def test_event_name_has_heading_role( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Event name heading in event form should have role="heading" - with aria-level="2". - - Note: Event header is only shown when display_event_info - is explicitly enabled for single events (auto mode hides it). - We use the display-event-info attribute to force it on. - """ - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'display-event-info': 'true'} - ) - - heading = page.locator( - '.pretix-widget-event-header strong[role="heading"]') - expect(heading).to_be_visible() - expect(heading).to_have_attribute('aria-level', '2') - - -@pytest.mark.django_db -class TestQuantityControlAccessibility: - """Test accessibility of quantity controls.""" - - def test_increment_button_has_aria_label( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Plus/minus buttons should have descriptive aria-labels. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Find increment button for first item - item_elem = page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")') - inc_btn = item_elem.locator('button[aria-label]').last - dec_btn = item_elem.locator('button[aria-label]').first - - # Should have aria-labels - inc_label = inc_btn.get_attribute('aria-label') - dec_label = dec_btn.get_attribute('aria-label') - assert inc_label is not None and len(inc_label) > 0 - assert dec_label is not None and len(dec_label) > 0 - - def test_quantity_input_has_aria_labelledby( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Quantity input should be connected to a label via aria-labelledby. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")') - qty_input = item_elem.locator('input[type="number"]') - - labelledby = qty_input.get_attribute('aria-labelledby') - assert labelledby is not None and len(labelledby) > 0 - - -@pytest.mark.django_db -class TestVoucherAccessibility: - """Test accessibility of voucher input.""" - - def test_voucher_input_has_aria_labelledby( - self, - page: Page, - live_server_url: str, - organizer, - event, - voucher, - widget_page - ): - """ - Voucher input should reference the headline via aria-labelledby. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - voucher_input = page.locator('.pretix-widget-voucher-input') - headline = page.locator('.pretix-widget-voucher-headline') - - # Headline should have an ID - headline_id = headline.get_attribute('id') - assert headline_id is not None - - # Input should reference it - labelledby = voucher_input.get_attribute('aria-labelledby') - assert labelledby == headline_id - - -@pytest.mark.django_db -class TestVariationAccessibility: - """Test accessibility of variation toggles.""" - - def test_variations_toggle_has_aria_expanded( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Variations toggle button should have aria-expanded - and aria-controls attributes. - """ - item, _ = item_with_variations - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - toggle_btn = item_elem.locator( - 'button[aria-expanded]') - - # Should start collapsed - expect(toggle_btn).to_have_attribute('aria-expanded', 'false') - - # Should reference the variations container - controls = toggle_btn.get_attribute('aria-controls') - assert controls is not None - - # Click to expand - toggle_btn.click() - expect(toggle_btn).to_have_attribute('aria-expanded', 'true') - - -@pytest.mark.django_db -class TestCalendarAccessibility: - """Test accessibility of calendar view.""" - - def test_calendar_table_is_focusable( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - Calendar table should have tabindex="0" and aria-labelledby. - """ - event, _ = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'calendar'} - ) - - table = page.locator('.pretix-widget-event-calendar-table') - expect(table).to_have_attribute('tabindex', '0') - - # Should be labeled by the month heading - labelledby = table.get_attribute('aria-labelledby') - assert labelledby is not None - - def test_calendar_day_headers_have_aria_labels( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - Calendar day-of-week headers should have full day names - as aria-labels (Mo -> Monday, Tu -> Tuesday, etc.). - """ - event, _ = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'calendar'} - ) - - # Check first day header has aria-label - first_header = page.locator( - '.pretix-widget-event-calendar-table thead th').first - label = first_header.get_attribute('aria-label') - assert label is not None - # Should be a full day name like "Monday" - assert len(label) > 2 - - -@pytest.mark.django_db -class TestKeyboardNavigation: - """Test keyboard navigation through the widget.""" - - def test_tab_reaches_interactive_elements( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Pressing Tab should cycle through interactive elements - (inputs, buttons) within the widget. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Tab through several elements - focused_tags = set() - for _ in range(10): - page.keyboard.press('Tab') - tag = page.evaluate('() => document.activeElement.tagName') - focused_tags.add(tag) - - # Should have reached at least inputs and buttons - assert 'INPUT' in focused_tags or 'BUTTON' in focused_tags diff --git a/src/tests/e2e/test_widget_availability.py b/src/tests/e2e/test_widget_availability.py deleted file mode 100644 index 72eaab0c5c..0000000000 --- a/src/tests/e2e/test_widget_availability.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -E2E Tests for Availability States - -Tests that verify: -- Sold out items show "Sold out" message -- Low stock items show "currently available: N" -- Require-voucher items show voucher message -- Not-yet-available items show "Not yet available" -- Available items show quantity selector -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestSoldOutState: - """Test sold out availability display.""" - - def test_sold_out_shows_message( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out, - widget_page - ): - """Sold out item should show 'Sold out' text.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_sold_out.name}")') - expect(item_elem).to_be_visible() - - # Should show "Sold out" in the availability area - avail = item_elem.locator('.pretix-widget-availability-gone') - expect(avail).to_be_visible() - - def test_sold_out_has_no_quantity_selector( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out, - widget_page - ): - """Sold out item should not show any input controls.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_sold_out.name}")') - expect(item_elem).to_be_visible() - - # Should not have any input controls - assert item_elem.locator('input').count() == 0 - - -@pytest.mark.django_db -class TestQuotaLeftDisplay: - """Test quota-left indicator for low stock items.""" - - def test_low_stock_shows_currently_available( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_low_stock, - widget_page - ): - """ - Item with low stock and show_quota_left should display - the number of remaining tickets. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_low_stock.name}")') - expect(item_elem).to_be_visible() - - # Should show quota left text containing "3" - # The widget uses "currently available: 3" format - expect(item_elem).to_contain_text('3') - - def test_low_stock_still_has_quantity_selector( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_low_stock, - widget_page - ): - """Low stock items should still be purchasable.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_low_stock.name}")') - expect(item_elem).to_be_visible() - - # Should have quantity input - expect(item_elem.locator('input')).to_be_visible() - - -@pytest.mark.django_db -class TestRequireVoucherState: - """Test require-voucher unavailability message.""" - - def test_require_voucher_shows_message( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_require_voucher, - widget_page - ): - """Item requiring voucher should show 'Only available with a voucher'.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_require_voucher.name}")') - expect(item_elem).to_be_visible() - - # Should show unavailability message with voucher link - unavail = item_elem.locator('.pretix-widget-availability-unavailable') - expect(unavail).to_be_visible() - expect(unavail).to_contain_text('voucher') - - def test_require_voucher_has_link_to_voucher_input( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_require_voucher, - voucher, - widget_page - ): - """Voucher-required message should link to the voucher input field.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_require_voucher.name}")') - unavail = item_elem.locator('.pretix-widget-availability-unavailable') - - # Should have a link (to jump to voucher input) - link = unavail.locator('a') - expect(link).to_be_visible() - - -@pytest.mark.django_db -class TestNotYetAvailable: - """Test not-yet-available state.""" - - def test_future_item_shows_not_yet_available( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_not_yet_available, - widget_page - ): - """Item with future available_from should show 'Not yet available'.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_not_yet_available.name}")') - expect(item_elem).to_be_visible() - - # Should show unavailability message - unavail = item_elem.locator('.pretix-widget-availability-unavailable') - expect(unavail).to_be_visible() - - def test_future_item_has_no_quantity_selector( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_not_yet_available, - widget_page - ): - """Not-yet-available items should not have quantity controls.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_not_yet_available.name}")') - expect(item_elem).to_be_visible() - - # Should not have any input controls - assert item_elem.locator('input').count() == 0 diff --git a/src/tests/e2e/test_widget_cart.py b/src/tests/e2e/test_widget_cart.py deleted file mode 100644 index 26d91fa780..0000000000 --- a/src/tests/e2e/test_widget_cart.py +++ /dev/null @@ -1,292 +0,0 @@ -""" -E2E Tests for Cart Management & Checkout Flow - -Tests that verify: -- Adding items to cart opens iframe checkout -- Empty cart does not open checkout -- Cart persistence via cookies -- Resume checkout after page reload -- Multiple item selection -- Mixed input types (checkbox + quantity) -""" -import pytest -from playwright.sync_api import Page, expect, BrowserContext - - -@pytest.mark.django_db -class TestCartBasics: - """Test basic cart functionality.""" - - def test_add_to_cart_opens_iframe_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Selecting items and clicking Buy should open iframe checkout.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 2) - widget_page.click_buy_button() - - # Iframe checkout should open - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'iframe=1' in src - assert 'take_cart_id' in src - - # page.pause() - - def test_empty_cart_does_not_open_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Clicking Buy without selecting items should not open checkout.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Don't select any items, just click buy - widget_page.click_buy_button() - page.wait_for_timeout(1000) - - # No iframe should have opened - expect(page.locator('.pretix-widget-frame-shown')).not_to_be_visible() - - # Widget should still be visible (didn't navigate away) - expect(page.locator('.pretix-widget')).to_be_visible() - - # Items should still be there - expect(page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")' - )).to_be_visible() - - -@pytest.mark.django_db -class TestCartPersistence: - """Test cart persistence across page reloads.""" - - def test_cart_cookie_set_after_checkout( - self, - page: Page, - context: BrowserContext, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Adding items to cart should create a pretix_widget cookie.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 1) - widget_page.click_buy_button() - - # Wait for iframe checkout to open (cookie is set during cart creation) - widget_page.wait_for_iframe_checkout() - page.wait_for_timeout(2000) - - cookies = context.cookies() - widget_cookies = [c for c in cookies if c['name'].startswith('pretix_widget_')] - assert len(widget_cookies) > 0, ( - f"Expected pretix_widget cookie after checkout, got: {[c['name'] for c in cookies]}" - ) - - def test_resume_checkout_after_reload( - self, - page: Page, - context: BrowserContext, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """After creating a cart and reloading, widget should show resume option.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Create a real cart by adding items and opening checkout - widget_page.select_item_quantity(items[0].name, 1) - widget_page.click_buy_button() - widget_page.wait_for_iframe_checkout() - - # Wait for cart cookie to be set (set in buy_callback when cart is created) - page.wait_for_function( - "() => document.cookie.includes('pretix_widget_')", - timeout=10000 - ) - - # Close iframe and reload - widget_page.close_iframe() - page.reload() - widget_page.wait_for_widget_load() - - # Should show "Resume checkout" button (class: pretix-widget-resume-button) - resume_btn = page.locator('.pretix-widget-resume-button') - expect(resume_btn).to_be_visible(timeout=5000) - - -@pytest.mark.django_db -class TestIframeCheckout: - """Test iframe checkout flow (enabled via skip-ssl-check + SITE_URL fix).""" - - def test_iframe_checkout_opens( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """With skip-ssl-check, checkout should open in iframe on HTTP.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 1) - widget_page.click_buy_button() - - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'iframe=1' in src - assert organizer.slug in src - - def test_close_iframe_button( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """User should be able to close checkout iframe.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 1) - widget_page.click_buy_button() - - widget_page.wait_for_iframe_checkout() - page.wait_for_timeout(3000) - - widget_page.close_iframe() - - expect(page.locator('.pretix-widget-frame-shown')).not_to_be_visible() - - def test_iframe_checkout_has_take_cart_id( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Iframe checkout URL should include take_cart_id parameter.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 2) - widget_page.click_buy_button() - - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'take_cart_id' in src - - def test_overlay_visible_during_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Iframe checkout overlay container should be visible during checkout.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 1) - widget_page.click_buy_button() - - widget_page.wait_for_iframe_checkout() - - overlay = page.locator('.pretix-widget-frame-holder') - expect(overlay).to_be_visible() - - -@pytest.mark.django_db -class TestMultipleItemSelection: - """Test selecting and submitting multiple items.""" - - def test_select_multiple_different_items( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Selecting multiple items should open checkout with all of them.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Select multiple items - widget_page.select_item_quantity(items[0].name, 2) - widget_page.select_item_quantity(items[1].name, 1) - - widget_page.click_buy_button() - - # Iframe should open with both items in the cart - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'take_cart_id' in src - - def test_mixed_checkbox_and_quantity_items( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - item_single_select, - widget_page - ): - """Selecting both checkbox and quantity items should open checkout.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Select quantity item - widget_page.select_item_quantity(items[0].name, 3) - - # Select checkbox item - widget_page.select_item_quantity(item_single_select.name, 1) - - widget_page.click_buy_button() - - # Iframe should open - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'take_cart_id' in src diff --git a/src/tests/e2e/test_widget_categories.py b/src/tests/e2e/test_widget_categories.py deleted file mode 100644 index bd5fa47879..0000000000 --- a/src/tests/e2e/test_widget_categories.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -E2E Tests for Categories & Organization - -Tests that verify: -- Category headers display correctly -- Category descriptions render -- Items are grouped under their respective categories -- Category sort order is maintained -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestCategoryDisplay: - """Test category header and description rendering.""" - - def test_category_headers_display( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Category names should be shown as h3 headers. - - The items fixture creates a 'Tickets' category. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Category header should be visible - category_header = page.locator( - '.pretix-widget-category-name:text-is("Tickets")') - expect(category_header).to_be_visible() - - def test_category_description_renders( - self, - page: Page, - live_server_url: str, - organizer, - event, - items_with_category_description, - widget_page - ): - """ - Category descriptions should be displayed below category name. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Category description should be visible - desc = page.locator('.pretix-widget-category-description') - expect(desc.first).to_be_visible() - expect(desc.first).to_contain_text('Early bird tickets available') - - def test_items_grouped_by_category( - self, - page: Page, - live_server_url: str, - organizer, - event, - items_multiple_categories, - widget_page - ): - """ - Items should be grouped under respective categories - and maintain category sort order. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Both categories should be visible - expect(page.locator( - '.pretix-widget-category-name:text-is("Music")' - )).to_be_visible() - expect(page.locator( - '.pretix-widget-category-name:text-is("Food & Drink")' - )).to_be_visible() - - # Concert Ticket should be under "Music" category - music_cat = page.locator( - '.pretix-widget-category:has(.pretix-widget-category-name' - ':text-is("Music"))') - expect(music_cat.locator( - '.pretix-widget-item:has-text("Concert Ticket")' - )).to_be_visible() - - # Food Pass should be under "Food & Drink" category - food_cat = page.locator( - '.pretix-widget-category:has(.pretix-widget-category-name' - ':text-is("Food & Drink"))') - expect(food_cat.locator( - '.pretix-widget-item:has-text("Food Pass")' - )).to_be_visible() - - # Verify ordering: Music (position=0) should come before - # Food & Drink (position=1) - categories = page.locator('.pretix-widget-category-name') - first_cat = categories.nth(0) - expect(first_cat).to_have_text('Music') diff --git a/src/tests/e2e/test_widget_config.py b/src/tests/e2e/test_widget_config.py deleted file mode 100644 index 5b1732d26c..0000000000 --- a/src/tests/e2e/test_widget_config.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -E2E Tests for Widget Configuration Attributes - -Tests that verify: -- items attribute filters to specific products -- categories attribute filters by category -- disable-vouchers hides voucher input -- disable-iframe forces new tab checkout -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestItemsFilter: - """Test items attribute for filtering products.""" - - def test_items_attribute_shows_only_specified_items( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - When items="" is set, only that item should be shown. - """ - # Get the first item's ID - target_item = items[0] - other_item = items[1] - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - items=str(target_item.pk) - ) - - # Target item should be visible - expect(page.locator( - f'.pretix-widget-item:has-text("{target_item.name}")' - )).to_be_visible() - - # Other item should NOT be visible - expect(page.locator( - f'.pretix-widget-item:has-text("{other_item.name}")' - )).to_have_count(0) - - def test_items_attribute_with_multiple_ids( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - When items=",", both items should be shown. - """ - ids = ','.join(str(item.pk) for item in items) - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - items=ids - ) - - # Both items should be visible - for item in items: - expect(page.locator( - f'.pretix-widget-item:has-text("{item.name}")' - )).to_be_visible() - - -@pytest.mark.django_db -class TestCategoriesFilter: - """Test categories attribute for filtering by category.""" - - def test_categories_attribute_shows_only_specified_category( - self, - page: Page, - live_server_url: str, - organizer, - event, - items_multiple_categories, - widget_page - ): - """ - When categories="" is set, only items from that category - should be shown. - """ - items = items_multiple_categories - # Get the category of the first item (Music) - target_category = items[0].category - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - categories=str(target_category.pk) - ) - - # Music item should be visible - expect(page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")' - )).to_be_visible() - - # Food item should NOT be visible - expect(page.locator( - f'.pretix-widget-item:has-text("{items[1].name}")' - )).to_have_count(0) - - -@pytest.mark.django_db -class TestDisableVouchers: - """Test disable-vouchers attribute.""" - - def test_disable_vouchers_hides_voucher_input( - self, - page: Page, - live_server_url: str, - organizer, - event, - voucher, - widget_page - ): - """ - When disable-vouchers is set, voucher input should be hidden - even when vouchers exist. - """ - # Navigate with disable-vouchers attribute - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'disable-vouchers': ''} - ) - - # Voucher section should NOT be visible - voucher_section = page.locator('.pretix-widget-voucher') - expect(voucher_section).to_have_count(0) - - -@pytest.mark.django_db -class TestDisableIframe: - """Test disable-iframe attribute.""" - - def test_disable_iframe_opens_new_tab( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - When disable-iframe is set, checkout should open in a new tab - instead of an iframe overlay. - """ - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'disable-iframe': ''} - ) - - # Select quantity for an item - widget_page.select_item_quantity('General Admission', 1) - - # Clicking buy should open a new tab (popup), not an iframe - with page.expect_popup() as popup_info: - widget_page.click_buy_button() - - popup = popup_info.value - # New tab should navigate to checkout URL - assert organizer.slug in popup.url - popup.close() diff --git a/src/tests/e2e/test_widget_display_modes.py b/src/tests/e2e/test_widget_display_modes.py deleted file mode 100644 index 09e4f1f095..0000000000 --- a/src/tests/e2e/test_widget_display_modes.py +++ /dev/null @@ -1,310 +0,0 @@ -""" -E2E Tests for Widget Display Modes - -Tests that verify: -- Default widget mode shows full ticket shop -- Calendar view for event series -- List view for event series -- Button mode opens checkout directly -- Calendar navigation (next/prev month) -- Clicking a date in calendar navigates to event -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestWidgetMode: - """Test default widget mode (full ticket shop).""" - - def test_widget_mode_shows_items_and_buy_button( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Default widget mode should show full product listing - with categories, items, and a buy button. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should show items - for item in items: - expect(page.locator( - f'.pretix-widget-item:has-text("{item.name}")' - )).to_be_visible() - - # Should show buy/add-to-cart button - expect(page.locator( - 'button:has-text("Add to cart"), button:has-text("Buy")' - ).first).to_be_visible() - - -@pytest.mark.django_db -class TestCalendarView: - """Test calendar display mode for event series.""" - - def test_calendar_view_displays_month_grid( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - Calendar view should show a monthly grid with event dates. - """ - event, subevents = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'calendar'} - ) - - # Should show calendar table - expect(page.locator( - '.pretix-widget-event-calendar-table' - )).to_be_visible() - - # Should have day cells with events - event_cells = page.locator('.pretix-widget-has-events') - expect(event_cells.first).to_be_visible() - - def test_calendar_view_navigation_next_month( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - Clicking next month button should navigate to the next month. - """ - event, subevents = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'calendar'} - ) - - # Get current month heading text - header = page.locator('.pretix-widget-event-calendar-head') - initial_text = header.inner_text() - - # Click next month button - next_btn = page.locator( - '.pretix-widget-event-calendar-head a').last - next_btn.click() - - # Wait for calendar to update - page.wait_for_timeout(1000) - - # Month heading should change - updated_text = header.inner_text() - assert updated_text != initial_text - - def test_calendar_event_links_are_clickable( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - Calendar event entries should be clickable links that show - event name, time, and availability status. - - Note: Full subevent navigation requires domain configuration - (target_url resolves to configured domain, not live_server). - We verify the links exist and have correct structure. - """ - event, _ = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'calendar'} - ) - - # Event links should exist and show event info - event_link = page.locator( - '.pretix-widget-event-calendar-event').first - expect(event_link).to_be_visible() - - # Should show event name - expect(event_link.locator( - '.pretix-widget-event-calendar-event-name' - )).to_be_visible() - - # Should show time range - expect(event_link.locator( - '.pretix-widget-event-calendar-event-date' - )).to_be_visible() - - # Should show availability ("Buy now" for available events) - expect(event_link.locator( - '.pretix-widget-event-calendar-event-availability' - )).to_be_visible() - - -@pytest.mark.django_db -class TestListView: - """Test list display mode for event series.""" - - def test_list_view_displays_events( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - List view should display events as a linear list. - """ - event, subevents = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'list'} - ) - - # Should show event list entries - entries = page.locator('.pretix-widget-event-list-entry') - expect(entries.first).to_be_visible() - - # Should have multiple entries - assert entries.count() > 0 - - def test_list_view_shows_event_names( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - List view should show subevent names. - """ - event, subevents = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'list'} - ) - - # At least the first subevent name should appear - expect(page.locator( - f':text-is("{subevents[0].name}")' - )).to_be_visible() - - def test_list_view_entries_show_availability( - self, - page: Page, - live_server_url: str, - organizer, - event_series, - widget_page - ): - """ - List view entries should show availability status. - - Note: Full subevent navigation requires domain configuration - (target_url resolves to configured domain, not live_server). - We verify the entries have correct structure. - """ - event, _ = event_series - - widget_page.goto( - live_server_url, - organizer.slug, - event.slug, - **{'list-type': 'list'} - ) - - # Each entry should show availability info - first_entry = page.locator( - '.pretix-widget-event-list-entry').first - expect(first_entry).to_be_visible() - - # Should show availability indicator (green = available) - availability = first_entry.locator( - '.pretix-widget-event-list-entry-availability') - expect(availability).to_be_visible() - - -@pytest.mark.django_db -class TestButtonMode: - """Test button display mode.""" - - def test_button_mode_shows_button( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Button mode should show a simple button. - """ - widget_page.goto_button_test_page( - live_server_url, - organizer.slug, - event.slug - ) - - # Wait for script to load and initialize - page.wait_for_timeout(2000) - - # Should show the button - button = page.locator('.pretix-button') - expect(button).to_be_visible() - expect(button).to_have_text('Buy tickets!') - - def test_button_click_opens_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Clicking the button should open checkout (new tab on HTTP). - """ - widget_page.goto_button_test_page( - live_server_url, - organizer.slug, - event.slug - ) - - page.wait_for_timeout(2000) - - # Click button - on HTTP it opens in new tab - with page.expect_popup() as popup_info: - page.locator('.pretix-button').click() - - popup = popup_info.value - assert organizer.slug in popup.url - popup.close() diff --git a/src/tests/e2e/test_widget_edge_cases.py b/src/tests/e2e/test_widget_edge_cases.py deleted file mode 100644 index b99dc9bbf9..0000000000 --- a/src/tests/e2e/test_widget_edge_cases.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -E2E Tests for Edge Cases - -Tests that verify: -- Empty event (no items) displays gracefully -- Item with min_per_order enforces minimum -- Zero quantity submission shows warning -- Widget handles special characters in names -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestEmptyStates: - """Test widget behavior with empty or minimal data.""" - - def test_event_with_no_items_shows_empty( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Event with no items should still load without errors. - Should show the widget container but no item rows. - """ - # Navigate without creating any items - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Widget should still be present - expect(page.locator('.pretix-widget')).to_be_visible() - - # No items should be shown - items = page.locator('.pretix-widget-item') - assert items.count() == 0 - - def test_zero_quantity_stays_on_widget( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Submitting with zero quantity should not navigate away. - The widget should remain visible without opening checkout. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Don't select any items, just click buy - widget_page.click_buy_button() - page.wait_for_timeout(1000) - - # Widget should still be on the same page (no checkout opened) - expect(page.locator('.pretix-widget')).to_be_visible() - - # Items should still be visible - expect(page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")' - )).to_be_visible() - - -@pytest.mark.django_db -class TestMinPerOrder: - """Test minimum order quantity enforcement.""" - - def test_item_with_min_per_order_shows_message( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_min_order, - widget_page - ): - """ - Items with min_per_order should display a text message - indicating the minimum quantity (e.g. "minimum amount to order: 2"). - """ - item = item_min_order - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should show minimum order message containing "2" - meta = item_elem.locator('.pretix-widget-item-meta') - expect(meta).to_be_visible() - expect(meta).to_contain_text('2') - - -@pytest.mark.django_db -class TestSpecialCharacters: - """Test widget handles special characters correctly.""" - - def test_item_name_with_special_characters( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_special_chars, - widget_page - ): - """ - Items with special characters (umlauts, ampersands, etc.) - should display correctly. - """ - item = item_special_chars - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Item with special characters should be visible - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() diff --git a/src/tests/e2e/test_widget_embedding.py b/src/tests/e2e/test_widget_embedding.py deleted file mode 100644 index 5e61d9317f..0000000000 --- a/src/tests/e2e/test_widget_embedding.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -E2E Tests for Widget Embedding & Initialization - -Tests that verify the pretix widget loads correctly, initializes properly, -and displays basic event information. -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestWidgetEmbedding: - """Test basic widget embedding and initialization.""" - - def test_widget_loads_successfully( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should load and display event information. - - Verifies that the widget loads on the page and shows: - - Event name - - All configured items - """ - # Navigate to test page with widget embedded - widget_page.goto( - live_server_url, organizer.slug, event.slug - ) - - # Verify widget container exists with event aria-label - widget = page.locator('.pretix-widget-wrapper') - expect(widget).to_have_attribute('aria-label', event.name) - - # Verify items are listed - for item in items: - expect(page.locator(f'text="{item.name}"')).to_be_visible() - - def test_widget_displays_loading_state( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Widget should show loading spinner during initial load. - - The loading spinner should eventually disappear when data is loaded. - """ - # Navigate to widget test page - widget_page.goto( - live_server_url, organizer.slug, event.slug, wait=False - ) - - # Wait for widget element - page.wait_for_selector('.pretix-widget', timeout=10000) - - # Loading spinner should eventually be hidden - loading = page.locator('.pretix-widget-loading') - expect(loading).to_be_hidden(timeout=10000) - - def test_widget_handles_invalid_event( - self, - page: Page, - live_server_url: str, - widget_page - ): - """Widget should display error message for invalid event.""" - widget_page.goto( - live_server_url, 'invalid-org', 'invalid-event', wait=False - ) - - # Should show widget container - page.wait_for_selector('.pretix-widget', timeout=10000) - - # Should show error message - error_msg = page.locator('.pretix-widget-error-message') - expect(error_msg).to_be_visible(timeout=10000) - expect(error_msg).to_contain_text('could not be loaded') - - def test_widget_shows_item_descriptions( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should display item descriptions. - - Item descriptions should be visible for items that have them. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug - ) - - # Check that descriptions are shown - for item in items: - if item.description: - expect( - page.locator(f'text="{item.description}"') - ).to_be_visible() - - def test_widget_shows_item_prices( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should display item prices correctly. - - Prices should be formatted with currency and decimal places. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug - ) - - # Verify prices are shown (with currency) - # Each item should have its price displayed - for item in items: - # Find the item container first, then check price within it - item_container = page.locator( - f'.pretix-widget-item:has-text("{item.name}")' - ) - expect(item_container).to_be_visible() - - # Check price is present (formatted as "EUR XX.XX") - price_text = f"{float(item.default_price):.2f}" - price_box = item_container.locator('.pretix-widget-pricebox') - expect(price_box).to_contain_text(price_text) - - -@pytest.mark.django_db -class TestWidgetEventInfo: - """Test event information display in widget.""" - - def test_widget_displays_event_date( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Widget should show event date and time. - - Note: The old widget.js implementation may not display event - date by default. This test verifies the widget loads without - checking for specific date display. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug - ) - - # Widget should be present and functional - # (Event date display varies by configuration) - widget = page.locator('.pretix-widget') - expect(widget).to_be_visible() - - def test_widget_hides_event_info_when_configured( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Widget should hide event info when display-event-info="false". - - This test would require creating a widget embed page with the attribute. - Currently just a placeholder for future implementation. - """ - # TODO: This requires creating a custom HTML page with widget embed - # For now, skip this test - pytest.skip("Requires custom widget embed page with attributes") diff --git a/src/tests/e2e/test_widget_errors.py b/src/tests/e2e/test_widget_errors.py deleted file mode 100644 index 7227c57824..0000000000 --- a/src/tests/e2e/test_widget_errors.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -E2E Tests for Error Handling - -Tests that verify: -- Error message on invalid event -- Error message shows "Open ticket shop" link -- Sold out items show unavailable state -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestErrorDisplay: - """Test error messages and states.""" - - def test_invalid_event_shows_error_message( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Loading a non-existent event should show an error message. - """ - widget_page.goto( - live_server_url, organizer.slug, 'nonexistent-event') - - # Should show error message - expect(page.locator( - '.pretix-widget-error-message' - )).to_be_visible() - - def test_error_shows_open_in_new_tab_link( - self, - page: Page, - live_server_url: str, - organizer, - event, - widget_page - ): - """ - Error state should include a link to open the ticket shop - in a new tab as a fallback. - """ - widget_page.goto( - live_server_url, organizer.slug, 'nonexistent-event') - - # Should show fallback action link - action_link = page.locator('.pretix-widget-error-action a') - expect(action_link).to_be_visible() - - # Link should open in new tab - expect(action_link).to_have_attribute('target', '_blank') - - -@pytest.mark.django_db -class TestSoldOutState: - """Test sold out item display.""" - - def test_sold_out_item_shows_unavailable( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out, - widget_page - ): - """ - Items with zero quota should show as unavailable/sold out. - """ - item = item_sold_out - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should not have a quantity input or checkbox (sold out) - input_count = item_elem.locator( - 'input[type="number"], input[type="checkbox"]').count() - assert input_count == 0 - - def test_sold_out_item_shows_status_text( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out, - widget_page - ): - """ - Sold out items should show a status message like - "Sold out" or "Currently unavailable". - """ - item = item_sold_out - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - - # Should show some unavailability text - avail_col = item_elem.locator( - '.pretix-widget-item-availability-col') - expect(avail_col).to_be_visible() - # The text could be "Sold out", "Currently unavailable", etc. - avail_text = avail_col.inner_text() - assert len(avail_text.strip()) > 0 diff --git a/src/tests/e2e/test_widget_lightbox.py b/src/tests/e2e/test_widget_lightbox.py deleted file mode 100644 index cc3424b773..0000000000 --- a/src/tests/e2e/test_widget_lightbox.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -E2E Tests for Item Images & Lightbox - -Tests that verify: -- Item with picture shows thumbnail -- Clicking thumbnail opens lightbox overlay -- Lightbox close button works -- Lightbox has correct ARIA structure -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestItemPicture: - """Test item picture thumbnail display.""" - - def test_item_with_picture_shows_thumbnail( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Item with picture should display a thumbnail image.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - expect(item_elem).to_be_visible() - - # Should have picture element - picture = item_elem.locator('.pretix-widget-item-picture') - expect(picture).to_be_visible() - - def test_item_with_picture_has_alt_text( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Item picture should have alt text.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - - img = item_elem.locator('.pretix-widget-item-picture') - alt = img.get_attribute('alt') - assert alt is not None and len(alt) > 0 - - def test_item_with_picture_has_link( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Picture should be wrapped in a clickable link.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - - link = item_elem.locator('.pretix-widget-item-picture-link') - expect(link).to_be_visible() - href = link.get_attribute('href') - assert href is not None and len(href) > 0 - - def test_item_has_picture_class( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Item row should have pretix-widget-item-with-picture class.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item-with-picture:has-text("{item_with_picture.name}")') - expect(item_elem).to_be_visible() - - -@pytest.mark.django_db -class TestLightbox: - """Test lightbox overlay for item pictures.""" - - def test_click_picture_opens_lightbox( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Clicking item picture should open lightbox overlay.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - - # Click the picture link - link = item_elem.locator('.pretix-widget-item-picture-link') - link.click() - - # Lightbox should appear - lightbox = page.locator('.pretix-widget-lightbox-shown') - expect(lightbox).to_be_visible() - - def test_lightbox_shows_fullsize_image( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Lightbox should display fullsize image.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - item_elem.locator('.pretix-widget-item-picture-link').click() - - # Wait for lightbox - page.wait_for_timeout(1000) - - # Should have an image inside the lightbox - lightbox_img = page.locator('.pretix-widget-lightbox-image img') - expect(lightbox_img).to_be_visible() - - def test_lightbox_close_button( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Lightbox close button should close the overlay.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - item_elem.locator('.pretix-widget-item-picture-link').click() - - # Wait for lightbox to appear - lightbox = page.locator('.pretix-widget-lightbox-shown') - expect(lightbox).to_be_visible() - - # Click close button - close_btn = page.locator('.pretix-widget-lightbox-close button') - close_btn.click() - - # Lightbox should close - page.wait_for_timeout(500) - expect(page.locator('.pretix-widget-lightbox-shown')).not_to_be_visible() - - def test_lightbox_has_alertdialog_role( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_picture, - widget_page - ): - """Lightbox dialog should have role='alertdialog'.""" - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item_with_picture.name}")') - item_elem.locator('.pretix-widget-item-picture-link').click() - - page.wait_for_timeout(1000) - - dialog = page.locator('.pretix-widget-lightbox-holder') - role = dialog.get_attribute('role') - assert role == 'alertdialog' diff --git a/src/tests/e2e/test_widget_loading.py b/src/tests/e2e/test_widget_loading.py deleted file mode 100644 index 2772544eb9..0000000000 --- a/src/tests/e2e/test_widget_loading.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -E2E Tests for Loading States & Performance - -Tests that verify: -- Loading spinner appears during widget initialization -- Loading spinner disappears after content loads -- Widget loads within acceptable time -- No JavaScript errors during widget initialization -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestLoadingStates: - """Test loading states and transitions.""" - - def test_loading_spinner_disappears_after_load( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Loading spinner should be hidden once widget content loads. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Loading spinner should be hidden (display:none) - loading = page.locator('.pretix-widget-loading') - expect(loading).to_be_hidden() - - def test_widget_loads_within_timeout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should fully load within 15 seconds. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug, wait=False) - - # Widget should appear within 15s - page.wait_for_selector('.pretix-widget', timeout=15000) - - # Items should be visible within 15s total - expect(page.locator( - f'.pretix-widget-item:has-text("{items[0].name}")' - )).to_be_visible(timeout=15000) - - def test_no_javascript_errors_on_load( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should load without any JavaScript console errors. - """ - errors = [] - page.on('pageerror', lambda err: errors.append(str(err))) - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # No JS errors should have occurred - assert len(errors) == 0, f"JavaScript errors: {errors}" - - -@pytest.mark.django_db -class TestWidgetReload: - """Test widget behavior on page interactions.""" - - def test_widget_css_loads_correctly( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget CSS should load and apply styles. - No SCSS syntax should leak into rendered styles. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Check that widget has actual styled dimensions - # (not zero-height which would indicate CSS failure) - widget = page.locator('.pretix-widget') - box = widget.bounding_box() - assert box is not None - assert box['height'] > 50 # Widget should have meaningful height - assert box['width'] > 100 # Widget should have meaningful width diff --git a/src/tests/e2e/test_widget_pricing.py b/src/tests/e2e/test_widget_pricing.py deleted file mode 100644 index 5bfeae0893..0000000000 --- a/src/tests/e2e/test_widget_pricing.py +++ /dev/null @@ -1,249 +0,0 @@ -""" -E2E Tests for Pricing & Tax Display - -Tests that verify: -- Net vs gross price display -- Tax information lines -- Mixed tax rates -- Original price strikethrough for discounts -- Free items display -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestPriceDisplay: - """Test price formatting and display.""" - - def test_price_displays_with_currency( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Item prices should display with currency code. - - Currency format should match event settings (EUR). - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Check that prices are displayed with EUR currency code - # General Admission is EUR 50.00 - # EUR is in a separate span, so check for both parts - # Use .first since there are multiple items with EUR - expect(page.locator('text=/EUR/').first).to_be_visible() - expect(page.locator('text=/50\\.00/').first).to_be_visible() - - def test_free_items_display_free_text( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_free, - widget_page - ): - """ - Items with price 0.00 should display "FREE" instead of $0.00. - - Makes free items more obvious to users. - """ - item = item_free - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should show "FREE" text - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should contain "FREE" or "free" text (case insensitive) - expect(item_elem.locator('text=/free/i')).to_be_visible() - - def test_price_includes_decimals( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_decimals, - widget_page - ): - """ - Prices should display with proper decimal formatting. - - EUR should show 2 decimal places (e.g., $25.00 not $25). - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should show price with .50 - expect(page.locator('text=/EUR.*12\\.50/')).to_be_visible() - - -@pytest.mark.django_db -class TestTaxDisplay: - """Test tax information display.""" - - def test_tax_rate_displayed_for_taxed_items( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_tax, - widget_page - ): - """ - Items with tax should show tax rate information. - - Should display "incl. X% VAT" or similar. - """ - item = item_with_tax - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should show tax information - # (could be "incl." or "plus" depending on settings) - # Looking for "19" and "%" near each other - expect(item_elem.locator('text=/19.*%|%.*19/')).to_be_visible() - - def test_items_without_tax_no_tax_line( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Items without tax rules should not show tax information. - - Tax line should be absent for tax-free items. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # items by default have no tax - # We just verify the items display without errors - for item in items: - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - -@pytest.mark.django_db -class TestDiscountedPricing: - """Test display of discounted prices.""" - - def test_widget_displays_prices_without_errors( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should display all prices without errors. - - This is a smoke test to ensure price rendering works. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # All items should display their prices - for item in items: - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should have EUR currency and price displayed - expect(item_elem.locator('text=/EUR/')).to_be_visible() - - -@pytest.mark.django_db -class TestPriceForVariations: - """Test price display for items with variations.""" - - def test_variation_price_range_when_collapsed( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Collapsed variations should show price range (min - max). - - E.g., "$20.00 - $30.00" for variations from $20 to $30. - """ - item, _ = item_with_variations - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Variations are collapsed by default - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should show price range in main row (not in hidden variations) - # Format: EUR 20.00 – 30.00 (en-dash, not hyphen) - # Look specifically in the main row's price column - main_row = item_elem.locator('.pretix-widget-main-item-row') - price_col = main_row.locator('.pretix-widget-item-price-col') - expect( - price_col.locator('text=/EUR.*20\\.00/') - ).to_be_visible() - expect(price_col.locator('text=/30\\.00/')).to_be_visible() - - def test_expanded_variations_show_individual_prices( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Expanded variations show individual prices for each variation. - - Each size should show its own price. - """ - item, variations = item_with_variations - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Expand variations - widget_page.expand_variations(item.name) - - # Check each variation shows a price with EUR currency - # We don't check exact amounts because formatting may vary - for var in variations: - var_elem = page.locator( - f'.pretix-widget-variation:has(' - f'strong:text-is("{var.value}"))' - ) - expect(var_elem).to_be_visible() - - # Should contain EUR currency code - expect(var_elem.locator('text=/EUR/')).to_be_visible() diff --git a/src/tests/e2e/test_widget_quantity_controls.py b/src/tests/e2e/test_widget_quantity_controls.py deleted file mode 100644 index d04aa00c15..0000000000 --- a/src/tests/e2e/test_widget_quantity_controls.py +++ /dev/null @@ -1,343 +0,0 @@ -""" -E2E Tests for Quantity Controls & Order Limits - -Tests that verify: -- Checkbox display for order_max=1 items -- +/- buttons for multi-quantity items -- Order minimum/maximum enforcement -- Auto-selection of single items -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestQuantityControls: - """Test quantity control UI elements.""" - - def test_checkbox_for_single_select_item( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_single_select, - widget_page - ): - """ - Item with order_max=1 should show checkbox instead of quantity input. - - For items limited to 1 per order, a checkbox is more intuitive than - a number input. - """ - item = item_single_select - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Find the item - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should have checkbox, NOT number input - expect(item_elem.locator('input[type="checkbox"]')).to_be_visible() - expect(item_elem.locator('input[type="number"]')).not_to_be_visible() - - def test_checkbox_selection( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_single_select, - widget_page - ): - """ - Checking the checkbox should select the item. - - User can check/uncheck to add/remove the item. - - Note: When there's only one item, it may be auto-selected by the widget. - This test verifies the check/uncheck functionality works regardless. - """ - item = item_single_select - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - checkbox = item_elem.locator('input[type="checkbox"]') - - # Ensure checkbox is unchecked to start test - if checkbox.is_checked(): - checkbox.uncheck() - expect(checkbox).not_to_be_checked() - - # Check it - checkbox.check() - expect(checkbox).to_be_checked() - - # Uncheck it - checkbox.uncheck() - expect(checkbox).not_to_be_checked() - - def test_plus_minus_buttons_for_multi_quantity( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Items with order_max > 1 should have +/- buttons. - - Plus/minus buttons provide an easy way to increment/decrement quantity. - """ - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Regular items should have number input with +/- buttons - item_elem = page.locator(f'.pretix-widget-item:has-text("{items[0].name}")') - - # Should have number input - expect(item_elem.locator('input[type="number"]')).to_be_visible() - - # Should have + and - buttons - expect(item_elem.locator('button:has-text("+")')).to_be_visible() - expect(item_elem.locator('button:has-text("-")')).to_be_visible() - - def test_increment_button_functionality( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Clicking + button should increase quantity by 1. - - Each click increments the value in the number input. - """ - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{items[0].name}")') - number_input = item_elem.locator('input[type="number"]') - plus_button = item_elem.locator('button:has-text("+")').first - - # Initial value should be 0 or empty - initial_value = number_input.input_value() - if not initial_value: - initial_value = "0" - - # Click + - plus_button.click() - page.wait_for_timeout(100) - - # Should be incremented - expect(number_input).to_have_value("1") - - # Click + again - plus_button.click() - page.wait_for_timeout(100) - - expect(number_input).to_have_value("2") - - def test_decrement_button_functionality( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Clicking - button should decrease quantity by 1. - - Quantity should not go below 0. - """ - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{items[0].name}")') - number_input = item_elem.locator('input[type="number"]') - plus_button = item_elem.locator('button:has-text("+")').first - minus_button = item_elem.locator('button:has-text("-")').first - - # Set to 2 - plus_button.click() - page.wait_for_timeout(100) - plus_button.click() - page.wait_for_timeout(100) - - expect(number_input).to_have_value("2") - - # Click - - minus_button.click() - page.wait_for_timeout(100) - - expect(number_input).to_have_value("1") - - # Click - again - minus_button.click() - page.wait_for_timeout(100) - - expect(number_input).to_have_value("0") - - def test_manual_quantity_input( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - User should be able to type quantity directly. - - Number input should accept typed values. - """ - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Directly set quantity using helper - widget_page.select_item_quantity(items[0].name, 5) - - # Verify value - item_elem = page.locator(f'.pretix-widget-item:has-text("{items[0].name}")') - number_input = item_elem.locator('input[type="number"]') - expect(number_input).to_have_value("5") - - -@pytest.mark.django_db -class TestOrderLimits: - """Test order minimum and maximum enforcement.""" - - def test_order_max_enforces_checkbox_for_single( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_single_select, - widget_page - ): - """Item with order_max=1 should show checkbox (implicit max enforcement).""" - item = item_single_select # This has order_max=1 - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # order_max=1 items get a checkbox (max is enforced by being binary) - expect(item_elem.locator('input[type="checkbox"]')).to_be_visible() - # No number input should exist - expect(item_elem.locator('input[type="number"]')).not_to_be_visible() - - def test_submit_with_multiple_items_opens_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """Multiple items with different quantities should open iframe checkout.""" - widget_page.goto(live_server_url, organizer.slug, event.slug) - - widget_page.select_item_quantity(items[0].name, 2) - widget_page.select_item_quantity(items[1].name, 1) - - widget_page.click_buy_button() - - # Iframe checkout should open - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'take_cart_id' in src - - -@pytest.mark.django_db -class TestFreePrice: - """Test pay-what-you-want (free price) items.""" - - def test_free_price_input_appears( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_free_price, - widget_page - ): - """ - Free price item should show price input field. - - User should be able to enter their own price. - """ - item = item_free_price - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should show a price input (type=number for price) - price_inputs = item_elem.locator('.pretix-widget-pricebox-price-input, input[name^="price_"]') - expect(price_inputs.first).to_be_visible() - - def test_free_price_minimum_enforcement( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_free_price, - widget_page - ): - """ - Free price input should have minimum value set. - - The min attribute should be set to the item's default price. - """ - item = item_free_price - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - price_input = item_elem.locator('.pretix-widget-pricebox-price-input, input[name^="price_"]').first - - # Should have min attribute - min_value = price_input.get_attribute('min') - assert min_value is not None - assert float(min_value) == float(item.default_price) - - def test_free_price_custom_amount( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_free_price, - widget_page - ): - """ - User should be able to enter custom price amount. - - Amount above minimum should be accepted. - """ - item = item_free_price - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - price_input = item_elem.locator('.pretix-widget-pricebox-price-input, input[name^="price_"]').first - - # Enter custom amount - price_input.fill("25.00") - - # Verify value - expect(price_input).to_have_value("25.00") diff --git a/src/tests/e2e/test_widget_responsive.py b/src/tests/e2e/test_widget_responsive.py deleted file mode 100644 index 3b905dcd9e..0000000000 --- a/src/tests/e2e/test_widget_responsive.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -E2E Tests for Responsive Behavior - -Tests that verify: -- Mobile layout at narrow widths (pretix-widget-mobile class) -- Layout updates on resize -- Desktop layout at wide widths -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestResponsiveLayout: - """Test responsive layout behavior.""" - - def test_mobile_class_at_narrow_width( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should add pretix-widget-mobile class when - container width <= 800px. - """ - # Set narrow viewport - page.set_viewport_size({"width": 375, "height": 667}) - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should have mobile class - expect(page.locator('.pretix-widget-mobile')).to_be_visible() - - def test_no_mobile_class_at_wide_width( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should NOT have pretix-widget-mobile class when - container width > 800px. - """ - # Ensure wide viewport (default is 1280) - page.set_viewport_size({"width": 1280, "height": 720}) - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should NOT have mobile class - expect(page.locator('.pretix-widget-mobile')).to_have_count(0) - - # But widget itself should be present - expect(page.locator('.pretix-widget')).to_be_visible() - - def test_responsive_layout_updates_on_resize( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Widget should switch to mobile layout when browser is resized - from desktop to mobile width. - """ - # Start with desktop viewport - page.set_viewport_size({"width": 1280, "height": 720}) - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Should not be mobile - expect(page.locator('.pretix-widget-mobile')).to_have_count(0) - - # Resize to mobile - page.set_viewport_size({"width": 375, "height": 667}) - - # Wait for ResizeObserver to fire - page.wait_for_timeout(500) - - # Should now have mobile class - expect(page.locator('.pretix-widget-mobile')).to_be_visible() diff --git a/src/tests/e2e/test_widget_variations.py b/src/tests/e2e/test_widget_variations.py deleted file mode 100644 index 029f17c830..0000000000 --- a/src/tests/e2e/test_widget_variations.py +++ /dev/null @@ -1,235 +0,0 @@ -""" -E2E Tests for Product Variations - -Tests that verify items with variations (sizes, colors, etc.) work correctly: -- Expand/collapse behavior -- Price ranges -- Individual variation selection -- Auto-expand when filtered -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestProductVariations: - """Test product variation functionality.""" - - def test_item_with_variations_shows_toggle_button( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Item with variations should show expand/collapse button. - - Variations should be collapsed by default with a button to expand them. - """ - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Should show item name - expect(page.locator(f'text="{item.name}"')).to_be_visible() - - # Should show variations toggle button - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - toggle_btn = item_elem.locator('button:has-text("variants"), button:has-text("Show variants")') - expect(toggle_btn.first).to_be_visible() - - def test_expand_variations_on_click( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Clicking toggle button should expand variations. - - After expanding, all variation options should be visible. - """ - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Expand variations - widget_page.expand_variations(item.name) - - # Wait for variations to be visible - page.wait_for_timeout(500) # Wait for animation - - # All variations should now be visible - for variation in variations: - expect(page.locator(f'text="{variation.value}"')).to_be_visible() - - def test_collapse_variations_on_second_click( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """Clicking toggle again should collapse variations.""" - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - toggle_btn = item_elem.locator('button:has-text("variants"), button:has-text("Show variants")') - - # First click - expand - toggle_btn.first.click() - page.wait_for_timeout(500) - - # Variations should be visible - for variation in variations: - expect(page.locator(f'text="{variation.value}"')).to_be_visible() - - # Second click - collapse - toggle_btn.first.click() - page.wait_for_timeout(500) - - # Variation inputs should no longer be visible - for variation in variations: - var_row = item_elem.locator( - f'.pretix-widget-variation:has(strong:text-is("{variation.value}"))') - expect(var_row.locator('input')).not_to_be_visible() - - def test_price_range_for_collapsed_variations( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Collapsed variations should show price range. - - When variations have different prices, should display range like "$20 - $30". - """ - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Should show price range - # Variations go from $20 to $30 - item_elem = page.locator(f'.pretix-widget-item:has-text("{item.name}")') - - # Look for price range indicators - # Format is "EUR 20.00 – 30.00" in the main item's price box (first one) - price_box = item_elem.locator('.pretix-widget-pricebox').first - expect(price_box).to_contain_text('20.00') - expect(price_box).to_contain_text('30.00') - - def test_select_variation_quantity( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - User should be able to select quantity for specific variation. - - After expanding variations, each should have its own quantity selector. - """ - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Expand variations - widget_page.expand_variations(item.name) - page.wait_for_timeout(500) - - # Select quantity for "Medium" variation - widget_page.select_variation_quantity(item.name, "Medium", 2) - - # Verify input has the value - medium_var = page.locator('.pretix-widget-variation:has-text("Medium")') - input_field = medium_var.locator('input[type="number"]') - expect(input_field).to_have_value("2") - - def test_variation_individual_prices( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """ - Each variation should show its individual price. - - When expanded, variations should display their specific prices. - """ - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Expand variations - widget_page.expand_variations(item.name) - page.wait_for_timeout(500) - - # Check each variation shows its price - variation_prices = { - 'Small': '20.00', - 'Medium': '25.00', - 'Large': '25.00', - 'X-Large': '30.00', - } - - for var_name, price in variation_prices.items(): - # Use exact heading match to avoid "Large" matching "X-Large" - var_elem = page.locator( - f'.pretix-widget-variation:has(strong:text-is("{var_name}"))' - ) - expect(var_elem).to_be_visible() - # Price should be visible within the variation element - expect(var_elem.locator(f'text=/{price}/')).to_be_visible() - - -@pytest.mark.django_db -class TestVariationSubmission: - """Test that variation selections submit correctly.""" - - def test_submit_with_variation_selection( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_with_variations, - widget_page - ): - """Submitting with a variation selected should open iframe checkout.""" - item, variations = item_with_variations - - widget_page.goto(live_server_url, organizer.slug, event.slug) - - # Expand and select a variation - widget_page.expand_variations(item.name) - page.wait_for_timeout(500) - widget_page.select_variation_quantity(item.name, "Large", 1) - - widget_page.click_buy_button() - - # Iframe checkout should open with the variation in the cart - widget_page.wait_for_iframe_checkout() - - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'take_cart_id' in src diff --git a/src/tests/e2e/test_widget_vouchers.py b/src/tests/e2e/test_widget_vouchers.py deleted file mode 100644 index 7b568dd32a..0000000000 --- a/src/tests/e2e/test_widget_vouchers.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -E2E Tests for Voucher Redemption - -Tests that verify: -- Voucher input field appears when vouchers exist -- Voucher redemption flow works -- Voucher input hidden when disable-vouchers is set -- Voucher explanation text displays -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestVoucherDisplay: - """Test voucher input rendering.""" - - def test_voucher_input_appears_when_vouchers_exist( - self, - page: Page, - live_server_url: str, - organizer, - event, - voucher, - widget_page - ): - """ - Voucher input field should be visible when event has vouchers. - - The widget checks `vouchers_exist` in the API response. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Voucher section should be visible - voucher_section = page.locator('.pretix-widget-voucher') - expect(voucher_section).to_be_visible() - - # Should have the "Redeem a voucher" heading - expect(page.locator( - '.pretix-widget-voucher-headline' - )).to_be_visible() - - # Should have the voucher input - expect(page.locator( - '.pretix-widget-voucher-input' - )).to_be_visible() - - def test_voucher_input_hidden_when_no_vouchers( - self, - page: Page, - live_server_url: str, - organizer, - event, - items, - widget_page - ): - """ - Voucher input should not appear when no vouchers exist. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Voucher section should NOT be visible - voucher_section = page.locator('.pretix-widget-voucher') - expect(voucher_section).to_have_count(0) - - def test_voucher_explanation_text_displays( - self, - page: Page, - live_server_url: str, - organizer, - event, - voucher, - widget_page - ): - """ - Voucher explanation text should display when configured. - """ - # Set voucher explanation text on the event - event.settings.set( - 'voucher_explanation_text', - 'Enter your voucher code to get a discount.' - ) - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Explanation text should be visible - explanation = page.locator('.pretix-widget-voucher-text') - expect(explanation).to_be_visible() - expect(explanation).to_contain_text('Enter your voucher code') - - -@pytest.mark.django_db -class TestVoucherRedemption: - """Test voucher redemption flow.""" - - def test_redeem_voucher_opens_checkout( - self, - page: Page, - live_server_url: str, - organizer, - event, - voucher, - widget_page - ): - """ - Entering a voucher code and clicking Redeem should open checkout. - - With skip-ssl-check (added by test harness), this opens in iframe. - """ - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Enter voucher code - voucher_input = page.locator('.pretix-widget-voucher-input') - voucher_input.fill('TESTCODE2024') - - # Click the Redeem button - page.locator( - '.pretix-widget-voucher-button-wrap button' - ).click() - - # With skip-ssl-check, voucher redemption opens in iframe - iframe = widget_page.wait_for_iframe_checkout() - - # The iframe src should contain the voucher code - iframe_elem = page.locator('iframe[name^="pretix-widget-"]') - src = iframe_elem.get_attribute('src') - assert 'TESTCODE2024' in src or 'voucher' in src diff --git a/src/tests/e2e/test_widget_waitinglist.py b/src/tests/e2e/test_widget_waitinglist.py deleted file mode 100644 index d03cacdee0..0000000000 --- a/src/tests/e2e/test_widget_waitinglist.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -E2E Tests for Waiting List Integration - -Tests that verify: -- Waiting list link appears for sold out items when enabled -- Waiting list link URL includes correct parameters -""" -import pytest -from playwright.sync_api import Page, expect - - -@pytest.mark.django_db -class TestWaitingList: - """Test waiting list display for sold out items.""" - - def test_waiting_list_link_appears_when_sold_out( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out_with_waitinglist, - widget_page - ): - """ - Sold out items with waiting list enabled should show - a "Waiting list" link. - - Requires: - - Event setting: waiting_list_enabled = True - - Item: allow_waitinglist = True - - Item availability < 100 (sold out) - """ - item = item_sold_out_with_waitinglist - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - # Find the sold out item - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - expect(item_elem).to_be_visible() - - # Should show waiting list link - waiting_list = item_elem.locator( - '.pretix-widget-waiting-list-link a') - expect(waiting_list).to_be_visible() - - def test_waiting_list_link_url_contains_item_id( - self, - page: Page, - live_server_url: str, - organizer, - event, - item_sold_out_with_waitinglist, - widget_page - ): - """ - Waiting list link URL should include item ID parameter. - """ - item = item_sold_out_with_waitinglist - - widget_page.goto( - live_server_url, organizer.slug, event.slug) - - item_elem = page.locator( - f'.pretix-widget-item:has-text("{item.name}")') - waiting_list_link = item_elem.locator( - '.pretix-widget-waiting-list-link a') - - href = waiting_list_link.get_attribute('href') - assert href is not None - assert f'item={item.pk}' in href - assert 'waitinglist' in href