Search code examples
python-3.xselenium-webdriverpytestpageobjectsparameterization

How do I select an item from a dropdown in a parameterized test in Selenium/Python, where the select method is in a page object?


My test goes through every single value in a dropdown. In the app the user selects a value, clicks a button to multiply the value by itself, and then the app displays the result of that calculation. The test verifies that the result of the calculation matches the number it is supposed to be.

Versions: pytest: 7.4.4 selenium: 4.16.0 python: 3.12

Here's my test_math.py :

import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from page_objects.app_page import AppPage

class TestMathApp:

    @pytest.mark.parametrize("input_value, expected_value",
                             [("1", "1"), ("2", "4"), ("3", "9"), ("4", "16"), ("5", "25"), ("6", "36"), ("7", "49"),
                              ("8", "64"), ("9", "81"), ("10", "100")])
    def test_nums_to_endpoint_1(self, driver, input_value, expected_value):
        app_page = AppPage(driver)
        app_page.open()
        app_page.select_value(input_value)
        app_page.click_endpoint_1()
        assert app_page.result == expected_value

And then here's my page object, app_page.py :

from selenium.webdriver.common.by import By
from selenium import webdriver
from selenium.webdriver.support.select import Select

from page_objects.base_page import BasePage


class AppPage(BasePage):
    __url = "http://localhost:3000/"
    __header = (By.TAG_NAME, "h1")
    __instruction = (By.TAG_NAME, "h2")
    __dropdown = (By.TAG_NAME, "select")
    __endpoint_1_button = (By.XPATH, "//button[contains(text(),'Send to Endpoint 1')]")
    __endpoint_2_button = (By.XPATH, "//button[contains(text(),'Send to Endpoint 2')]")
    __response_text = (By.TAG_NAME, "span")
    __result = (By.TAG_NAME, "label")

    def __init__(self, driver: webdriver):
        super().__init__(driver)

    def open(self):
        super()._open_url(self.__url)

    def click_endpoint_1(self):
        super()._click(self.__endpoint_1_button)


    def select_value(self, input_value):  # will have to unravel this
        super()._wait_until_element_is_visible(self.__dropdown)
        Select(self._driver.find_element(self.__dropdown)).select_by_value(input_value)

    @property
    def result(self) -> str:
        return super()._get_text(self.__result)

and my base_page.py :

from selenium import webdriver # may not need, idk
from selenium.common import NoSuchElementException
from selenium.webdriver.remote.webelement import WebElement # see if I don't need this. it messes with select_by_value and WebElement below is being weird anyway.
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec

class BasePage:
    def __init__(self, driver: webdriver):
        self._driver = driver

    def _find(self, locator: tuple) -> WebElement: # think I need to add return?
        return self._driver.find_element(*locator)

    def _wait_until_element_is_visible(self, locator, time: int = 10):
        wait = WebDriverWait(self._driver, 10)
        wait.until(ec.visibility_of_element_located(locator))

    def _open_url(self, url: str):
        self._driver.get(url)  # does this need a wait?

    def _get_text(self, locator: tuple,  time: int = 10) -> str:
        self._wait_until_element_is_visible(locator, time)
        return self._find(locator).text

    def _click(self, locator: tuple, time: int = 10):
        self._wait_until_element_is_visible(locator, time)
        self._find(locator).click()

Here's what the dropdown looks like in the console:

<select>
    <option value="">Select</option>
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6">6</option>
    <option value="7">7</option>
    <option value="8">8</option>
    <option value="9">9</option>
    <option value="10">10</option>
</select>

When I ignore the page object model for actually selecting the item for the dropdown like below, the tests pass. I verified that the result is correct depending on the input (for example, the result is 9 when the input is 3):

    def test_nums_to_endpoint_1(self, driver):
        app_page = AppPage(driver)
        app_page.open()
        dropdown_locator = Select(driver.find_element(By.TAG_NAME, "select"))
        dropdown_locator.select_by_value("3")
        app_page.click_endpoint_1()
        assert app_page.result == "9", print("Endpoint 1 should have returned '" + str(expected_value) + "' for '" + str(input_value) + "' but returned '" + str(app_page.result) + "'")

I keep getting the error selenium.common.exceptions.InvalidArgumentException: Message: invalid argument: 'using' must be a string on this line in my test:

app_page.select_value(input_value)

...when trying to convert a parameterized test to work in the page object model.

What's wrong with my select_value method? It looks like I have a string, but at some point along the way it looks like the contents of my tuple stop being a string?


Solution

  • To fix the issue, you should use the _find method from BasePage instead of directly calling find_element on the driver in AppPage. Here's the revised select_value method:

    def select_value(self, input_value):
        super()._wait_until_element_is_visible(self.__dropdown)
        dropdown_element = self._find(self.__dropdown)
        Select(dropdown_element).select_by_value(input_value)
    

    base_page.py lacks _click() definition, which is fixed as below:

    def _click(self, locator):
            element = self._driver.find_element(*locator)
            element.click()