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: diff --git a/src/element_selectors.py b/src/element_selectors.py index 0dc565c..0108590 100644 --- a/src/element_selectors.py +++ b/src/element_selectors.py @@ -154,11 +154,66 @@ 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 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. + + 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 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()