First real upload lands: three browser-flow bugs, found by doing it

A Gentle Rain is on BGG (collid 148198034, version 701315 — the
English Bloom edition matched from a shelf photo), verified by
re-fetching the collection. The failures on the way, all in the
login gate the docs called "verified" and none in the version-picker
code they called "unverified":

1. BGG's Sign In is an <a class="btn"> with NO href, so it has no
   implicit link role: get_by_role("link", name="Sign In") matched
   zero elements in EVERY state, and "no Sign In link" was read as
   "already signed in". Every run browsed anonymously.
2. The header hydrates after domcontentloaded, so for a moment
   neither control exists — a check resting on one absence guesses.
   _signed_out() now polls until the page proves one state or the
   other (Sign In vs Sign Out) and raises after 30s rather than
   assume; login is verified by the transition, and the session file
   saves only after that proof.
3. get_by_label("Own") also matched "Prev. Owned" — strict-mode
   violation; the Own checkbox is now matched exactly.

Also: Playwright's multi-line call logs no longer break upload_log.csv
into ragged rows (errors flatten to one line). Three unit tests cover
the hydration window, positive detection, and the undeterminable case.

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:22:11 -04:00
co-authored by Claude Fable 5
parent 481fa63488
commit 6ae2667c58
6 changed files with 206 additions and 24 deletions
+54
View File
@@ -491,3 +491,57 @@ def test_drift_warning_suppressed_for_multi_edition_partial_run(tmp_path, capsys
)
run_upload(cfg, uploader=FakeUploader(), sleep=lambda s: None, now=_now)
assert "manual correction" not in capsys.readouterr().out
# -- sign-in detection: never guess from a single absence ----------------
class _FakePage:
"""Counts per selector, replayed one poll at a time — the header
hydrates late, so the first reads legitimately see nothing."""
def __init__(self, script):
self.script = list(script)
self.polls = 0
def locator(self, selector):
counts = self.script[min(self.polls, len(self.script) - 1)]
class _Loc:
def count(self):
return counts["out" if "Sign In" in selector else "in"]
return _Loc()
def wait_for_timeout(self, ms):
self.polls += 1
def _uploader_with(script):
from bggpipe.upload import PlaywrightUploader
up = PlaywrightUploader("someone")
up._page = _FakePage(script)
return up
def test_signed_out_waits_through_hydration():
# neither control for two polls (the window the old check misread as
# "already signed in"), then the Sign In control appears
up = _uploader_with([{"out": 0, "in": 0}, {"out": 0, "in": 0}, {"out": 1, "in": 0}])
assert up._signed_out() is True
def test_signed_in_is_detected_positively():
up = _uploader_with([{"out": 0, "in": 0}, {"out": 0, "in": 1}])
assert up._signed_out() is False
def test_undeterminable_state_raises_instead_of_guessing(monkeypatch):
from bggpipe.upload import LoginError
ticks = iter([0, 1, 2, 31]) # force the deadline to pass
monkeypatch.setattr("time.monotonic", lambda: next(ticks))
up = _uploader_with([{"out": 0, "in": 0}])
with pytest.raises(LoginError, match="could not tell"):
up._signed_out()