From f23afb756876e6fb21fefde831ffc6b53b36381c Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:00:44 +0200 Subject: [PATCH 1/4] match daily set activities by target instead of by position get_daily_set_elements returned everything after the first link in the panel. The panel also carries promotional links, so position hands one back as an activity. Clicking it leaves rewards.bing.com and every element captured before that goes stale, which is the exception reported in #45. Activities always point at a Bing search, so match on that. When nothing matches it returns nothing rather than falling back to position, since clicking a promo is worse than skipping the task and complete_bing_daily_set already reports the shortfall. --- src/element_selectors.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/element_selectors.py b/src/element_selectors.py index 0dc565c..6708372 100644 --- a/src/element_selectors.py +++ b/src/element_selectors.py @@ -157,8 +157,28 @@ class ElementSelectionUtils: return self._streaks_button(3) def get_daily_set_elements(self): - # The first link in the opened panel is the progress row, not an activity. - return self.get_sidebar_section().find_elements(By.TAG_NAME, "a")[1:] + """The daily set activities in the opened panel. + + Everything after the first link is not reliably an activity. The panel + also carries promotional links, a referral card and a Bing app promo have + both been observed sitting between the progress row and the activities. + Handing one of those back gets it clicked, which navigates away from + rewards.bing.com, and every element captured beforehand then goes stale. + + Activities always point at a Bing search, so match on that rather than on + position. If nothing matches, return nothing: clicking a promo is worse + than skipping the task, and the caller already reports the shortfall. + """ + activities = [] + + for link in self.get_sidebar_section().find_elements(By.TAG_NAME, "a"): + try: + if "bing.com/search" in (link.get_dom_attribute("href") or ""): + activities.append(link) + except StaleElementReferenceException: + continue + + return activities # ------------------------------------------------------------------ # explore on bing (absent in en-US, present in some other markets) From 238050ba9abd92cffa8254714d23e3ab96a8c082 Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:44:01 +0200 Subject: [PATCH 2/4] wait for the daily set panel to fill before checking it The panel hydrates progressively, so waiting for the section only tells you it opened, not that it filled. A check running on the first non-empty state reports whatever happened to be rendered at that moment, which is why the daily set came out differently run to run. Same wait as complete_bing_daily_set: hold out for the full set, report what is there if it never arrives. --- src/check_selectors.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/check_selectors.py b/src/check_selectors.py index 218b2be..03d29c9 100644 --- a/src/check_selectors.py +++ b/src/check_selectors.py @@ -24,6 +24,9 @@ from constants import USER_DATA_DIR, PROFILE_NAME RENDER_TIMEOUT = 60 +# How many activities a fully rendered daily set panel holds. +DAILY_SET_ACTIVITIES = 3 + def build_driver(): options = webdriver.EdgeOptions() @@ -149,7 +152,15 @@ def main(): 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) + + # Waiting for the section only tells you the panel opened, not that + # it filled. It hydrates progressively, so a check that runs on the + # first non-empty state reports whatever happened to be rendered at + # that moment, which is why this came out differently run to run. + # Same wait as complete_bing_daily_set: hold out for the full set, + # and report what is there if it never arrives. + wait_until(lambda: len(elements.get_daily_set_elements()) >= DAILY_SET_ACTIVITIES, 30) + report.check("get_daily_set_elements", elements.get_daily_set_elements) try: From 806156c6c6922f48cdcf33edc160046ceff091c4 Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:46:47 +0200 Subject: [PATCH 3/4] do not click a different streak when the daily set label is missing The positional fallback returned whatever sat at index 3 of the streaks section. On a partially rendered page that is not the daily set: observed live returning 'Mobile App | Check-in: 0/1', and clicking it opens the app store page instead of the panel, which is what #45 and #46 describe. It now checks the label before handing the button back and raises otherwise, so the task is skipped rather than the wrong streak clicked. --- src/element_selectors.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/element_selectors.py b/src/element_selectors.py index 6708372..24d3381 100644 --- a/src/element_selectors.py +++ b/src/element_selectors.py @@ -154,7 +154,24 @@ class ElementSelectionUtils: try: return self._button_containing(Labels.DAILY_SET_STREAK) except NoSuchElementException: - return self._streaks_button(3) + pass + + # The positional fallback only helps if what sits there really is the + # daily set entry. On a partially rendered streaks section it is not: + # observed returning the mobile app entry, and clicking that opens the + # app store page instead of the panel, which is what the reports in #45 + # and #46 describe. Check before handing it back, and skip the task + # rather than click the wrong streak. + candidate = self._streaks_button(3) + label = (candidate.text or "").strip() + + if "daily set" not in label.lower(): + raise NoSuchElementException( + "daily set opener not found by label, and position 3 holds " + f"{label.splitlines()[0] if label else ''!r} instead" + ) + + return candidate def get_daily_set_elements(self): """The daily set activities in the opened panel. From 8ff99c0e9d4404042f58e37ddc0264ab47c37814 Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:14:19 +0200 Subject: [PATCH 4/4] cover the rewards urls the daily set actually uses Matching on bing.com/search alone was too narrow. Turn referrals into rewards is a real daily set activity that awards points and it points at a rewards url, so the filter skipped it and the run came up short without saying why. Three shapes now count, bing.com/search, bing.com/rewards and rewards.bing.com. The bing app promo from #45 sits on bingapp.microsoft.com and stays out of all three, which is what keeps that fix intact. Five tests, one per shape plus a mixed panel. --- src/element_selectors.py | 26 +++++++++++++--- tests/test_element_selectors.py | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/element_selectors.py b/src/element_selectors.py index 24d3381..0108590 100644 --- a/src/element_selectors.py +++ b/src/element_selectors.py @@ -182,21 +182,39 @@ class ElementSelectionUtils: Handing one of those back gets it clicked, which navigates away from rewards.bing.com, and every element captured beforehand then goes stale. - Activities always point at a Bing search, so match on that rather than on - position. If nothing matches, return nothing: clicking a promo is worse - than skipping the task, and the caller already reports the shortfall. + Matching on a Bing search alone was too narrow. "Turn referrals into + rewards" is a real daily set activity that awards points, and it points + at a rewards URL rather than a search. Three shapes have been observed: + + 1. `bing.com/search?q=...`, the classic search activity, + 2. `bing.com/rewards/...`, seen on daily sets alongside the searches, + 3. `rewards.bing.com/...`, the same activity written against the + rewards host. + + The Bing app promo behind #45 is on `bingapp.microsoft.com`, so it stays + out of all three, and so does anything else off those hosts. If nothing + matches, return nothing: clicking a promo is worse than skipping the + task, and the caller already reports the shortfall. """ activities = [] for link in self.get_sidebar_section().find_elements(By.TAG_NAME, "a"): try: - if "bing.com/search" in (link.get_dom_attribute("href") or ""): + if self._is_daily_set_activity(link.get_dom_attribute("href") or ""): activities.append(link) except StaleElementReferenceException: continue return activities + @staticmethod + def _is_daily_set_activity(href: str) -> bool: + """Whether an href in the daily set panel is an activity rather than a promo.""" + return any( + marker in href + for marker in ("bing.com/search", "bing.com/rewards", "rewards.bing.com/") + ) + # ------------------------------------------------------------------ # explore on bing (absent in en-US, present in some other markets) # ------------------------------------------------------------------ diff --git a/tests/test_element_selectors.py b/tests/test_element_selectors.py index 5e5b8a3..3db8431 100644 --- a/tests/test_element_selectors.py +++ b/tests/test_element_selectors.py @@ -190,5 +190,59 @@ class DailySetOpener(unittest.TestCase): selectors_for(driver).get_open_daily_set_button() +class DailySetActivityUrls(unittest.TestCase): + """Which hrefs in the panel count as activities and which are promos.""" + + def _links(self, *hrefs): + links = [FakeElement(text="activity", attributes={"href": h}) for h in hrefs] + panel = FakeElement( + attributes={"id": "react-aria-42"}, + children={(By.TAG_NAME, "a"): links}, + ) + + return FakeDriver(children={(By.TAG_NAME, "section"): [panel]}) + + def test_matches_a_plain_search_activity(self): + found = selectors_for( + self._links("https://www.bing.com/search?q=weather") + ).get_daily_set_elements() + + self.assertEqual(len(found), 1) + + def test_matches_a_rewards_path_activity(self): + # "Turn referrals into rewards" awards points and is not a search. + found = selectors_for( + self._links("https://www.bing.com/rewards/panelflyout") + ).get_daily_set_elements() + + self.assertEqual(len(found), 1) + + def test_matches_an_activity_on_the_rewards_host(self): + found = selectors_for( + self._links("https://rewards.bing.com/redeem/12345") + ).get_daily_set_elements() + + self.assertEqual(len(found), 1) + + def test_skips_the_bing_app_promo(self): + # The link behind #45. Clicking it leaves the rewards host and every + # element captured beforehand goes stale. + found = selectors_for( + self._links("https://bingapp.microsoft.com/bing?adjust=14u4j3kz") + ).get_daily_set_elements() + + self.assertEqual(found, []) + + def test_keeps_activities_and_drops_promos_from_the_same_panel(self): + found = selectors_for(self._links( + "https://bingapp.microsoft.com/bing?adjust=14u4j3kz", + "https://www.bing.com/search?q=news", + "https://www.bing.com/rewards/panelflyout", + "https://play.google.com/store/apps/details?id=com.microsoft.bing", + )).get_daily_set_elements() + + self.assertEqual(len(found), 2) + + if __name__ == "__main__": unittest.main()