Version picker: real pagination, and matching that declines to guess

Five games uploaded; two landed version-less. Neither was the picker's
fault: paging is an AngularJS <ul class="pagination"> of anchors, not
buttons named "next", so the old guess found no control and quit after
page one — and BGG's API version names carry printing qualifiers the
picker omits ("English edition 2018-2" vs "(English edition) (2018)").

_select_version now scans the WHOLE list (verified selectors: rows are
<li>s with a thumbnail; a[title="Next Page"] advances; the parent <li>
disables at the end), collects every candidate, then decides: one exact
match wins; failing that, one match after stripping a trailing year
qualifier wins and says so; several matches are refused outright rather
than guessed, and the reason reaches upload_log.csv. Both call sites
carry the reason through.

docs/bgg-upload-flow.md records what the live site actually does —
including that every login-gate selector the doc called "verified" was
wrong, while the "unverified" dialog structure was right.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-05 22:36:48 -04:00
co-authored by Claude Fable 5
parent 6ae2667c58
commit ba55863cef
4 changed files with 253 additions and 24 deletions
+122
View File
@@ -545,3 +545,125 @@ def test_undeterminable_state_raises_instead_of_guessing(monkeypatch):
up = _uploader_with([{"out": 0, "in": 0}])
with pytest.raises(LoginError, match="could not tell"):
up._signed_out()
# -- version picker: pagination + never-guess matching -------------------
class _FakePicker:
"""Minimal stand-in for the picker sub-view: pages of row texts, a
Next anchor that disables on the last page."""
def __init__(self, pages):
self.pages = pages
self.page = 0
self.clicked = None
self.cancelled = False
# -- locator plumbing the uploader uses ---------------------------
def locator(self, selector):
picker = self
class _Loc:
def __init__(self, texts=None):
self.texts = texts
def all_text_contents(self):
return picker.pages[picker.page]
def count(self):
if "pagination" in selector:
return 1
return len(picker.pages[picker.page])
@property
def first(self):
return self
def wait_for(self, **kw):
return None
def evaluate(self, _js):
last = picker.page >= len(picker.pages) - 1
return "ng-scope disabled" if last else "ng-scope"
def click(self):
if "Next Page" in selector:
picker.page += 1
elif "First Page" in selector:
picker.page = 0
def filter(self, has_text=None):
picker.clicked = has_text.pattern if has_text else None
return self
return _Loc()
def get_by_role(self, role, name=None, **kw):
picker = self
class _Btn:
@property
def first(self):
return self
def click(self):
if name == "Cancel":
picker.cancelled = True
return _Btn()
def _picker_uploader(pages):
from bggpipe.upload import PlaywrightUploader
up = PlaywrightUploader("someone")
class _Page:
def wait_for_timeout(self, ms):
pass
up._page = _Page()
return up, _FakePicker(pages)
def test_version_found_on_a_later_page():
up, dialog = _picker_uploader(
[
["Sleeping Gods (Czech edition) (2023)"],
["Sleeping Gods (English Gamefound edition) (2023)"],
]
)
picked, why = up._select_version(dialog, "English Gamefound edition")
assert picked is True # the old code gave up after page one
assert "Gamefound" in dialog.clicked # regex-escaped row text
assert why == ""
def test_api_name_with_printing_qualifier_matches_loosely():
up, dialog = _picker_uploader([["Mysterium (English edition) (2018)"]])
picked, why = up._select_version(dialog, "English edition 2018-2")
assert picked is True
assert "matched loosely" in why
def test_ambiguous_versions_are_refused_not_guessed():
up, dialog = _picker_uploader(
[
[
"Munchkin (English edition) (2009)",
"Munchkin (English edition) (2015)",
]
]
)
picked, why = up._select_version(dialog, "English edition 2009")
assert picked is False
assert "refusing to guess" in why
assert dialog.cancelled is True
def test_absent_version_reports_pages_scanned():
up, dialog = _picker_uploader([["Catan (German edition) (2015)"]])
picked, why = up._select_version(dialog, "English edition")
assert picked is False
assert "not offered" in why and "1 page(s)" in why