Paging clicks only visible controls: First/Prev are mobile-only
Munchkin Big Box and Tang Garden hung 30s each on a "First Page" anchor that exists but is invisible: BGG renders every paging control twice, and the First/Prev pair lives only in the mobile set (<li class="visible-xs-inline">). A desktop viewport can never click it. Paging now selects the first VISIBLE match, and returning to page 1 closes and reopens the sub-view (which always opens on page 1) instead of reaching for a control that isn't there. A test proves no hidden control is ever clicked — the fake picker raises if one is. 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
ba55863cef
commit
9283a3e980
+24
-14
@@ -368,8 +368,10 @@ class PlaywrightUploader:
|
||||
# an AngularJS <ul class="pagination"> of anchors — NOT buttons named
|
||||
# "next", which is why the old guess quit after page one.
|
||||
_VERSION_ROWS = "li:has(.summary-item-thumbnail)"
|
||||
# Paging controls render TWICE — a desktop set and a mobile set marked
|
||||
# visible-xs-* — so every one of these selectors matches hidden nodes
|
||||
# too; clicking one waits forever. Always pick the visible match.
|
||||
_NEXT_PAGE = 'ul.pagination a[title="Next Page"]'
|
||||
_FIRST_PAGE = 'ul.pagination a[title="First Page"]'
|
||||
# Rows read "<game name> (<version name>) (<year>)", so the version name
|
||||
# is matched inside its parentheses — bare substrings would let
|
||||
# "English edition" match "English edition, second printing".
|
||||
@@ -381,13 +383,20 @@ class PlaywrightUploader:
|
||||
for t in dialog.locator(self._VERSION_ROWS).all_text_contents()
|
||||
]
|
||||
|
||||
def _visible(self, dialog, selector):
|
||||
"""The first VISIBLE match, or None — see _NEXT_PAGE."""
|
||||
loc = dialog.locator(selector)
|
||||
for i in range(loc.count()):
|
||||
candidate = loc.nth(i)
|
||||
if candidate.is_visible():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
def _has_next_page(self, dialog) -> bool:
|
||||
nxt = dialog.locator(self._NEXT_PAGE)
|
||||
if nxt.count() == 0:
|
||||
nxt = self._visible(dialog, self._NEXT_PAGE)
|
||||
if nxt is None:
|
||||
return False # single-page list: the container is ng-show'd off
|
||||
return "disabled" not in (
|
||||
nxt.first.evaluate("e => e.closest('li').className") or ""
|
||||
)
|
||||
return "disabled" not in (nxt.evaluate("e => e.closest('li').className") or "")
|
||||
|
||||
def _select_version(self, dialog, version_name: str) -> tuple[bool, str]:
|
||||
"""Pick the version whose name matches, paging the whole list first
|
||||
@@ -422,7 +431,7 @@ class PlaywrightUploader:
|
||||
loose.append((page_ix, text))
|
||||
if not self._has_next_page(dialog):
|
||||
break
|
||||
dialog.locator(self._NEXT_PAGE).first.click()
|
||||
self._visible(dialog, self._NEXT_PAGE).click()
|
||||
self._page.wait_for_timeout(400) # client-side paging: no request
|
||||
else:
|
||||
# never saw the end of the list: "not in picker" would be a false
|
||||
@@ -453,14 +462,15 @@ class PlaywrightUploader:
|
||||
)
|
||||
|
||||
def _goto_picker_page(self, dialog, page_ix: int) -> None:
|
||||
"""Back to page 1, then forward — the numbered anchors render twice
|
||||
(mobile + desktop variants), so stepping is the unambiguous route."""
|
||||
first = dialog.locator(self._FIRST_PAGE)
|
||||
if first.count():
|
||||
first.first.click()
|
||||
self._page.wait_for_timeout(400)
|
||||
"""Reopen the sub-view (it always starts on page 1) and step
|
||||
forward. The First/Prev anchors exist only in the mobile variant,
|
||||
so they can never be clicked on a desktop viewport."""
|
||||
dialog.get_by_role("button", name="Cancel").first.click()
|
||||
self._page.wait_for_timeout(300)
|
||||
dialog.get_by_role("button", name="Set version/edition").click()
|
||||
dialog.locator(self._VERSION_ROWS).first.wait_for(timeout=15_000)
|
||||
for _ in range(page_ix):
|
||||
dialog.locator(self._NEXT_PAGE).first.click()
|
||||
self._visible(dialog, self._NEXT_PAGE).click()
|
||||
self._page.wait_for_timeout(400)
|
||||
|
||||
def add_game(self, job: UploadJob) -> tuple[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user