Iterable Structures

Sometimes you’ll have some sort of structure that either doesn’t have universally consistent series of data, or it just has an immense number of items. For example, a table of data with many rows.

Just building out all the components for each possible item in the series would be either extremely difficult (if not impossible), or just plain tedious. Luckily, there’s a better way.

First, let’s say we have a table of cars, where each row is an individual car, and their make, model, year, and color are provided, each in their own column. In this table, you can select each row, and delete them, thus deleting the record of that car. There’s also a form just before this table, through which, new cars can be added to the table. Let’s also say that every time the page is loaded, the table is empty, so if we want to have any cars listed, we have to add them ourselves.

The HTML

Here’s some example HTML to represent this (let’s also assume there’s magic JavaScript that will just make this work flawlessly):

<form id="add-car-form" onsubmit="addCar()">
    <select name="make" onchange="updateModels()" required>
        <option value="" selected disabled hidden>--Choose a make--</option>
        <option value="chevrolet">Chevrolet</option>
        <option value="toyota">Toyota</option>
        <option value="ford">Ford</option>
    </select>
    <select name="model" required disabled>
        <option value="" selected disabled hidden>--Choose a model--</option>
    </select>
    <input name="year" required placeholder="year">
    <select name="color" required>
        <option value="" selected disabled hidden>--Choose a color--</option>
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
    </select>
    <button id="add-button" type="submit">Add car</button>
</form>
<button id="delete-button" onclick="deleteSelectedCars()">Delete</button>
<table class="carTable">
    <thead>
        <tr>
            <th></th>
            <th>Make</th>
            <th>Model</th>
            <th>Year</th>
            <th>Color</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><input type="checkbox" name="car" value="1"></td>
            <td>Chevrolet</td>
            <td>Malibu</td>
            <td>1997</td>
            <td>Red</td>
        </tr>
        <tr>
            <td><input type="checkbox" name="car" value="2"></td>
            <td>Toyota</td>
            <td>Corola</td>
            <td>2014</td>
            <td>Green</td>
        </tr>
    </tbody>
</table>

The Tests

Now that we have the table’s HTML, we can get started on the code. To figure what our bottom-level code should be, let’s look at how we want the tests to look. Here’s one example of how these could look:

@pytest.fixture(scope="class", autouse=True)
def page(driver, url):
    driver.get(url)
    return CarTablePage(driver)

@pytest.fixture(scope="class")
def car():
    return Car(CarMake.CHEVROLET, ChevroletModel.IMPALA, 1995, Color.RED)

class TestTableIsEmptyOnLoad:
    def test_table_has_no_entries(self, page):
        assert len(page.cars) == 0

class TestCarIsAdded:
    @pytest.fixture(scope="class", autouse=True)
    def add_car(self, car, page):
        page.add_car(car)

    def test_car_is_in_table(self, page, car):
        assert car in page.cars

class TestCarIsRemoved:
    @pytest.fixture(scope="class", autouse=True)
    def add_car(self, car, page):
        page.add_car(car)

    @pytest.fixture(scope="class", autouse=True)
    def remove_car(self, car, page, add_car):
        page.remove_car(car)

    def test_car_is_not_in_table(self, page, car):
        assert car not in page.cars

That looks pretty straightforward and easy to read, but the question is how to achieve that. To find out, let’s keep working backwards.

The Page

We can start by looking at the page class itself. This serves as the top-level abstraction for how we interact with the page in terms of behavior, so it’s important that it provides us with a readable and useable API. It should have methods that fully describe what were doing, so anyone reading it can follow along:

class CarTablePage(Page):
    add_car_form = AddCarForm()
    car_table = CarTable()

    def add_car(self, car: Car):
        self.add_car_form.add_car(car)

    def remove_car(self, car: Car):
        self.car_table.remove_car(car)

    @property
    def cars(self) -> List[Car]:
        return self.car_table.cars

The Add Form

The code is starting to take shape, and this is pretty self-explainatory, so let’s dig deeper, and look into how the cars get added:

class SelectComponent(PC):
    @property
    def _el(self) -> Select:
        el = self._reference_node.find_element(*self._locator)
        return Select(el)
    def __set__(self, instance, value: Any):
        self.driver = instance.driver
        self._parent = instance
        self._select(value)

class MakeSelect(SelectComponent):
    _locator = (By.CSS_SELECTOR, "[name=make]")
    def _select(self, value: CarMake):
        self.select_by_value(value)

class ModelSelect(SelectComponent):
    _locator = (By.CSS_SELECTOR, "[name=model]")
    def _select(self, value: CarModel):
        self.select_by_value(value)

class YearInput(PC):
    _locator = (By.CSS_SELECTOR, "[name=year]")

class ColorSelect(SelectComponent):
    _locator = (By.CSS_SELECTOR, "[name=color]")
    def _select(self, value: Color):
        self.select_by_value(value)

class AddCarButton(PC):
    _locator = (By.CSS_SELECTOR, "#add-button")

def count_greater_than(
    component: PC,
    count: int,
    **kwargs: dict
) -> Callable[[RemoteWebDriver], bool]:
    """Given a number, checks that the car message count in the list is greater."""
    def callable(driver: RemoteWebDriver) -> bool:
        return len(component._parent.cars) > count
    return callable

class AddCarForm(PC):
    _locator = (By.CSS_SELECTOR, "#add-car-form")

    make = MakeSelect()
    model = ModelSelect()
    year = YearInput()
    color = ColorInput()
    add_car_button = AddCarButton()

    _expected_conditions = {
        "count_greater_than": count_greater_than,
    }

    def add_car(self, car: Car):
        current_car_count = len(self._parent.cars)
        self.make = car.make
        self.model = car.model
        self.year = car.year
        self.color = car.color
        self.add_car_button.click()
        self.wait_until("count_greater_than", count=current_car_count)

Now it’s starting to get a bit more complicated, as it combines multiple advanced concepts. It first uses a generic component that overrides the normal __set__ and _el logic so that Select can be used while the API it provides remains consistent with other form control elements. Further down, it uses a custom wait function that has it rely on its parent (in this case, the page itself) to see if the number of shown cars has changed.

This last step where it waits for the change in car count is essential so that anything leveraging that add_car method doesn’t have to worry about any race conditions created by JavaScript that hasn’t had a chance to run (i.e. a change was made to the DOM, so the thing changing it should wait for the DOM change to complete before moving on).

The Table

This is only one half of the page, though, so let’s look at the other half and see what’s going on inside the table component itself:


class RowCheckbox(PC):
    _find_from_parent = True
    _locator = (By.CSS_SELECTOR, "td:nth-of-type(1) input")
class RowMake(PC):
    _find_from_parent = True
    _locator = (By.CSS_SELECTOR, "td:nth-of-type(2)")
class RowModel(PC):
    _find_from_parent = True
    _locator = (By.CSS_SELECTOR, "td:nth-of-type(3)")
class RowYear(PC):
    _find_from_parent = True
    _locator = (By.CSS_SELECTOR, "td:nth-of-type(4)")
class RowColor(PC):
    _find_from_parent = True
    _locator = (By.CSS_SELECTOR, "td:nth-of-type(5)")

class CarItem(PC):
    _index = None
    _find_from_parent = True
    __locator = "tbody tr:nth-of-type({index})"

    checkbox = RowCheckbox()
    _make = RowMake()
    _model = RowModel()
    _year = RowYear()
    _color = RowColor()

    @property
    def _locator(self) -> tuple:
        return (By.CSS_SELECTOR, self.__locator.format(index=self._index + 1))

    def __init__(self, index: int, parent: PC):
        self._index = index
        self._parent = parent
        self.driver = self._parent.driver

    @property
    def id(self) -> int:
        return int(self.checkbox.get_attribute("value"))
    @property
    def make(self) -> CarMake:
        return CarMake[self._make.text.lower()]
    @property
    def model(self) -> CarModel:
        return CarModel[self._model.text.lower()]
    @property
    def year(self) -> int:
        return int(self._year.text)
    @property
    def color(self) -> Color:
        return  Color[self._color.text.lower()]

class DeleteButton(PC):
    _locator = (By.CSS_SELECTOR, "#delete-button")

class CarTable(PC):
    _locator = (By.CSS_SELECTOR, ".carTable")
    _item_locator = (By.CSS_SELECTOR, "tbody tr")

    delete_button = DeleteButton()

    @property
    def car_count(self) -> int:
        return len(self.find_elements(*self._item_locator))

    @property
    def car_items(self) -> List[CarItem]:
        return list(CarItem(i, self) for i in range(self.car_count))

    @property
    def cars(self) -> List[Car]:
        cars = []
        for car in self.car_items:
            cars.append(Car(car.make, car.model, car.year, car.color, car.id))
        return list(CarItem(i, self) for i in range(self.car_count))

    def remove_car(self, car: Car):
        self.car_items[self.car_items.index(car)].checkbox.click()
        self.delete_button.click()

This does a small trick where instances of CarItem are given a reference to the driver, their parent table component, and an index for the row they represent. They aren’t hooked up like a normal descriptor-based component, but they don’t need to be as the driver and parent reference was passed down explicitely. All the parent table component needs to do is figure out how many items (i.e. rows) it has, and create that many instances of CarItem, giving each one the appropriate index (i.e. 0 to n), a reference to itself, and the driver. That’s all the information each instance needs to still function properly (this is also why the _locator is a property).

Down at the bottom, there’s also car_items and cars, each one providing something similar, but very different. Having car_items on its own gives us an easy means to access those components in the DOM, and giving them their own properties that have meaningful values allows us to get fancier.

The Car

With that in mind, let’s take a look at the final chunk of code, and see the custom data types and enumerators that make this whole operation tick:

class CarMake(Enum):
    @property
    def description(self):
        return self.value.title()

    CHEVROLET = "chevrolet"
    TOYOTA = "toyota"
    FORD = "ford"

class CarModel(Enum):
    @property
    def description(self):
        return self.value.title()

class ChevroletModel(CarModel):
    MALIBU = "malibu"
    IMPALA = "impala"

class ToyotaModel(CarModel):
    COROLA = "corola"
    PRIUS = "prius"

class FordModel(CarModel):
    FIESTA = "fiesta"
    FOCUS = "focus"

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

class Car:

    def __init__(
        self,
        make: CarMake,
        model: CarModel,
        year: int,
        color: Color,
        id: int = None,
    ):
        self._id = id
        self.make = make
        self.model = model
        self.year = year
        self.color = color

    def __eq__(self, other):
        if all(self._id is not None, other._id is not None):
            return self._id == other._id
        return all(
            self.make == other.make,
            self.model == other.model,
            self.year == other.year,
            self.color == other.color,
        )

This lets us consider each car’s data independently of any implementation that uses this data by giving those implementations a means to store and work with the data in a common shape. We no longer have to worry about how a specific method will be expecting the information for a given car, how a method might return such information, or how to compare one car to another, because it’s all handled through this class, and the supporting classes are enumerators to help streamline the development process. They act as a common language for every piece to talk to the others with, and allow us to write such simple tests as the ones above.

The __eq__ in particular helps with several aspects of this example. It allows the comparisons with the instances of CarItem, which in turns allows for things like self.car_items.index(car), because Python leverages __eq__ for a lot of common operations.