The picker's paging state outlives a reopen — navigate, don't assume
Sleeping Gods and Gloomhaven "vanished on the second pass" because the second pass began wherever the first ended: closing and reopening the version sub-view does NOT reset it to page 1 (Angular keeps the scope), so the rescan started mid-list and never revisited the earlier pages holding the row. Verified against the live picker. The second pass now clicks the visible numbered "1" anchor first — and so does the initial scan, since paging state can outlive anything. The reopen is gone entirely. Docs record both this and the has_text whitespace trap. 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
6f2f2dac53
commit
6e72ef22d1
+41
-18
@@ -551,8 +551,9 @@ def test_undeterminable_state_raises_instead_of_guessing(monkeypatch):
|
||||
|
||||
|
||||
class _FakePicker:
|
||||
"""Minimal stand-in for the picker sub-view: pages of row texts, a
|
||||
Next anchor that disables on the last page."""
|
||||
"""Stand-in for the picker sub-view. Models the two traps the real one
|
||||
sets: paging controls render twice (a hidden mobile duplicate first),
|
||||
and paging state SURVIVES closing and reopening the sub-view."""
|
||||
|
||||
def __init__(self, pages):
|
||||
self.pages = pages
|
||||
@@ -562,9 +563,12 @@ class _FakePicker:
|
||||
self.cancelled = False
|
||||
self.reopened = 0
|
||||
|
||||
# -- locator plumbing the uploader uses ---------------------------
|
||||
def locator(self, selector):
|
||||
picker = self
|
||||
is_next = 'title="Next Page"' in selector
|
||||
is_pager = "pagination" in selector
|
||||
# anchors: [hidden mobile "1", "1", "2", ...] mirroring the real DOM
|
||||
anchors = ["1"] + [str(n + 1) for n in range(len(picker.pages))]
|
||||
|
||||
class _Loc:
|
||||
def __init__(self, index=0):
|
||||
@@ -574,22 +578,25 @@ class _FakePicker:
|
||||
return picker.pages[picker.page]
|
||||
|
||||
def count(self):
|
||||
if "pagination" in selector:
|
||||
return 2 # BGG renders a mobile AND a desktop control
|
||||
if is_next:
|
||||
return 2 # hidden mobile + visible desktop
|
||||
if is_pager:
|
||||
return len(anchors)
|
||||
return len(picker.pages[picker.page])
|
||||
|
||||
def nth(self, i):
|
||||
return _Loc(i)
|
||||
|
||||
def is_visible(self):
|
||||
# among PAGING controls, index 0 is the mobile
|
||||
# (visible-xs-*) duplicate — present but not clickable
|
||||
return "pagination" not in selector or self.index != 0
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
return _Loc(0)
|
||||
|
||||
def is_visible(self):
|
||||
return not (is_pager and self.index == 0)
|
||||
|
||||
def text_content(self):
|
||||
return anchors[self.index] if is_pager and not is_next else ""
|
||||
|
||||
def wait_for(self, **kw):
|
||||
return None
|
||||
|
||||
@@ -598,16 +605,17 @@ class _FakePicker:
|
||||
return "ng-scope disabled" if last else "ng-scope"
|
||||
|
||||
def click(self):
|
||||
if "pagination" in selector and not self.is_visible():
|
||||
if is_pager and not self.is_visible():
|
||||
raise AssertionError("clicked a HIDDEN paging control")
|
||||
if "Next Page" in selector:
|
||||
if is_next:
|
||||
picker.page += 1
|
||||
elif "summary-item-thumbnail" in selector:
|
||||
elif is_pager:
|
||||
picker.page = int(anchors[self.index]) - 1
|
||||
else:
|
||||
picker.clicked_index = self.index
|
||||
picker.clicked = picker.pages[picker.page][self.index]
|
||||
|
||||
def filter(self, has_text=None):
|
||||
picker.clicked = has_text.pattern if has_text else None
|
||||
return self
|
||||
|
||||
return _Loc()
|
||||
@@ -624,8 +632,7 @@ class _FakePicker:
|
||||
if name == "Cancel":
|
||||
picker.cancelled = True
|
||||
elif name == "Set version/edition":
|
||||
picker.page = 0 # the sub-view reopens on page 1
|
||||
picker.reopened += 1
|
||||
picker.reopened += 1 # note: does NOT reset the page
|
||||
|
||||
return _Btn()
|
||||
|
||||
@@ -697,8 +704,7 @@ def test_paging_never_clicks_a_hidden_mobile_control():
|
||||
)
|
||||
picked, _ = up._select_version(dialog, "English edition 2009")
|
||||
assert picked is True # no AssertionError from the fake = no hidden click
|
||||
# the initial open plus ONE reopen — never a "First Page" click
|
||||
assert dialog.reopened == 2
|
||||
assert dialog.clicked_index == 0 # found on the later page, clicked
|
||||
|
||||
|
||||
def test_outstanding_failures_ignores_retried_jobs():
|
||||
@@ -731,3 +737,20 @@ def test_outstanding_failures_ignores_retried_jobs():
|
||||
]
|
||||
assert outstanding_failures(rows) == 1 # game 1 was retried and landed
|
||||
assert outstanding_failures([]) == 0
|
||||
|
||||
|
||||
def test_paging_state_survives_reopen_so_page_one_is_clicked():
|
||||
"""The real sub-view reopens wherever it was left, so the second pass
|
||||
must navigate back to page 1 explicitly — Sleeping Gods and Gloomhaven
|
||||
both "vanished" when it didn't."""
|
||||
up, dialog = _picker_uploader(
|
||||
[
|
||||
["Sleeping Gods (English Gamefound edition) (2023)"],
|
||||
["Sleeping Gods (German edition) (2022)"],
|
||||
]
|
||||
)
|
||||
dialog.page = 1 # left on the last page by a previous scan
|
||||
picked, why = up._select_version(dialog, "English Gamefound edition")
|
||||
assert picked is True
|
||||
assert why == ""
|
||||
assert dialog.clicked.startswith("Sleeping Gods (English Gamefound")
|
||||
|
||||
Reference in New Issue
Block a user