Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

consider pydantic style of PageObjects | opinionated POM implementation in Selene #505

Open
yashaka opened this issue Jan 22, 2024 · 6 comments

Comments

@yashaka
Copy link
Owner

yashaka commented Jan 22, 2024

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

@aleksandr-kotlyar
Copy link
Collaborator

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.

@yashaka
Copy link
Owner Author

yashaka commented Jan 25, 2024

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
Copy link
Owner Author

yashaka commented Jul 17, 2024

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
Copy link
Owner Author

yashaka commented Jul 17, 2024

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

@yashaka
Copy link
Owner Author

yashaka commented Jul 20, 2024

First experiments results:)

image

Looks promising :)

yashaka added a commit that referenced this issue Jul 21, 2024
... via descriptor.within(context)

+ add test example
github-actions bot added a commit that referenced this issue Jul 21, 2024
... via descriptor.within(context)

+ add test example
yashaka added a commit that referenced this issue Jul 21, 2024
... to allow to define postponed context resolution
yashaka added a commit that referenced this issue Jul 21, 2024
... built on top of other ones, that were not accessed yet
yashaka added a commit that referenced this issue Jul 21, 2024
yashaka added a commit that referenced this issue Jul 21, 2024
…tities...

this way the API of descriptors will be easier to learn (because of similarity, and consistency), and less different from future version when we make descriptors built into selene.Entities... But... might confuse if we fail to make them built in:D Let's see...
yashaka added a commit that referenced this issue Jul 21, 2024
... where first from "sub-" ones has no name
yashaka added a commit that referenced this issue Jul 21, 2024
... where first from "sub-" ones has no name

+ TODO: should not we set attrs on instance, not lru_cache __get__ result?
yashaka added a commit that referenced this issue Jul 21, 2024
... to fix tests on CI

+ TODO: seems like general timeout becomes 0.5 in some test, and stayed so mutated... find and consider fixing it...
yashaka added a commit that referenced this issue Jul 21, 2024
@yashaka yashaka changed the title consider pydantic style of PageObjects consider pydantic style of PageObjects | opinionated POM implementation in Selene Jul 21, 2024
@yashaka
Copy link
Owner Author

yashaka commented Jul 21, 2024

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 added a commit that referenced this issue Jul 22, 2024
... for lesser interference with selene.Element, etc.
yashaka added a commit that referenced this issue Jul 22, 2024
... not descriptor (even via lru_cache)
yashaka added a commit that referenced this issue Jul 23, 2024
+ DOCS: FAQ: How to simplify search by Test IDs?

+ [#505] TEST: utilize config.selector_to_by_strategy in pom example
github-actions bot added a commit that referenced this issue Jul 23, 2024
+ DOCS: FAQ: How to simplify search by Test IDs?

+ [#505] TEST: utilize config.selector_to_by_strategy in pom example
yashaka added a commit that referenced this issue Jul 24, 2024
+ TODO: implement pom-descriptor-like decorators to name objects returned from methods
github-actions bot added a commit that referenced this issue Jul 24, 2024
+ TODO: implement pom-descriptor-like decorators to name objects returned from methods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants