diff --git a/CHANGELOG.md b/CHANGELOG.md index 1522a62..e5db6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- **Credit system test suite** (`tests/test_credits.py` — 24 tests) — covers + `get_balance`, `add_credits`, `spend_credits`, `compute_credit_cost`, + `already_unlocked`, `unlock_lead`, `monthly_credit_refill`, `get_ledger`; + tests run against real in-memory SQLite with no mocking +- **Supplier webhook test suite** (`tests/test_supplier_webhooks.py` — 10 tests) + — integration tests POSTing signed payloads to `/billing/webhook/paddle` and + verifying DB state for credit pack purchases, sticky boosts, supplier + subscription activation (growth/pro tiers, boost items, noop on missing + supplier_id), and business plan PDF export + +### Removed +- **Dead test** `test_mobile_nav_no_overflow` from `test_visual.py` — evaluated + JS but never asserted the result; always passed regardless of overflow state + +### Fixed +- **Supplier logo missing on public directory** — directory cards and supplier + detail page only checked `logo_url` (external URL); now checks `logo_file` + first (uploaded via dashboard), falling back to `logo_url` +- **Admin forward history links to wrong page** — supplier name in lead forward + history linked to `/directory/` (public index) instead of + `/admin/suppliers/` (admin detail page) +- **Dashboard lead country filter drops heat/timeline state** — hidden inputs + used mismatched names (`heat_hidden`/`timeline_hidden` vs `heat`/`timeline`), + silently resetting other filters when country changed +- **Directory pagination drops region filter** — paginating after filtering by + region lost the `region` parameter; added to all pagination link templates +- **`boost_card_color` missing from webhook handler** — `BOOST_PRICE_KEYS` + omitted `boost_card_color`, so purchasing the card color boost via Paddle + would silently fail to create the `supplier_boosts` record; also removed + stale `boost_newsletter` entry (replaced by card color boost) +- **Sticky boost DB operations not atomic** — `boost_sticky_week` and + `boost_sticky_month` handlers issued two bare `execute()` calls (INSERT + + UPDATE) without a transaction; wrapped in `db_transaction()` to prevent + partial writes + ### Added - **Clickable admin list rows** — entire row is clickable on leads, suppliers, and users admin list pages (`data-href` + JS click handler that skips diff --git a/padelnomics/tests/test_credits.py b/padelnomics/tests/test_credits.py new file mode 100644 index 0000000..0b4314b --- /dev/null +++ b/padelnomics/tests/test_credits.py @@ -0,0 +1,294 @@ +""" +Tests for the credit system (credits.py). + +Pure SQL operations against real in-memory SQLite — no mocking needed. +""" +from datetime import datetime + +import pytest + +from padelnomics.credits import ( + InsufficientCredits, + add_credits, + already_unlocked, + compute_credit_cost, + get_balance, + get_ledger, + monthly_credit_refill, + spend_credits, + unlock_lead, +) + + +# ── Fixtures ───────────────────────────────────────────────── + + +@pytest.fixture +async def supplier(db): + """Supplier with credit_balance=100, monthly_credits=30, tier=growth.""" + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO suppliers + (name, slug, country_code, region, category, tier, + credit_balance, monthly_credits, created_at) + VALUES ('Test Supplier', 'test-supplier', 'DE', 'Europe', 'Courts', + 'growth', 100, 30, ?)""", + (now,), + ) as cursor: + supplier_id = cursor.lastrowid + await db.commit() + return {"id": supplier_id} + + +@pytest.fixture +async def lead(db): + """Lead request with heat_score=warm, credit_cost=20.""" + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO lead_requests + (lead_type, heat_score, credit_cost, status, created_at) + VALUES ('supplier_quote', 'warm', 20, 'new', ?)""", + (now,), + ) as cursor: + lead_id = cursor.lastrowid + await db.commit() + return {"id": lead_id, "credit_cost": 20} + + +# ── GetBalance ─────────────────────────────────────────────── + + +class TestGetBalance: + async def test_returns_zero_for_unknown_supplier(self, db): + assert await get_balance(99999) == 0 + + async def test_returns_current_balance(self, db, supplier): + assert await get_balance(supplier["id"]) == 100 + + +# ── AddCredits ─────────────────────────────────────────────── + + +class TestAddCredits: + async def test_adds_credits_and_updates_balance(self, db, supplier): + new_balance = await add_credits(supplier["id"], 50, "pack_purchase") + assert new_balance == 150 + assert await get_balance(supplier["id"]) == 150 + + async def test_creates_ledger_entry(self, db, supplier): + await add_credits( + supplier["id"], 50, "pack_purchase", + reference_id=42, note="Credit pack: credits_50", + ) + rows = await db.execute_fetchall( + "SELECT * FROM credit_ledger WHERE supplier_id = ?", + (supplier["id"],), + ) + assert len(rows) == 1 + row = rows[0] + assert row[2] == 50 # delta + assert row[3] == 150 # balance_after + assert row[4] == "pack_purchase" # event_type + assert row[5] == 42 # reference_id + assert row[6] == "Credit pack: credits_50" # note + + async def test_multiple_adds_accumulate(self, db, supplier): + await add_credits(supplier["id"], 10, "pack_purchase") + await add_credits(supplier["id"], 10, "pack_purchase") + await add_credits(supplier["id"], 10, "pack_purchase") + assert await get_balance(supplier["id"]) == 130 + + +# ── SpendCredits ───────────────────────────────────────────── + + +class TestSpendCredits: + async def test_spends_credits_and_updates_balance(self, db, supplier): + new_balance = await spend_credits(supplier["id"], 30, "lead_unlock") + assert new_balance == 70 + assert await get_balance(supplier["id"]) == 70 + + async def test_creates_negative_ledger_entry(self, db, supplier): + await spend_credits(supplier["id"], 30, "lead_unlock") + rows = await db.execute_fetchall( + "SELECT * FROM credit_ledger WHERE supplier_id = ?", + (supplier["id"],), + ) + assert len(rows) == 1 + assert rows[0][2] == -30 # delta + assert rows[0][3] == 70 # balance_after + + async def test_raises_insufficient_credits(self, db, supplier): + with pytest.raises(InsufficientCredits) as exc_info: + await spend_credits(supplier["id"], 200, "lead_unlock") + assert exc_info.value.balance == 100 + assert exc_info.value.required == 200 + + async def test_spend_exact_balance(self, db, supplier): + new_balance = await spend_credits(supplier["id"], 100, "lead_unlock") + assert new_balance == 0 + assert await get_balance(supplier["id"]) == 0 + + +# ── ComputeCreditCost ──────────────────────────────────────── + + +class TestComputeCreditCost: + def test_hot_costs_35(self): + assert compute_credit_cost({"heat_score": "hot"}) == 35 + + def test_warm_costs_20(self): + assert compute_credit_cost({"heat_score": "warm"}) == 20 + + def test_cool_costs_8(self): + assert compute_credit_cost({"heat_score": "cool"}) == 8 + + def test_missing_heat_defaults_to_cool(self): + assert compute_credit_cost({}) == 8 + assert compute_credit_cost({"heat_score": None}) == 8 + + +# ── AlreadyUnlocked ────────────────────────────────────────── + + +class TestAlreadyUnlocked: + async def test_returns_false_when_not_unlocked(self, db, supplier, lead): + assert await already_unlocked(supplier["id"], lead["id"]) is False + + async def test_returns_true_after_unlock(self, db, supplier, lead): + now = datetime.utcnow().isoformat() + await db.execute( + """INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, created_at) + VALUES (?, ?, 20, ?)""", + (lead["id"], supplier["id"], now), + ) + await db.commit() + assert await already_unlocked(supplier["id"], lead["id"]) is True + + +# ── UnlockLead ──────────────────────────────────────────────── + + +class TestUnlockLead: + async def test_unlocks_lead_and_returns_details(self, db, supplier, lead): + result = await unlock_lead(supplier["id"], lead["id"]) + + assert result["forward_id"] is not None + assert result["credit_cost"] == 20 + assert result["new_balance"] == 80 + assert result["lead"]["id"] == lead["id"] + + # Verify lead_forwards row + row = await db.execute_fetchall( + "SELECT * FROM lead_forwards WHERE supplier_id = ? AND lead_id = ?", + (supplier["id"], lead["id"]), + ) + assert len(row) == 1 + assert row[0][3] == 20 # credit_cost + + # Verify ledger entry + ledger = await db.execute_fetchall( + "SELECT * FROM credit_ledger WHERE supplier_id = ? AND event_type = 'lead_unlock'", + (supplier["id"],), + ) + assert len(ledger) == 1 + assert ledger[0][2] == -20 # delta + assert ledger[0][3] == 80 # balance_after + + # Verify unlock_count incremented + lead_row = await db.execute_fetchall( + "SELECT unlock_count FROM lead_requests WHERE id = ?", (lead["id"],), + ) + assert lead_row[0][0] == 1 + + async def test_raises_on_duplicate_unlock(self, db, supplier, lead): + await unlock_lead(supplier["id"], lead["id"]) + with pytest.raises(ValueError, match="already unlocked"): + await unlock_lead(supplier["id"], lead["id"]) + + async def test_raises_on_missing_lead(self, db, supplier): + with pytest.raises(ValueError, match="not found"): + await unlock_lead(supplier["id"], 99999) + + async def test_raises_insufficient_credits(self, db, lead): + """Supplier with only 5 credits tries to unlock a 20-credit lead.""" + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO suppliers + (name, slug, country_code, region, category, tier, + credit_balance, monthly_credits, created_at) + VALUES ('Poor Supplier', 'poor-supplier', 'DE', 'Europe', 'Courts', + 'growth', 5, 0, ?)""", + (now,), + ) as cursor: + poor_id = cursor.lastrowid + await db.commit() + + with pytest.raises(InsufficientCredits) as exc_info: + await unlock_lead(poor_id, lead["id"]) + assert exc_info.value.balance == 5 + assert exc_info.value.required == 20 + + +# ── MonthlyRefill ───────────────────────────────────────────── + + +class TestMonthlyRefill: + async def test_refills_monthly_credits(self, db, supplier): + new_balance = await monthly_credit_refill(supplier["id"]) + assert new_balance == 130 # 100 + 30 + assert await get_balance(supplier["id"]) == 130 + + # Verify ledger entry + ledger = await db.execute_fetchall( + "SELECT * FROM credit_ledger WHERE supplier_id = ? AND event_type = 'monthly_allocation'", + (supplier["id"],), + ) + assert len(ledger) == 1 + assert ledger[0][2] == 30 # delta + + async def test_noop_when_no_monthly_credits(self, db): + """Supplier with monthly_credits=0 gets no refill.""" + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO suppliers + (name, slug, country_code, region, category, tier, + credit_balance, monthly_credits, created_at) + VALUES ('Free Supplier', 'free-supplier', 'DE', 'Europe', 'Courts', + 'free', 50, 0, ?)""", + (now,), + ) as cursor: + free_id = cursor.lastrowid + await db.commit() + + result = await monthly_credit_refill(free_id) + assert result == 0 + assert await get_balance(free_id) == 50 + + +# ── GetLedger ───────────────────────────────────────────────── + + +class TestGetLedger: + async def test_returns_entries_in_descending_order(self, db, supplier): + await add_credits(supplier["id"], 10, "pack_purchase", note="first") + await add_credits(supplier["id"], 20, "pack_purchase", note="second") + await add_credits(supplier["id"], 30, "pack_purchase", note="third") + + entries = await get_ledger(supplier["id"]) + assert len(entries) == 3 + # Most recent first + assert entries[0]["note"] == "third" + assert entries[1]["note"] == "second" + assert entries[2]["note"] == "first" + + async def test_respects_limit(self, db, supplier): + for i in range(5): + await add_credits(supplier["id"], 1, "pack_purchase", note=f"entry_{i}") + + entries = await get_ledger(supplier["id"], limit=2) + assert len(entries) == 2 + + async def test_empty_for_unknown_supplier(self, db): + entries = await get_ledger(99999) + assert entries == [] diff --git a/padelnomics/tests/test_supplier_webhooks.py b/padelnomics/tests/test_supplier_webhooks.py new file mode 100644 index 0000000..bf15790 --- /dev/null +++ b/padelnomics/tests/test_supplier_webhooks.py @@ -0,0 +1,380 @@ +""" +Integration tests for supplier webhook handlers. + +POST real webhook payloads to /billing/webhook/paddle and verify DB state. +Uses the existing client, db, sign_payload from conftest. +""" +import json +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, patch + +import pytest +from conftest import sign_payload + +WEBHOOK_PATH = "/billing/webhook/paddle" +SIG_HEADER = "Paddle-Signature" + + +# ── Fixtures ───────────────────────────────────────────────── + + +@pytest.fixture +async def supplier(db): + """Supplier with tier=free, credit_balance=0.""" + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO suppliers + (name, slug, country_code, region, category, tier, + credit_balance, monthly_credits, created_at) + VALUES ('Webhook Test Supplier', 'webhook-test', 'DE', 'Europe', + 'Courts', 'free', 0, 0, ?)""", + (now,), + ) as cursor: + supplier_id = cursor.lastrowid + await db.commit() + return {"id": supplier_id} + + +@pytest.fixture +async def paddle_products(db): + """Insert paddle_products rows for all keys the handlers need.""" + now = datetime.utcnow().isoformat() + products = [ + ("credits_25", "pri_credits25", "Credit Pack 25", 999, "one_time"), + ("credits_100", "pri_credits100", "Credit Pack 100", 3290, "one_time"), + ("boost_sticky_week", "pri_sticky_week", "Sticky Week", 4900, "one_time"), + ("boost_sticky_month", "pri_sticky_month", "Sticky Month", 14900, "one_time"), + ("boost_highlight", "pri_highlight", "Highlight", 2900, "recurring"), + ("boost_verified", "pri_verified", "Verified Badge", 1900, "recurring"), + ("boost_card_color", "pri_card_color", "Card Color", 1900, "recurring"), + ("supplier_growth", "pri_growth", "Growth Plan", 14900, "recurring"), + ("supplier_pro", "pri_pro", "Pro Plan", 39900, "recurring"), + ("business_plan", "pri_bplan", "Business Plan PDF", 9900, "one_time"), + ] + for key, price_id, name, price_cents, billing_type in products: + await db.execute( + """INSERT INTO paddle_products + (key, paddle_product_id, paddle_price_id, name, price_cents, billing_type, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + (key, f"pro_{key}", price_id, name, price_cents, billing_type, now), + ) + await db.commit() + + +# ── Helpers ────────────────────────────────────────────────── + + +def make_transaction_payload(items, supplier_id, **custom_data_extra): + """Build a transaction.completed event payload.""" + custom_data = {"supplier_id": str(supplier_id), **custom_data_extra} + return { + "event_type": "transaction.completed", + "data": { + "id": "txn_test_123", + "status": "completed", + "customer_id": "ctm_supplier_test", + "custom_data": custom_data, + "items": [ + {"price": {"id": price_id}, "quantity": qty} + for price_id, qty in items + ], + }, + } + + +def make_supplier_activation_payload(items, supplier_id, plan, user_id): + """Build a subscription.activated event for supplier plans.""" + return { + "event_type": "subscription.activated", + "data": { + "id": "sub_supplier_test_456", + "status": "active", + "customer_id": "ctm_supplier_test", + "custom_data": { + "supplier_id": str(supplier_id), + "plan": plan, + "user_id": str(user_id), + }, + "current_billing_period": { + "starts_at": "2026-02-01T00:00:00.000000Z", + "ends_at": "2026-03-01T00:00:00.000000Z", + }, + "items": [ + {"price": {"id": price_id}, "quantity": qty} + for price_id, qty in items + ], + }, + } + + +def _post_webhook(client, payload): + """Sign and POST a webhook payload, return awaitable response.""" + payload_bytes = json.dumps(payload).encode() + sig = sign_payload(payload_bytes) + return client.post( + WEBHOOK_PATH, + data=payload_bytes, + headers={SIG_HEADER: sig, "Content-Type": "application/json"}, + ) + + +# ── Credit Pack Purchase ───────────────────────────────────── + + +class TestCreditPackPurchase: + async def test_credits_25_adds_25_credits(self, client, db, supplier, paddle_products): + payload = make_transaction_payload([("pri_credits25", 1)], supplier["id"]) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + row = await db.execute_fetchall( + "SELECT credit_balance FROM suppliers WHERE id = ?", (supplier["id"],), + ) + assert row[0][0] == 25 + + ledger = await db.execute_fetchall( + "SELECT delta, event_type, note FROM credit_ledger WHERE supplier_id = ?", + (supplier["id"],), + ) + assert len(ledger) == 1 + assert ledger[0][0] == 25 + assert ledger[0][1] == "pack_purchase" + + async def test_credits_100_adds_100_credits(self, client, db, supplier, paddle_products): + payload = make_transaction_payload([("pri_credits100", 1)], supplier["id"]) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + row = await db.execute_fetchall( + "SELECT credit_balance FROM suppliers WHERE id = ?", (supplier["id"],), + ) + assert row[0][0] == 100 + + +# ── Sticky Boost Purchase ──────────────────────────────────── + + +class TestStickyBoostPurchase: + async def test_sticky_week_creates_boost_and_updates_supplier( + self, client, db, supplier, paddle_products, + ): + payload = make_transaction_payload( + [("pri_sticky_week", 1)], supplier["id"], + sticky_country="DE", + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + # Verify supplier_boosts row + boosts = await db.execute_fetchall( + "SELECT boost_type, status, expires_at FROM supplier_boosts WHERE supplier_id = ?", + (supplier["id"],), + ) + assert len(boosts) == 1 + assert boosts[0][0] == "sticky_week" + assert boosts[0][1] == "active" + # expires_at should be ~7 days from now + expires = datetime.fromisoformat(boosts[0][2]) + assert abs((expires - datetime.utcnow()).days - 7) <= 1 + + # Verify sticky_until set on supplier + sup = await db.execute_fetchall( + "SELECT sticky_until, sticky_country FROM suppliers WHERE id = ?", + (supplier["id"],), + ) + assert sup[0][0] is not None # sticky_until set + assert sup[0][1] == "DE" + + async def test_sticky_month_creates_boost_and_updates_supplier( + self, client, db, supplier, paddle_products, + ): + payload = make_transaction_payload( + [("pri_sticky_month", 1)], supplier["id"], + sticky_country="ES", + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + boosts = await db.execute_fetchall( + "SELECT boost_type, expires_at FROM supplier_boosts WHERE supplier_id = ?", + (supplier["id"],), + ) + assert len(boosts) == 1 + assert boosts[0][0] == "sticky_month" + expires = datetime.fromisoformat(boosts[0][1]) + assert abs((expires - datetime.utcnow()).days - 30) <= 1 + + async def test_sticky_boost_sets_country(self, client, db, supplier, paddle_products): + payload = make_transaction_payload( + [("pri_sticky_week", 1)], supplier["id"], + sticky_country="FR", + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + sup = await db.execute_fetchall( + "SELECT sticky_country FROM suppliers WHERE id = ?", (supplier["id"],), + ) + assert sup[0][0] == "FR" + + +# ── Supplier Subscription Activated ────────────────────────── + + +class TestSupplierSubscriptionActivated: + async def test_growth_plan_sets_tier_and_credits( + self, client, db, supplier, paddle_products, test_user, + ): + payload = make_supplier_activation_payload( + items=[("pri_growth", 1)], + supplier_id=supplier["id"], + plan="supplier_growth", + user_id=test_user["id"], + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + row = await db.execute_fetchall( + "SELECT tier, credit_balance, monthly_credits, claimed_by FROM suppliers WHERE id = ?", + (supplier["id"],), + ) + assert row[0][0] == "growth" + assert row[0][1] == 30 + assert row[0][2] == 30 + assert row[0][3] == test_user["id"] + + async def test_pro_plan_sets_tier_and_credits( + self, client, db, supplier, paddle_products, test_user, + ): + payload = make_supplier_activation_payload( + items=[("pri_pro", 1)], + supplier_id=supplier["id"], + plan="supplier_pro", + user_id=test_user["id"], + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + row = await db.execute_fetchall( + "SELECT tier, credit_balance, monthly_credits FROM suppliers WHERE id = ?", + (supplier["id"],), + ) + assert row[0][0] == "pro" + assert row[0][1] == 100 + assert row[0][2] == 100 + + async def test_boost_items_create_boost_records( + self, client, db, supplier, paddle_products, test_user, + ): + payload = make_supplier_activation_payload( + items=[ + ("pri_growth", 1), + ("pri_highlight", 1), + ("pri_verified", 1), + ], + supplier_id=supplier["id"], + plan="supplier_growth", + user_id=test_user["id"], + ) + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + # Verify boost rows + boosts = await db.execute_fetchall( + "SELECT boost_type FROM supplier_boosts WHERE supplier_id = ? ORDER BY boost_type", + (supplier["id"],), + ) + boost_types = [b[0] for b in boosts] + assert "highlight" in boost_types + assert "verified" in boost_types + + # Verify denormalized columns + sup = await db.execute_fetchall( + "SELECT highlight, is_verified FROM suppliers WHERE id = ?", + (supplier["id"],), + ) + assert sup[0][0] == 1 # highlight + assert sup[0][1] == 1 # is_verified + + async def test_no_supplier_id_is_noop( + self, client, db, supplier, paddle_products, test_user, + ): + """Activation without supplier_id in custom_data does nothing.""" + payload = { + "event_type": "subscription.activated", + "data": { + "id": "sub_no_supplier", + "status": "active", + "customer_id": "ctm_test", + "custom_data": { + "plan": "supplier_growth", + "user_id": str(test_user["id"]), + # no supplier_id + }, + "current_billing_period": { + "starts_at": "2026-02-01T00:00:00.000000Z", + "ends_at": "2026-03-01T00:00:00.000000Z", + }, + "items": [], + }, + } + resp = await _post_webhook(client, payload) + assert resp.status_code == 200 + + # Supplier unchanged + row = await db.execute_fetchall( + "SELECT tier, credit_balance FROM suppliers WHERE id = ?", + (supplier["id"],), + ) + assert row[0][0] == "free" + assert row[0][1] == 0 + + +# ── Business Plan Purchase ─────────────────────────────────── + + +class TestBusinessPlanPurchase: + async def test_creates_export_record( + self, client, db, supplier, paddle_products, test_user, + ): + # Need a scenario for the export + now = datetime.utcnow().isoformat() + async with db.execute( + """INSERT INTO scenarios (user_id, name, state_json, created_at) + VALUES (?, 'Test Scenario', '{}', ?)""", + (test_user["id"], now), + ) as cursor: + scenario_id = cursor.lastrowid + await db.commit() + + payload = { + "event_type": "transaction.completed", + "data": { + "id": "txn_bplan_123", + "status": "completed", + "customer_id": "ctm_test", + "custom_data": { + "user_id": str(test_user["id"]), + "scenario_id": str(scenario_id), + "language": "de", + }, + "items": [{"price": {"id": "pri_bplan"}, "quantity": 1}], + }, + } + + # enqueue is lazily imported inside the handler via `from ..worker import enqueue` + # Patching at the module level ensures the lazy import picks up the mock + with patch("padelnomics.worker.enqueue", new_callable=AsyncMock): + resp = await _post_webhook(client, payload) + + assert resp.status_code == 200 + + # Verify business_plan_exports row + exports = await db.execute_fetchall( + "SELECT user_id, scenario_id, language, status FROM business_plan_exports", + ) + assert len(exports) == 1 + assert exports[0][0] == test_user["id"] + assert exports[0][1] == scenario_id + assert exports[0][2] == "de" + assert exports[0][3] == "pending" diff --git a/padelnomics/tests/test_visual.py b/padelnomics/tests/test_visual.py index ccd4192..ddde3ec 100644 --- a/padelnomics/tests/test_visual.py +++ b/padelnomics/tests/test_visual.py @@ -275,21 +275,6 @@ def test_mobile_landing_screenshot(live_server, browser): page.close() -def test_mobile_nav_no_overflow(live_server, browser): - """Verify nav doesn't overflow on mobile.""" - page = browser.new_page(viewport={"width": 375, "height": 812}) - page.goto(live_server) - page.wait_for_load_state("networkidle") - - page.evaluate(""" - (() => { - const nav = document.querySelector('nav'); - return nav.scrollWidth > nav.clientWidth; - })() - """) - page.close() - # Nav may wrap on mobile, which is fine — just verify no JS errors - def test_landing_no_dark_remnants(live_server, page): """Check that no major elements have dark backgrounds."""