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:
Eric Wagoner
2026-08-05 22:53:05 -04:00
co-authored by Claude Fable 5
parent 6f2f2dac53
commit 6e72ef22d1
4 changed files with 79 additions and 32 deletions
+24 -11
View File
@@ -382,6 +382,7 @@ class PlaywrightUploader:
# 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"]'
_PAGE_LINKS = "ul.pagination a"
# 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".
@@ -428,6 +429,8 @@ class PlaywrightUploader:
# BGG's API name can carry a printing qualifier the picker omits
# ("English edition 2018-2" vs "(English edition) (2018)")
relaxed = f"({self._TRAILING_YEAR.sub('', version_name).strip()})".casefold()
self._goto_first_page(dialog) # a fresh sub-view opens on page 1,
# but never assume it: paging state outlives a close/reopen
exact: list[tuple[int, str]] = []
loose: list[tuple[int, str]] = []
pages = 0
@@ -472,18 +475,28 @@ class PlaywrightUploader:
f"({', '.join(t for _, t in hits[:3])}…) — refusing to guess"
)
def _click_row(self, dialog, text: str) -> bool:
"""Reopen the sub-view (it always starts on page 1) and hunt for the
row again by its NORMALIZED text, clicking by index.
def _goto_first_page(self, dialog) -> None:
"""Back to page 1 via the numbered "1" anchor. Closing and
reopening the sub-view does NOT reset Angular's paging state (it
reopens wherever it was left), and First/Prev render in a
mobile-only variant that a desktop viewport can never click."""
pager = dialog.locator(self._PAGE_LINKS)
for i in range(pager.count()):
anchor = pager.nth(i)
if anchor.is_visible() and (
" ".join((anchor.text_content() or "").split()) == "1"
):
anchor.click()
self._page.wait_for_timeout(400)
return
# no pagination rendered: a single-page list is already page 1
Two traps this avoids: First/Prev exist only in the mobile variant
(unclickable on a desktop viewport), and Playwright's has_text
regex matches raw textContent — whose tabs and newlines a
whitespace-normalized capture will never equal."""
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)
def _click_row(self, dialog, text: str) -> bool:
"""Find the row again by its NORMALIZED text and click it by index.
Playwright's has_text regex matches raw textContent — whose tabs and
newlines a normalized capture will never equal — so the comparison
happens in Python, and the click addresses a position."""
self._goto_first_page(dialog)
for _ in range(MAX_VERSION_PAGES):
for i, row_text in enumerate(self._rows_on_page(dialog)):
if row_text == text: