diff --git a/.gitignore b/.gitignore index a629200..00dc558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.png +*.jpg *.txt *.log !nouns.txt diff --git a/README.md b/README.md index 2fba250..a02b5ea 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ $env:QUERY_SOURCE="trends"; python src/main.py # PowerShell You should also have an Ollama account created (for the LLM), the `ollama` tool installed, and you should have signed in to the Ollama CLI via the command line using `ollama signin`. This project will use a minimal amount of Ollama cloud usage using `gemma4:cloud`. If you wish to use a different model, please change the `model` parameter in the `get_ollama_response` function in `src/llm_utils.py`. -You must also provide an image for the script to upload to complete the visual search task. Currently, this image is named `keypress_times.png` and is located in the root directory of the project (yes, I used a random image from my keyboard analysis to do this). You may provide an image of your own, just ensure that the absolute path of the image is placed in the `VISUAL_SEARCH_IMAGE_PATH` constant at the top of `rewards_tasks.py`. +You must also provide an image for the script to upload to complete the visual search task. Currently, this image is named `visual_search.jpg` and is located in the root directory of the project. You may provide an image of your own, just ensure that the absolute path of the image is placed in the `VISUAL_SEARCH_IMAGE_PATH` constant at the top of `rewards_tasks.py`. Activate the virtual environment & install dependencies (you may have to use `python -m poetry` instead of `poetry`). You must have Python 3.12+ and Poetry installed. diff --git a/src/check_selectors.py b/src/check_selectors.py index 7c5fc55..218b2be 100644 --- a/src/check_selectors.py +++ b/src/check_selectors.py @@ -184,6 +184,18 @@ def main(): driver.execute_script("arguments[0].click();", elements.get_points_breakdown_button()) wait_until(lambda: elements.get_sidebar_section() is not None, 30) + # The section exists before it has content: the panel renders a + # "Loading..." placeholder inside it first, and that satisfies the + # presence check above immediately. Waiting only for the section leaves + # the two selectors below reading an empty panel, so they report FAILED + # for markup that is fine, on a page that is merely slow. Wait for the + # content itself. A selector that really is broken still reports FAILED, + # it just costs the timeout first. + wait_until( + lambda: elements.get_points_earned_from_searches_on_points_breakdown() is not None, + 30, + ) + report.check("get_sidebar_section", elements.get_sidebar_section) report.check( "get_points_earned_from_searches_on_points_breakdown", diff --git a/src/rewards_tasks.py b/src/rewards_tasks.py index c5f80d1..367b79d 100644 --- a/src/rewards_tasks.py +++ b/src/rewards_tasks.py @@ -61,24 +61,39 @@ class RewardsTaskUtils: elem = self.wait_for_element(element_getter, timeout) self.move_to_and_click(elem) - def complete_bing_daily_set(self): + def complete_bing_daily_set(self, expected_activities: int = 3): self.switch_to_earn_page() self.wait_for_then_click(self.elements.get_open_daily_set_button) - daily_set_links = self.wait_for_element(self.elements.get_daily_set_elements) + # The panel hydrates progressively, so the first non-empty snapshot can + # hold fewer than 3 activities. wait_for_element returns on the first + # truthy result, so a 1-element list satisfied it and indexing [1] and + # [2] then raised IndexError, taking the whole task down. Wait for the + # full set instead, and if it never fills, work with what is there. + def full_activity_list(): + activities = self.elements.get_daily_set_elements() - self.move_to_and_click(daily_set_links[0]) - time.sleep(random.uniform(2, 3)) - self.driver.switch_to.window(self.driver.current_window_handle) # refocus on the main tab + return activities if len(activities) >= expected_activities else False - self.move_to_and_click(daily_set_links[1]) - time.sleep(random.uniform(2, 3)) - self.driver.switch_to.window(self.driver.current_window_handle) + try: + daily_set_links = self.wait_for_element(full_activity_list, timeout=30) + except TimeoutException: + daily_set_links = self.elements.get_daily_set_elements() - self.move_to_and_click(daily_set_links[2]) - time.sleep(random.uniform(2, 3)) - self.driver.switch_to.window(self.driver.current_window_handle) + print(f"[WARNING] Daily set panel only shows {len(daily_set_links)} of {expected_activities} activities") + + # Re-read the panel per index: clicking an activity can re-render it and + # stale the captured references. + for index in range(len(daily_set_links)): + activities = self.elements.get_daily_set_elements() + + if index >= len(activities): + break + + self.move_to_and_click(activities[index]) + time.sleep(random.uniform(2, 3)) + self.driver.switch_to.window(self.driver.current_window_handle) # refocus on the main tab self.tab_utils.close_all_other_tabs() @@ -205,9 +220,14 @@ class RewardsTaskUtils: def read_search_points(self): """Open the points breakdown, read the Bing search row, close it again.""" self.switch_to_earn_page() - self.wait_for_then_click(self.elements.get_points_breakdown_button) - close_btn = self.wait_for_element(self.elements.get_close_button_on_points_breakdown) + # 30s rather than the default 10s: this runs after the earlier tasks have + # navigated away, so the earn page re-renders from scratch first and the + # breakdown button regularly needs longer than 10s to appear. Timing out + # here skipped the entire search task while points were still available. + self.wait_for_then_click(self.elements.get_points_breakdown_button, timeout=30) + + close_btn = self.wait_for_element(self.elements.get_close_button_on_points_breakdown, timeout=15) points_earned, max_pts = self.elements.get_points_earned_from_searches_on_points_breakdown()