Test dashboard: full menu system, all-service tests, filtering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s

- Run tests for all 10 services via per-service pytest subprocesses
- Group results by service with section headers
- Clickable summary cards filter by outcome (passed/failed/errors/skipped)
- Service filter nav using ~nav-link buttons in menu bar
- Full menu integration: ~header-row + ~header-child + ~menu-row
- Show logo image via cart-mini rendering
- Mount full service directories in docker-compose for test access
- Add 24 unit test files across 9 services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:54:25 +00:00
parent 81e51ae7bc
commit 3809affcab
41 changed files with 2484 additions and 110 deletions

0
market/tests/__init__.py Normal file
View File

21
market/tests/conftest.py Normal file
View File

@@ -0,0 +1,21 @@
"""Market test fixtures — direct module loading to avoid bp __init__ chains."""
from __future__ import annotations
import importlib.util
import sys
def _load(name: str, path: str):
"""Import a .py file directly, bypassing package __init__ chains."""
if name in sys.modules:
return sys.modules[name]
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
sys.path.insert(0, "/app/market")
_load("market.scrape.listings", "/app/market/scrape/listings.py")

View File

@@ -0,0 +1,78 @@
"""Unit tests for listings parsing utilities."""
from __future__ import annotations
import pytest
from market.scrape.listings import (
parse_total_pages_from_text,
_first_from_srcset,
_dedupe_preserve_order_by,
_filename_key,
)
class TestParseTotalPages:
def test_standard(self):
assert parse_total_pages_from_text("Showing 36 of 120") == 4
def test_exact_fit(self):
assert parse_total_pages_from_text("Showing 36 of 36") == 1
def test_partial_page(self):
assert parse_total_pages_from_text("Showing 36 of 37") == 2
def test_no_match(self):
assert parse_total_pages_from_text("no data") is None
def test_case_insensitive(self):
# shown=12 is in {12,24,36} so per_page=36, ceil(60/36)=2
assert parse_total_pages_from_text("showing 12 of 60") == 2
def test_small_shown(self):
# shown=10, not in {12,24,36}, so per_page=10
assert parse_total_pages_from_text("Showing 10 of 100") == 10
class TestFirstFromSrcset:
def test_single_entry(self):
assert _first_from_srcset("img.jpg 1x") == "img.jpg"
def test_multiple_entries(self):
assert _first_from_srcset("a.jpg 1x, b.jpg 2x") == "a.jpg"
def test_empty(self):
assert _first_from_srcset("") is None
def test_none(self):
assert _first_from_srcset(None) is None
class TestDedupePreserveOrder:
def test_no_dupes(self):
result = _dedupe_preserve_order_by(["a", "b", "c"], key=str)
assert result == ["a", "b", "c"]
def test_removes_dupes(self):
result = _dedupe_preserve_order_by(["a", "b", "a"], key=str)
assert result == ["a", "b"]
def test_empty_strings_skipped(self):
result = _dedupe_preserve_order_by(["a", "", "b"], key=str)
assert result == ["a", "b"]
def test_preserves_order(self):
result = _dedupe_preserve_order_by(["c", "b", "a", "b"], key=str)
assert result == ["c", "b", "a"]
class TestFilenameKey:
def test_basic(self):
assert _filename_key("https://img.com/photos/pic.jpg") == "img.com:pic.jpg"
def test_trailing_slash(self):
k = _filename_key("https://img.com/photos/")
assert k == "img.com:photos"
def test_case_insensitive(self):
k = _filename_key("https://IMG.COM/PIC.JPG")
assert k == "img.com:pic.jpg"

View File

@@ -0,0 +1,96 @@
"""Unit tests for price parsing utilities."""
from __future__ import annotations
import pytest
from market.scrape.product.helpers.price import parse_price, parse_case_size
class TestParsePrice:
def test_gbp(self):
val, cur, raw = parse_price("£30.50")
assert val == 30.5
assert cur == "GBP"
def test_eur(self):
val, cur, raw = parse_price("€1,234.00")
assert val == 1234.0
assert cur == "EUR"
def test_usd(self):
val, cur, raw = parse_price("$9.99")
assert val == 9.99
assert cur == "USD"
def test_no_symbol(self):
val, cur, raw = parse_price("42.50")
assert val == 42.5
assert cur is None
def test_no_match(self):
val, cur, raw = parse_price("no price here")
assert val is None
assert cur is None
def test_empty_string(self):
val, cur, raw = parse_price("")
assert val is None
assert raw == ""
def test_none_input(self):
val, cur, raw = parse_price(None)
assert val is None
assert raw == ""
def test_thousands_comma_stripped(self):
val, cur, raw = parse_price("£1,000.50")
assert val == 1000.5
def test_whitespace_around_symbol(self):
val, cur, raw = parse_price("£ 5.00")
assert val == 5.0
assert cur == "GBP"
def test_raw_preserved(self):
_, _, raw = parse_price(" £10.00 ")
assert raw == "£10.00"
class TestParseCaseSize:
def test_standard(self):
count, qty, unit, _ = parse_case_size("6 x 500g")
assert count == 6
assert qty == 500.0
assert unit == "g"
def test_no_space(self):
count, qty, unit, _ = parse_case_size("12x1L")
assert count == 12
assert qty == 1.0
assert unit == "L"
def test_multiplication_sign(self):
count, qty, unit, _ = parse_case_size("24 × 330 ml")
assert count == 24
assert qty == 330.0
assert unit == "ml"
def test_uppercase_x(self):
count, qty, unit, _ = parse_case_size("6X500g")
assert count == 6
def test_no_match(self):
count, qty, unit, raw = parse_case_size("just text")
assert count is None
assert qty is None
assert unit is None
def test_empty(self):
count, qty, unit, raw = parse_case_size("")
assert count is None
assert raw == ""
def test_none_input(self):
count, _, _, raw = parse_case_size(None)
assert count is None
assert raw == ""

View File

@@ -0,0 +1,58 @@
"""Unit tests for product registry utilities."""
from __future__ import annotations
import pytest
from market.scrape.product.registry import merge_missing
class TestMergeMissing:
def test_fills_empty_dict(self):
dst = {}
merge_missing(dst, {"a": 1, "b": "hello"})
assert dst == {"a": 1, "b": "hello"}
def test_existing_value_not_overwritten(self):
dst = {"a": "original"}
merge_missing(dst, {"a": "new"})
assert dst["a"] == "original"
def test_none_overwritten(self):
dst = {"a": None}
merge_missing(dst, {"a": "filled"})
assert dst["a"] == "filled"
def test_empty_string_overwritten(self):
dst = {"a": ""}
merge_missing(dst, {"a": "filled"})
assert dst["a"] == "filled"
def test_empty_list_overwritten(self):
dst = {"a": []}
merge_missing(dst, {"a": [1, 2]})
assert dst["a"] == [1, 2]
def test_empty_dict_overwritten(self):
dst = {"a": {}}
merge_missing(dst, {"a": {"key": "val"}})
assert dst["a"] == {"key": "val"}
def test_zero_not_overwritten(self):
dst = {"a": 0}
merge_missing(dst, {"a": 42})
assert dst["a"] == 0
def test_false_not_overwritten(self):
dst = {"a": False}
merge_missing(dst, {"a": True})
assert dst["a"] is False
def test_none_src(self):
dst = {"a": 1}
merge_missing(dst, None)
assert dst == {"a": 1}
def test_new_keys_added(self):
dst = {"a": 1}
merge_missing(dst, {"b": 2})
assert dst == {"a": 1, "b": 2}

View File

@@ -0,0 +1,45 @@
"""Unit tests for market slug helpers."""
from __future__ import annotations
import pytest
from market.bp.browse.services.slugs import (
product_slug_from_href, canonical_html_slug,
)
class TestProductSlugFromHref:
def test_html_extension(self):
result = product_slug_from_href("https://site.com/foo/bar-thing.html")
assert result == "bar-thing-html"
def test_htm_extension(self):
result = product_slug_from_href("https://site.com/products/widget.htm")
assert result == "widget-html"
def test_no_extension(self):
result = product_slug_from_href("https://site.com/item/cool-product")
assert result == "cool-product-html"
def test_empty_path(self):
result = product_slug_from_href("https://site.com/")
assert result == ""
def test_already_has_html_suffix_in_slug(self):
result = product_slug_from_href("https://site.com/prod/item-html.html")
assert result == "item-html"
class TestCanonicalHtmlSlug:
def test_adds_html_suffix(self):
assert canonical_html_slug("product-name") == "product-name-html"
def test_idempotent(self):
assert canonical_html_slug("product-name-html") == "product-name-html"
def test_double_html_kept(self):
# canonical_html_slug only appends -html if not already present
assert canonical_html_slug("product-name-html-html") == "product-name-html-html"
def test_strips_htm(self):
assert canonical_html_slug("product-name-htm") == "product-name-html"