1

I’m working on a Python project using Selenium and pytest. All tests pass locally in normal (non-headless) mode, but when I run them in GitHub Actions or even locally in headless mode, many of them start failing. I’ve already added several JavaScript-based workarounds, but they don’t help. Could you please advise what I should try next?

Test Results

============================= test session starts =============================
collecting ... collected 1 item

tests/test_header_click.py::test_header_clicks 

============================= 1 failed in 38.27s ==============================
FAILED                    [100%]
tests\test_header_click.py:6 (test_header_clicks)
driver = <selenium.webdriver.chrome.webdriver.WebDriver (session="c4abb5b393cb24809aa039173f966654")>

    @allure.feature("Header")
    @allure.story("Top Navigation")
    @allure.title("Verify all header menu links are clickable and open correct pages")
    @pytest.mark.smoke
    def test_header_clicks(driver):
    
        header = HeaderPage(driver)
    
        items = [
            (header.click_magazine, "magazine", True),
            (header.click_reviews, "reviews", True),
            (header.click_support, "support", True),
            (header.click_profile_icon, "my-account", True)
        ]
    
        for action, expected, need_back in items:
            with allure.step(f"Check header link: {expected}"):
>               action()

tests\test_header_click.py:24: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pages\header_page.py:28: in click_magazine
    self.click(self.MAGAZINE)
pages\base_page.py:31: in click
    element = self.wait.until(EC.element_to_be_clickable(locator))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <selenium.webdriver.support.wait.WebDriverWait (session="c4abb5b393cb24809aa039173f966654")>
method = <function element_to_be_clickable.<locals>._predicate at 0x000002235C2B07D0>
message = ''

    def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """Wait until the method returns a value that is not False.
    
        Calls the method provided with the driver as an argument until the
        return value does not evaluate to ``False``.
    
        Parameters:
        -----------
        method: callable(WebDriver)
            - A callable object that takes a WebDriver instance as an argument.
    
        message: str
            - Optional message for :exc:`TimeoutException`
    
        Return:
        -------
        object: T
            - The result of the last call to `method`
    
        Raises:
        -------
        TimeoutException
            - If 'method' does not return a truthy value within the WebDriverWait
            object's timeout
    
        Example:
        --------
        >>> from selenium.webdriver.common.by import By
        >>> from selenium.webdriver.support.ui import WebDriverWait
        >>> from selenium.webdriver.support import expected_conditions as EC
    
        # Wait until an element is visible on the page
        >>> wait = WebDriverWait(driver, 10)
        >>> element = wait.until(EC.visibility_of_element_located((By.ID, "exampleId")))
        >>> print(element.text)
        """
        screen = None
        stacktrace = None
    
        end_time = time.monotonic() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, "screen", None)
                stacktrace = getattr(exc, "stacktrace", None)
            if time.monotonic() > end_time:
                break
            time.sleep(self._poll)
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message:

.venv\Lib\site-packages\selenium\webdriver\support\wait.py:138: TimeoutException

Process finished with exit code 1

helpers/browser.py

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def get_driver():
    options = Options()

    options.add_argument("--window-size=1920,1080")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-gpu")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)

    driver.implicitly_wait(5)
    return driver

pages/base_page.py

import time

from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from utils.popup_handler import PopupHandler


class BasePage:
    def __init__(self, driver, timeout=15):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)
        self.popups = PopupHandler(driver)

    #locators

    def pause(self, seconds):
        time.sleep(seconds)

    def open(self, url):
        self.driver.get(url)

    def click(self, locator):
        element = self.wait.until(EC.presence_of_element_located(locator))
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()

    def scroll_to(self, locator):
        element = self.wait.until(EC.presence_of_element_located(locator))
        self.driver.execute_script("""
            const rect = arguments[0].getBoundingClientRect();
            window.scrollBy({top: rect.top - window.innerHeight/3, behavior: 'smooth'});
        """, element)

    def type(self, locator, text):
        element = self.wait.until(EC.visibility_of_element_located(locator))
        element.clear()
        element.send_keys(text)


    def get_text(self, locator):
        element = self.wait.until(EC.visibility_of_element_located(locator))
        return element.text

    def is_visible(self, locator):
        try:
            self.wait.until(EC.visibility_of_element_located(locator))
            return True
        except:
            return False

    def hover(self, locator):
        # Ensure element is visible and scrolled into view before hovering
        element = self.wait.until(EC.presence_of_element_located(locator))
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
        element = self.wait.until(EC.visibility_of_element_located(locator))
        # Use JavaScript hover for better headless mode compatibility
        # Trigger both mouseover and mouseenter events to ensure dropdowns appear
        self.driver.execute_script("""
            var element = arguments[0];
            var mouseover = new MouseEvent('mouseover', {bubbles: true, cancelable: true});
            var mouseenter = new MouseEvent('mouseenter', {bubbles: true, cancelable: true});
            element.dispatchEvent(mouseover);
            element.dispatchEvent(mouseenter);
        """, element)
        # Also try ActionChains as backup
        try:
            ActionChains(self.driver).move_to_element(element).perform()
        except Exception:
            pass  # JavaScript hover already executed

    def check_availability(self):
        self.click(self.AVAILABILITY_BUTTON)

    def add_to_cart(self):
        element = self.wait.until(EC.presence_of_element_located(self.ADD_TO_CART_BUTTON))
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
        element = self.wait.until(EC.element_to_be_clickable(self.ADD_TO_CART_BUTTON))
        self.driver.execute_script("arguments[0].click();", element)
        self.wait_for_cart_popup()

    def close_cart_popup(self):
        self.pause(0.5)
        self.click(self.CART_CLOSE_BUTTON)

    def open_cart(self):
        self.pause(1.5)
        element = self.wait.until(EC.presence_of_element_located(self.VIEW_CART_POPUP))
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
        self.wait.until(EC.element_to_be_clickable(self.VIEW_CART_POPUP))
        self.driver.execute_script("arguments[0].click();", element)

    def clean_popups(self):
        try:
            self.popups.clean()
        except Exception as e:
            print(f"[Popups] clean() failed but ignored: {e}")

    def wait_for_cart_popup(self, timeout=10):
        WebDriverWait(self.driver, timeout).until(
            EC.visibility_of_element_located(self.VIEW_CART_POPUP)
        )

tests/test_header_click.py

import allure
from pages.header_page import HeaderPage
import pytest

@allure.feature("Header")
@allure.story("Top Navigation")
@allure.title("Verify all header menu links are clickable and open correct pages")
@pytest.mark.smoke
def test_header_clicks(driver):

    header = HeaderPage(driver)

    items = [
        (header.click_magazine, "magazine", True),
        (header.click_reviews, "reviews", True),
        (header.click_support, "support", True),
        (header.click_profile_icon, "my-account", True)
    ]

    for action, expected, need_back in items:
        with allure.step(f"Check header link: {expected}"):
            action()
            assert expected in driver.current_url.lower()
            if need_back:
                driver.back()

    with allure.step("Click logo to return to homepage"):
        header.click_logo()
        assert "example.com" in driver.current_url.lower()

pages/header_page.py

from selenium.webdriver.support import expected_conditions as EC

from selenium.webdriver.common.by import By
from pages.base_page import BasePage

class HeaderPage(BasePage):

    #HEADER
    LOGO = (By.XPATH, "//a[@class='logo']")

    PRODUCTS = (By.XPATH, "//*[@id='menu-item-112235']")

    MAGAZINE = (By.CSS_SELECTOR, "header a[href*='/category/particle-magazine/']")
    REVIEWS = (By.CSS_SELECTOR, "header a[href*='/particle-reviews/']")
    SUPPORT = (By.CSS_SELECTOR, "header a[href*='/faq-support']")

    PROFILE_ICON = (By.XPATH, "//*[@class='btn btn-transparent login ']")
    CART_ICON = (By.XPATH, "//*[@class='shopping-basket']")

    # METHODS CLICK HEADER

    def click_logo(self):
        self.click(self.LOGO)

    def click_magazine(self):
        self.click(self.MAGAZINE)

    def click_reviews(self):
        self.click(self.REVIEWS)

    def click_support(self):
        self.click(self.SUPPORT)

    def click_profile_icon(self):
        self.click(self.PROFILE_ICON)

    def click_cart_icon(self):
        self.click(self.CART_ICON)

    # HOVERs
    def hover_support(self):
        self.hover(self.SUPPORT)
        try:
            submenu = (By.CSS_SELECTOR, "#menu-item-534292 .sub-menu, #menu-item-534292 ul")
            self.wait.until(EC.visibility_of_element_located(submenu))
        except Exception:
            self.pause(0.5)

    def hover_products(self):
        self.hover(self.PRODUCTS)
        try:
            submenu = (By.CSS_SELECTOR, "#menu-item-112235 .sub-menu, #menu-item-112235 ul")
            self.wait.until(EC.visibility_of_element_located(submenu))
        except Exception:
            self.pause(0.5)

GitHub Actions workflow

name: UI tests - regression

on:
  schedule:
    - cron: "0 5 * * *"
  workflow_dispatch:

jobs:
  regression-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Cache pip
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install Chrome
        uses: browser-actions/setup-chrome@v2
        with:
          chrome-version: stable
          install-chromedriver: false

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run pytest (regression)
        run: |
          pytest -m "regression"

      - name: Upload Allure results (regression)
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-results-regression
          path: allure-results

Tests run fine locally in normal mode on Windows. On Ubuntu or in headless mode, they usually fail with different exceptions like TimeoutException or ElementClickInterceptedException: element click intercepted.

In normal mode, everything works well and all tests pass.

1 Answer 1

2

It's most likely because the -window-size=1920,1080 no longer works in Chrome when in headless mode, so it's running at a different size than when not in headless mode. You should use --screen-info={0,0 1920x1080} or else resize the Window after you launch the browser. See: https://issues.chromium.org/issues/423334494

Also, you should remove all the JavaScript click workarounds and driver.implicitly_wait(5) (you are overriding the explicit waits you are trying to use).

Sign up to request clarification or add additional context in comments.

2 Comments

options.add_argument("--screen-info={0,0 1920x1080}"). All my tests "PASSED" on headless mode. Thank you so much!
This has helped me very much, Thank you!!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.