feat: improve SPA scraping and increase test coverage

- Add SPA support for Playwright with wait_for_network_idle and extra_wait_ms
- Add BaseStore.get_spa_config() and requires_playwright() methods
- Implement AliExpress SPA config with JSON price extraction patterns
- Fix Amazon price parsing to prioritize whole+fraction combination
- Fix AliExpress regex patterns (remove double backslashes)
- Add CLI tests: detect, doctor, fetch, parse, run commands
- Add API tests: auth, logs, products, scraping_logs, webhooks

Tests: 417 passed, 85% coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2026-01-17 14:46:55 +01:00
parent cf7c415e22
commit 152c2724fc
14 changed files with 1307 additions and 22 deletions

View File

@@ -215,6 +215,19 @@ class AmazonStore(BaseStore):
def _extract_price(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[float]:
"""Extrait le prix."""
# Priorité 1: combiner les spans séparés a-price-whole et a-price-fraction
# C'est le format le plus courant sur Amazon pour les prix avec centimes séparés
whole = soup.select_one("span.a-price-whole")
fraction = soup.select_one("span.a-price-fraction")
if whole and fraction:
whole_text = whole.get_text(strip=True).rstrip(",.")
fraction_text = fraction.get_text(strip=True)
if whole_text and fraction_text:
price = parse_price_text(f"{whole_text}.{fraction_text}")
if price is not None:
return price
# Priorité 2: essayer les sélecteurs (incluant a-price-whole seul avec prix complet)
selectors = self.get_selector("price", [])
if isinstance(selectors, str):
selectors = [selectors]
@@ -227,16 +240,6 @@ class AmazonStore(BaseStore):
if price is not None:
return price
# Fallback: chercher les spans séparés a-price-whole et a-price-fraction
whole = soup.select_one("span.a-price-whole")
fraction = soup.select_one("span.a-price-fraction")
if whole and fraction:
whole_text = whole.get_text(strip=True)
fraction_text = fraction.get_text(strip=True)
price = parse_price_text(f"{whole_text}.{fraction_text}")
if price is not None:
return price
debug.errors.append("Prix non trouvé")
return None