There is nothing like actually using code that you have written in anger at a client-site. In fact, I think most people who write code should use it in its intended marketplace. Using Py.Saunter over 6 weeks at a client led me to re-write how to interact with Selects were handled.

<pre lang="python">from saunter.po.webdriver.element import Element
from selenium.webdriver.support.select import Select as WebDriverSelect

class Select(Element, WebDriverSelect):
    def __set__(self, obj, val):
        s = WebDriverSelect(obj.driver.find_element_by_locator(self.locator))
        method = val[:val.find("=")]
        value = val[val.find("=") + 1:]
        if method == "value":
            s.select_by_value(value)
        elif method == "index":
            s.select_by_index(value)
        elif method == "text":
            s.select_by_visible_text(value)
        else:
            raise saunter.exceptions.InvalidLocatorString(val)

    def __get__(self, obj, cls=None):
        try:
            s = WebDriverSelect(obj.driver.find_element_by_locator(self.locator))
            e = s.first_selected_option
            return str(e.text)
        except AttributeError as e:
            if str(e) == "'SeleniumWrapper' object has no attribute 'connection'":
                pass
            else:
                raise e

Which you then use something like this.

Page Object:

<pre lang="python">from saunter.po.webdriver.select import Select

locators = {
    "current search category": 'id=gh-cat'
}

class CurrentSearchCategory(Select):
    def __init__(self):
        self.locator = locators["current search category"]

class MyPageObject(SaunterTestCase):
    current_search_category = CurrentSearchCategory()
    
    def __init__(self, driver):
        self.driver = driver
    
    # rest of page object

Script:

<pre lang="python">@pytest.marks('deep', 'ebay', 'select', 'single')
def test_single_set_by_value(self):
    s = ShirtPage(self.driver)
    s.go_to_mens_dress_shirts()
    s.current_search_category = "value=550"
    assert(s.current_search_category == "Art")

And that both looks clean and works well in 98% of the situations. Except when you want the list of options in the select box to be return. The, well, it falls on its face. (Care to guess the situation I found myself in while at the client?)

Enter Select2.

Its a crappy name, but there is some precedence for it. Select2 uses properties rather than the descriptor protocol to control the interactions with the browser.

<pre lang="python">class Select2(Element, WebDriverSelect):
    def __init__(self, driver, locator):
        self.driver = driver
        self.locator = locator
    
    @property
    def selected(self):
        s = WebDriverSelect(self.driver.find_element_by_locator(self.locator))
        e = s.first_selected_option
        return str(e.text)

    @selected.setter
    def selected(self, val):
        s = WebDriverSelect(self.driver.find_element_by_locator(self.locator))
        method = val[:val.find("=")]
        value = val[val.find("=") + 1:]
        if method == "value":
            s.select_by_value(value)
        elif method == "index":
            s.select_by_index(value)
        elif method == "text":
            s.select_by_visible_text(value)
        else:
            raise saunter.exceptions.InvalidLocatorString(val)

    @property
    def options(self):
        s = WebDriverSelect(self.driver.find_element_by_locator(self.locator))
        options = s.options
        text = [option.text.strip() for option in options]
        return text

The interaction in the script looks similar to that of Select, but now you need the .selected in there as well.

<pre lang="python">@pytest.marks('deep', 'ebay', 'select', 'single')
def test_single_set_by_value(self):
    s = ShirtPage(self.driver)
    s.go_to_mens_dress_shirts()
    s.current_search_category.selected = "value=550"
    assert(s.current_search_category.selected == "Art")
    assert(len(s.current_search_category.options] == 9)

Should I have gone straight to the property method for selects? Maybe … but I didn’t really understand it at the time and the descriptor protocol route looks cleaner I think. I’ll use Select except when I actually need the options.