diff --git a/tests/SELENIUM_FIXES.md b/tests/SELENIUM_FIXES.md new file mode 100644 index 00000000..b0fcaf27 --- /dev/null +++ b/tests/SELENIUM_FIXES.md @@ -0,0 +1,128 @@ +# Selenium Test Fixes for ChromeDriver Timeouts + +## Problem Description + +Selenium tests were intermittently failing on GitHub Actions with `ReadTimeoutError` exceptions. The ChromeDriver process would hang without providing feedback, causing tests to timeout and fail. This particularly affected tests involving websockets and complex page interactions. + +## Root Causes + +1. **ChromeDriver Hangs**: ChromeDriver can hang in headless mode, especially in resource-constrained CI environments +2. **Insufficient Timeouts**: Default timeout values (3 seconds) were too short for complex operations +3. **WebSocket Delays**: WebSocket communication requires more time to establish connections and propagate updates +4. **Hard-coded Waits**: Use of `time.sleep()` instead of explicit waits led to timing issues + +## Solutions Implemented + +### 1. WebDriver Timeout Configuration (`tests/utils.py`) + +Added explicit timeout configuration to ChromeDriver instances: + +```python +def get_chrome_webdriver(cls): + driver = super().get_chrome_webdriver() + driver.set_page_load_timeout(60) # Max time for page loads + driver.set_script_timeout(60) # Max time for async scripts + driver.implicitly_wait(10) # Element search timeout + return driver +``` + +### 2. Configurable Timeout Values (`tests/config.json`) + +Added configurable timeout parameters: + +- `webdriver_wait_timeout`: 20 seconds (for general WebDriverWait calls) +- `websocket_wait_timeout`: 30 seconds (for websocket-related operations) + +### 3. Improved Alert Handling (`tests/utils.py`) + +Replaced hard-coded `time.sleep(2)` with explicit wait: + +```python +def _ignore_location_alert(self, driver=None): + try: + WebDriverWait(driver, 5).until(EC.alert_is_present()) + # Handle alert... + except (NoAlertPresentException, Exception): + pass # Timeout or no alert is okay +``` + +### 4. Dynamic Timeout Usage in Tests + +Tests now use configurable timeouts instead of hard-coded values: + +```python +timeout = self.config.get("webdriver_wait_timeout", 10) +WebDriverWait(self.base_driver, timeout).until(...) +``` + +### 5. Restored Tests with Improvements + +Re-added the following tests that were removed in PR #581: + +- `test_websocket_marker`: Tests real-time location updates via websockets +- `test_topology_graph`: Tests network topology visualization +- `test_create_prefix_users`: Tests RADIUS batch user creation with PDF generation + +All restored tests include: +- Increased timeout values +- Explicit waits instead of sleeps +- Better error handling + +## Testing Recommendations + +### Local Testing + +```bash +cd /opt/openwisp/docker-openwisp +make develop-pythontests +``` + +### CI Testing + +The CI workflow already includes retry logic (5 attempts with 30-second delays). With these fixes, tests should pass consistently on the first attempt. + +### Timeout Tuning + +If tests still timeout in CI: + +1. Increase values in `tests/config.json`: + - `webdriver_wait_timeout`: Increase for general operations + - `websocket_wait_timeout`: Increase for websocket tests + +2. Check ChromeDriver version compatibility +3. Review GitHub Actions runner resource constraints + +## Known Issues and Workarounds + +### Issue: ChromeDriver Still Hangs + +**Workaround**: The CI workflow uses a retry mechanism. If hangs persist: + +1. Check ChromeDriver and Selenium versions for compatibility +2. Consider adding `--disable-dev-shm-usage` Chrome option +3. Verify headless mode is properly configured + +### Issue: WebSocket Tests Fail Intermittently + +**Workaround**: WebSocket connections depend on multiple services: + +1. Ensure all containers are healthy before tests +2. Check Redis container status (used for channels) +3. Verify WebSocket container logs for connection issues + +## References + +- PR #581: Tests removed due to timeout issues +- PR #585: This fix (investigation and resolution) +- Selenium Documentation: https://selenium-python.readthedocs.io/ +- ChromeDriver Issues: https://bugs.chromium.org/p/chromedriver/issues/list + +## Maintenance Notes + +When adding new Selenium tests: + +1. Use `self.config.get('webdriver_wait_timeout', 10)` for standard waits +2. Use `self.config.get('websocket_wait_timeout', 30)` for websocket operations +3. Always use explicit waits (`WebDriverWait`) instead of `time.sleep()` +4. Test locally with `SELENIUM_HEADLESS=1` to simulate CI environment +5. Add appropriate timeout parameters to `find_elements()` calls for dynamic content diff --git a/tests/config.json b/tests/config.json index a9fe43bd..3f92f60f 100644 --- a/tests/config.json +++ b/tests/config.json @@ -10,5 +10,7 @@ "password": "admin", "services_max_retries": 25, "services_delay_retries": 5, - "custom_css_filename": "custom-openwisp-test.css" + "custom_css_filename": "custom-openwisp-test.css", + "webdriver_wait_timeout": 20, + "websocket_wait_timeout": 30 } diff --git a/tests/runtests.py b/tests/runtests.py index 22f99240..e730edf7 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -247,8 +247,10 @@ def test_device_monitoring_charts(self): self.login() self.get_resource("test-device", "/admin/config/device/") self.find_element(By.CSS_SELECTOR, "ul.tabs li.charts").click() + # Increase timeout for chart loading in CI environments + timeout = self.config.get("webdriver_wait_timeout", 10) try: - WebDriverWait(self.base_driver, 3).until(EC.alert_is_present()) + WebDriverWait(self.base_driver, timeout).until(EC.alert_is_present()) except TimeoutException: # No alert means that the request to fetch # monitoring charts was successful. @@ -267,6 +269,7 @@ def test_default_topology(self): def test_console_errors(self): url_list = [ "/admin/", + "/admin/geo/location/add/", "/accounts/password/reset/", "/admin/config/device/add/", "/admin/config/template/add/", @@ -288,6 +291,7 @@ def test_console_errors(self): "/admin/firmware_upgrader/category/add/", ] change_form_list = [ + ["automated-selenium-location01", "/admin/geo/location/"], ["users", "/admin/openwisp_radius/radiusgroup/"], ["default-management-vpn", "/admin/config/template/"], ["default", "/admin/config/vpn/"], @@ -297,6 +301,7 @@ def test_console_errors(self): ["test_superuser2", "/admin/openwisp_users/user/", "field-username"], ] self.login() + self.create_mobile_location("automated-selenium-location01") self.create_superuser("sample@email.com", "test_superuser2") # url_list tests for url in url_list: @@ -314,16 +319,131 @@ def test_add_superuser(self): self.login() self.create_superuser() self.assertEqual( - "The user “test_superuser” was changed successfully.", + 'The user "test_superuser" was changed successfully.', self.find_element(By.CLASS_NAME, "success").text, ) + def test_websocket_marker(self): + """Ensures that the websocket service is running correctly. + + This test uses selenium, it creates a new location, sets a map + marker and checks if the location changed in a second window. + """ + location_name = "automated-websocket-selenium-loc01" + self.login() + self.login(driver=self.second_driver) + self.create_mobile_location(location_name) + self.get_resource(location_name, "/admin/geo/location/") + self.get_resource( + location_name, "/admin/geo/location/", driver=self.second_driver + ) + self.find_element(By.NAME, "is_mobile", driver=self.base_driver).click() + + # Use websocket-specific timeout for marker invisibility check + ws_timeout = self.config.get("websocket_wait_timeout", 30) + mark = len( + self.find_elements( + By.CLASS_NAME, "leaflet-marker-icon", + wait_for="invisibility", + timeout=ws_timeout + ) + ) + self.assertEqual(mark, 0) + + self.add_mobile_location_point(location_name, driver=self.second_driver) + + # Use websocket-specific timeout for marker presence check + mark = len( + self.find_elements( + By.CLASS_NAME, "leaflet-marker-icon", + wait_for="presence", + timeout=ws_timeout + ) + ) + self.assertEqual(mark, 1) + + def test_topology_graph(self): + """Test network topology visualization with improved timeout handling.""" + path = "/admin/topology/topology" + label = "automated-selenium-test-02" + self.login() + self.create_network_topology(label) + self.get_resource(label, path, select_field="field-label") + + # Click on "Visualize topology graph" button + self.find_element(By.CSS_SELECTOR, "input.visualizelink").click() + + # Wait for the graph to load before interacting + self._wait_until_page_ready() + + # Click on sidebar handle + self.find_element(By.CSS_SELECTOR, "button.sideBarHandle").click() + + # Verify topology label + self.assertEqual( + self.find_element(By.CSS_SELECTOR, ".njg-valueLabel").text.lower(), + label, + ) + + try: + console_logs = self.console_error_check() + self.assertEqual(len(console_logs), 0) + except AssertionError: + print("Browser console logs", console_logs) + self.fail() + + self.action_on_resource(label, path, "delete_selected") + self.assertNotIn("