selene icon indicating copy to clipboard operation
selene copied to clipboard

consider pydantic style of PageObjects | opinionated POM implementation in Selene

Open yashaka opened this issue 1 year ago • 6 comments

Example:

class ReactContinuousSlider(selene.PageModel):
    config = selene.PageModel.Config(url='https://mui.com/material-ui/react-slider/#ContinuousSlider')
    container = selene.PageModel.Element('#ContinuousSlider+*')
    thumb = container.element('.MuiSlider-thumb')
    thumb_input = thumb.element('input')
    volume_up = container.element('[data-testid=VolumeUpIcon]')
    volume_down = container.element('[data-testid=VolumeDownIcon]')
    rail = container.element('.MuiSlider-rail')


reactSlider = ReactContinuousSlider(browser).open()

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.volume_up))
reactSlider.thumb_input.should(have.value('100'))

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.volume_down))
reactSlider.thumb_input.should(have.value('0'))

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.rail))
reactSlider.thumb_input.should(have.value('50'))

as a shortcut to

class ReactContinuousSlider:
    def __init__(self, browser: selene.Browser | None):
        self.browser = browser if browser else selene.browser
        self.container = self.browser.element('#ContinuousSlider+*')
        self.thumb = self.container.element('.MuiSlider-thumb')
        self.thumb_input = self.thumb.element('input')
        self.volume_up = self.container.element('[data-testid=VolumeUpIcon]')
        self.volume_down = self.container.element('[data-testid=VolumeDownIcon]')
        self.rail = self.container.element('.MuiSlider-rail')

    def open(self):
        self.browser.open('https://mui.com/material-ui/react-slider/#ContinuousSlider')
        return self


reactSlider = ReactContinuousSlider(browser).open()

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.volume_up))
reactSlider.thumb_input.should(have.value('100'))

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.volume_down))
reactSlider.thumb_input.should(have.value('0'))

reactSlider.thumb.perform(command.drag_and_drop_to(reactSlider.rail))
reactSlider.thumb_input.should(have.value('50'))

consider also simplifying container = selene.PageModel.Element('#ContinuousSlider+*') to container = selene.Element('#ContinuousSlider+*')

P.S. related to #439

yashaka avatar Jan 22 '24 12:01 yashaka

Pay attention to difference between init and class attributes https://stackoverflow.com/questions/46720838/python-init-vs-class-attributes As far as I understand it: If you will need by somehow more than one instance of class, then it will rewrite all attributes of the first instance.

aleksandr-kotlyar avatar Jan 25 '24 07:01 aleksandr-kotlyar

Pay attention to difference between init and class attributes https://stackoverflow.com/questions/46720838/python-init-vs-class-attributes As far as I understand it: If you will need by somehow more than one instance of class, then it will rewrite all attributes of the first instance.

It will not, because they are not class attributes. In both python dataclasses and pydantic-based classes – the attributes that you define on the class level – are not class attributes – they are instance attributes. This is the main idea of such type of DSL.

yashaka avatar Jan 25 '24 10:01 yashaka

Hm, seems like we even not need full pydantic style of complete class kitchen DSL-based implementation. With simple python descriptors we already can achieve the following analog of the previous code example:

class ReactContinuousSlider:
    thumb = Element('.MuiSlider-thumb')
    thumb_input = thumb.element('input')
    volume_up = Element('[data-testid=VolumeUpIcon]')
    volume_down = Element('[data-testid=VolumeDownIcon]')
    rail = Element('.MuiSlider-rail')

    def __init__(self, element: Element | None = None):
        self.context = element or browser.element('#ContinuousSlider+*')

Where each Element object passed in context of "descriptor" will check fo existance of context self attribute, and if yes - use it as a root instead of a browser, otherwise – user selene shared browser.

yashaka avatar Jul 17 '24 22:07 yashaka

While self.open can be still implemented explicitely ;) No need to build a complete POM DSL around it...

yashaka avatar Jul 17 '24 22:07 yashaka

First experiments results:)

image

Looks promising :)

yashaka avatar Jul 20 '24 23:07 yashaka

Hm, looks as good enough for POC...

image
import pytest

from selene import browser, have, be, command, query
from selene.support._pom import element, all_


class DataGridMIT:
    grid = element('[role=grid]')

    header = grid.element('.MuiDataGrid-columnHeaders')
    toggle_all_checkbox = header.element('.PrivateSwitchBase-input')

    column_headers = grid.all('[role=columnheader]')

    footer = Element('.MuiDataGrid-footerContainer')
    selected_rows_count = footer.element('.MuiDataGrid-selectedRowCount')
    pagination = footer.element('.MuiTablePagination-root')
    pagination_rows_displayed = pagination.element('.MuiTablePagination-displayedRows')
    page_to_right = pagination.element('[data-testid=KeyboardArrowRightIcon]')
    page_to_left = pagination.element('[data-testid=KeyboardArrowLeftIcon]')

    content = grid.element('[role=rowgroup]')
    rows = content.all('[role=row]')
    _cells_selector = '[role=gridcell]'
    cells = content.all(_cells_selector)
    editing_cell_input = content.element('.MuiDataGrid-cell--editing').element('input')

    def __init__(self, context):
        self.context = context

    def cells_of_row(self, number, /):
        return self.rows[number - 1].all(self._cells_selector)

    def cell(self, *, row, column_data_field=None, column=None):
        if column:
            column_data_field = self.column_headers.element_by(
                have.exact_text(column)
            ).get(query.attribute('data-field'))

        return self.cells_of_row(row).element_by(
            have.attribute('data-field').value(column_data_field)
        )

    def set_cell(self, *, row, column_data_field=None, column=None, to_text):
        self.cell(
            row=row, column_data_field=column_data_field, column=column
        ).double_click()
        self.editing_cell_input.perform(command.select_all).type(to_text).press_enter()


@pytest.mark.parametrize(
    'characters',
    [
        DataGridMIT(
            browser.with_(timeout=2.0).element('#DataGridDemo+* .MuiDataGrid-root')
        ),
    ],
)
def test_material_ui__react_x_data_grid_mit(characters):
    browser.driver.refresh()

    # WHEN
    browser.open('https://mui.com/x/react-data-grid/#DataGridDemo')

    # THEN
    # - check headers
    characters.column_headers.should(have.size(6))
    characters.column_headers.should(
        have._exact_texts_like(
            {...}, 'ID', 'First name', 'Last name', 'Age', 'Full name'
        )
    )

    # - pagination works
    characters.pagination_rows_displayed.should(have.exact_text('1–5 of 9'))
    characters.page_to_right.click()
    characters.pagination_rows_displayed.should(have.exact_text('6–9 of 9'))
    characters.page_to_left.click()
    characters.pagination_rows_displayed.should(have.exact_text('1–5 of 9'))

    # - toggle all works to select all rows
    characters.selected_rows_count.should(be.not_.visible)
    characters.toggle_all_checkbox.should(be.not_.checked)
    characters.toggle_all_checkbox.click()
    characters.toggle_all_checkbox.should(be.checked)
    characters.selected_rows_count.should(have.exact_text('9 rows selected'))
    characters.toggle_all_checkbox.click()
    characters.toggle_all_checkbox.should(be.not_.checked)
    characters.selected_rows_count.should(be.not_.visible)

    # - check rows
    characters.rows.should(have.size(5))
    characters.cells_of_row(1).should(
        have._exact_texts_like({...}, {...}, 'Jon', 'Snow', '14', 'Jon Snow')
    )

    # - sorting works
    # TODO: implement

    # - filtering works
    # TODO: implement

    # - hiding works
    # TODO: implement

    # - a cell can be edited
    characters.set_cell(row=1, column_data_field='firstName', to_text='John')
    characters.cells_of_row(1).should(
        have._exact_texts_like({...}, {...}, 'John', 'Snow', '14', 'John Snow')
    )
    characters.set_cell(row=1, column='First name', to_text='Jon')
    characters.cells_of_row(1).should(
        have._exact_texts_like({...}, {...}, 'Jon', 'Snow', '14', 'Jon Snow')
    )

yashaka avatar Jul 21 '24 22:07 yashaka