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:
co-authored by
Claude Fable 5
parent
6ae2667c58
commit
ba55863cef
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user