add a selector self check and stop reporting skipped tasks as done

check_selectors.py walks every selector and prints what resolved, what is absent and what broke, along with browser, driver, page language and the earn section ids. Absent is a normal result for a task a variant does not ship. It completes no activities and claims nothing, so it is safe to run for a bug report.

complete_explore_on_bing_tasks now raises when the section is missing instead of returning quietly, which made complete_all_tasks print [OK] for a task that never ran.

The visible labels the lookups match on are collected in one Labels class. The selectors are market independent but still language dependent, and this makes that explicit and fixable in one place.
This commit is contained in:
mardausdennis
2026-08-25 14:14:04 +02:00
parent 2773018161
commit 3dba5aa849
3 changed files with 248 additions and 9 deletions
+217
View File
@@ -0,0 +1,217 @@
"""Report which selectors resolve against the Rewards UI you actually get.
The Rewards markup differs between markets and changes between deploys, so a
selector that works for one account silently finds nothing for another. This
walks every selector and prints what resolved, what is absent, and what broke.
It completes no activities and claims no points. It only reads, and opens the
points breakdown and daily set panels, which award nothing.
poetry run python src/check_selectors.py
Paste the output into a bug report. Absent is a normal result for a task the
variant does not ship. FAILED is what needs fixing.
"""
import sys
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
import element_selectors
from constants import USER_DATA_DIR, PROFILE_NAME
RENDER_TIMEOUT = 60
def build_driver():
options = webdriver.EdgeOptions()
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(f"--user-data-dir={USER_DATA_DIR}")
options.add_argument(f"--profile-directory={PROFILE_NAME}")
return webdriver.Edge(options=options)
def wait_until(predicate, timeout=RENDER_TIMEOUT):
deadline = time.time() + timeout
while time.time() < deadline:
try:
if predicate():
return True
except Exception:
pass
time.sleep(2)
return False
class Report:
def __init__(self):
self.rows = []
def record(self, name, status, detail=""):
self.rows.append((name, status, detail))
print(f" {status:<8} {name:<44} {detail}")
def check(self, name, fn, optional=False):
try:
value = fn()
except Exception as exc:
self.record(name, "ABSENT" if optional else "FAILED", type(exc).__name__)
return None
if isinstance(value, list):
detail = f"{len(value)} element(s)"
if not value and optional:
self.record(name, "ABSENT", "0 elements")
return value
elif isinstance(value, tuple):
detail = str(value)
else:
try:
detail = repr((value.text or "").replace("\n", " | ")[:44])
except Exception:
detail = "<element>"
self.record(name, "OK", detail)
return value
def summary(self):
counts = {"OK": 0, "ABSENT": 0, "FAILED": 0}
for _, status, _ in self.rows:
counts[status] = counts.get(status, 0) + 1
print(f"\nOK={counts['OK']} ABSENT={counts['ABSENT']} FAILED={counts['FAILED']}")
return counts["FAILED"]
def describe_environment(driver, report):
print("\n## environment")
caps = driver.capabilities
print(f" browser {caps.get('browserVersion')}")
print(f" msedgedriver {caps.get('msedge', {}).get('msedgedriverVersion', '?').split(' ')[0]}")
print(f" selenium {getattr(__import__('selenium'), '__version__', '?')}")
print(f" python {sys.version.split()[0]}")
print(f" page lang {driver.find_element(By.TAG_NAME, 'html').get_attribute('lang')!r}")
print(f" viewport {driver.execute_script('return [window.innerWidth, window.innerHeight];')}")
sections = [
s.get_dom_attribute("id")
for s in driver.find_elements(By.XPATH, "/html/body/div[2]/div[2]/div/main/section")
]
print(f" earn sections {sections}")
duplicates = [i for i in set(sections) if i and sections.count(i) > 1]
if duplicates:
print(f" duplicated ids {duplicates}")
def main():
driver = build_driver()
elements = element_selectors.ElementSelectionUtils(driver)
report = Report()
try:
driver.get("https://rewards.bing.com/earn")
rendered = wait_until(lambda: elements.get_points_breakdown_button() is not None)
if not rendered:
print("The earn page never finished rendering.")
print("In the EU the cookie consent banner blocks it until answered, and it")
print("cannot be dismissed reliably from selenium. Open the profile in a")
print("normal Edge window, answer the banner once, then run this again.")
return 2
describe_environment(driver, report)
print("\n## navigation")
report.check("get_earn_tab", elements.get_earn_tab)
report.check("get_dashboard_tab", elements.get_dashboard_tab)
report.check("get_points_breakdown_button", elements.get_points_breakdown_button)
print("\n## daily set")
opener = report.check("get_open_daily_set_button", elements.get_open_daily_set_button)
if opener is not None:
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", opener)
time.sleep(1)
driver.execute_script("arguments[0].click();", opener)
wait_until(lambda: elements.get_sidebar_section() is not None, 30)
report.check("get_daily_set_elements", elements.get_daily_set_elements)
try:
driver.execute_script(
"arguments[0].click();", elements.get_generic_sidebar_close_button()
)
time.sleep(2)
except Exception:
driver.get("https://rewards.bing.com/earn")
wait_until(lambda: elements.get_points_breakdown_button() is not None)
print("\n## optional tasks")
report.check("get_explore_on_bing_elements", elements.get_explore_on_bing_elements, optional=True)
report.check("get_open_visual_search_sidebar", elements.get_open_visual_search_sidebar, optional=True)
print("\n## cards")
cards = report.check("get_all_misc_cards", elements.get_all_misc_cards)
if cards:
for index, card in enumerate(cards, start=1):
try:
points = elements.get_card_point_value(card)
done = elements.card_is_complete(card)
description = elements.extract_card_descriptions(card)[:38]
print(f" card[{index}] points={points:<4} completed={done!s:<5} {description!r}")
except Exception as exc:
print(f" card[{index}] unreadable: {type(exc).__name__}")
print("\n## points breakdown")
driver.get("https://rewards.bing.com/earn")
wait_until(lambda: elements.get_points_breakdown_button() is not None)
driver.execute_script("arguments[0].click();", elements.get_points_breakdown_button())
wait_until(lambda: elements.get_sidebar_section() is not None, 30)
report.check("get_sidebar_section", elements.get_sidebar_section)
report.check(
"get_points_earned_from_searches_on_points_breakdown",
elements.get_points_earned_from_searches_on_points_breakdown,
)
report.check("get_close_button_on_points_breakdown", elements.get_close_button_on_points_breakdown)
print("\n## bonus")
driver.get("https://rewards.bing.com/dashboard")
wait_until(lambda: elements.get_bonus_button_on_dashboard() is not None, 30)
report.check("get_bonus_button_on_dashboard", elements.get_bonus_button_on_dashboard, optional=True)
print("\n## bing")
driver.get("https://www.bing.com/")
wait_until(lambda: bool(driver.find_elements(By.TAG_NAME, "textarea")), 30)
report.check("get_bing_search_bar", elements.get_bing_search_bar)
failures = report.summary()
if failures:
print("\nFAILED entries are selectors that should have resolved on this page.")
else:
print("\nEvery selector that this variant ships resolved.")
return 1 if failures else 0
finally:
driver.quit()
if __name__ == "__main__":
sys.exit(main())
+26 -7
View File
@@ -6,6 +6,25 @@ from selenium.common.exceptions import NoSuchElementException, StaleElementRefer
from selenium import webdriver
class Labels:
"""Visible labels the selectors match on.
The Rewards markup carries no stable hooks for these controls, so they have
to be found by their text. That makes the lookups language dependent even
though they are market independent: a Rewards UI rendered in another
language needs these translated, and there is exactly one place to do it.
Matching is case insensitive and by substring unless noted.
"""
POINTS_BREAKDOWN = "points breakdown"
READY_TO_CLAIM = "ready to claim"
CLAIM = "claim" # exact label preferred, substring as fallback
DAILY_SET_STREAK = "daily set streak"
CARD_COMPLETED = "completed"
VISUAL_SEARCH = ("visual search", "image search")
class ElementSelectionUtils:
"""Selectors for the Rewards UI.
@@ -131,7 +150,7 @@ class ElementSelectionUtils:
# "daily set streak" rather than "daily set", because the level up
# section also has "Complete the Daily Set for 7 days in a row".
try:
return self._button_containing("daily set streak")
return self._button_containing(Labels.DAILY_SET_STREAK)
except NoSuchElementException:
return self._streaks_button(3)
@@ -156,7 +175,7 @@ class ElementSelectionUtils:
# ------------------------------------------------------------------
def get_open_visual_search_sidebar(self):
for needle in ("visual search", "image search"):
for needle in Labels.VISUAL_SEARCH:
try:
return self._button_containing(needle)
except NoSuchElementException:
@@ -210,7 +229,7 @@ class ElementSelectionUtils:
except NoSuchElementException:
return False
return "completed" in (status or "").lower()
return Labels.CARD_COMPLETED in (status or "").lower()
def get_card_point_value(self, card: WebElement):
try:
@@ -244,7 +263,7 @@ return (
# ------------------------------------------------------------------
def get_points_breakdown_button(self):
return self._button_containing("points breakdown")
return self._button_containing(Labels.POINTS_BREAKDOWN)
def get_close_button_on_points_breakdown(self):
return self.get_generic_sidebar_close_button()
@@ -290,7 +309,7 @@ return (
# ------------------------------------------------------------------
def get_bonus_button_on_dashboard(self):
return self._button_containing("ready to claim")
return self._button_containing(Labels.READY_TO_CLAIM)
def get_claim_bonus_points_button(self):
sidebar = self.get_sidebar_section()
@@ -300,14 +319,14 @@ return (
# would hit the "Ready to claim" heading before the actual Claim button.
for button in buttons:
try:
if (button.text or "").strip().lower() == "claim":
if (button.text or "").strip().lower() == Labels.CLAIM:
return button
except StaleElementReferenceException:
continue
for button in buttons:
try:
if "claim" in (button.text or "").lower():
if Labels.CLAIM in (button.text or "").lower():
return button
except StaleElementReferenceException:
continue
+5 -2
View File
@@ -85,8 +85,11 @@ class RewardsTaskUtils:
explore_on_bing_links = self.elements.get_explore_on_bing_elements()
if not explore_on_bing_links:
print("[INFO] No Explore on Bing section in this UI variant, skipping.")
return
# Raise rather than return, so complete_all_tasks reports this as
# [SKIP]. Returning quietly made it print [OK] for a task that never
# ran, which is exactly the kind of false success a scheduled run
# must not produce.
raise NoSuchElementException("no Explore on Bing section in this UI variant")
for card in explore_on_bing_links:
desc = self.elements.extract_card_descriptions(card)