yashaka / selene

User-oriented Web UI browser tests in Python
https://yashaka.github.io/selene/
MIT License
676 stars 145 forks source link

consider moving to protocols for the majority of helper-entities like Command #518

Open yashaka opened 4 months ago

yashaka commented 4 months ago

This might allow to move from something like this:

# command.py

class js:

    class __ClickWithOffset(Command[Element]):
        def __init__(self):
            self._description = 'click'

        @overload
        def __call__(self, element: Element) -> None: ...

        @overload
        def __call__(self, *, xoffset=0, yoffset=0) -> Command[Element]: ...

        def __call__(self, element: Element | None = None, *, xoffset=0, yoffset=0):
            def func(element: Element):
                element.execute_script(
                    '''
                    const offsetX = arguments[0]
                    const offsetY = arguments[1]
                    const rect = element.getBoundingClientRect()

                    function mouseEvent() {
                      if (typeof (Event) === 'function') {
                        return new MouseEvent('click', {
                          view: window,
                          bubbles: true,
                          cancelable: true,
                          clientX: rect.left + rect.width / 2 + offsetX,
                          clientY: rect.top + rect.height / 2 + offsetY
                        })
                      }
                      else {
                        const event = document.createEvent('MouseEvent')
                        event.initEvent('click', true, true)
                        event.type = 'click'
                        event.view = window
                        event.clientX = rect.left + rect.width / 2 + offsetX
                        event.clientY = rect.top + rect.height / 2 + offsetY
                        return event
                      }
                    }
                    element.dispatchEvent(mouseEvent())
                    ''',
                    xoffset,
                    yoffset,
                )

            if element is not None:
                # somebody passed command as `.perform(command.js.click)`
                # not as `.perform(command.js.click())`
                func(element)
                return None

            return Command(
                (
                    self.__str__()
                    if (not xoffset and not yoffset)
                    else f'click(xoffset={xoffset},yoffset={yoffset})'
                ),
                func,
            )

    click = __ClickWithOffset()

to something like this:

# command.py

class js:
    @staticmethod
    @overload
    def click(element: Element | None) -> None: ...

    @staticmethod
    @overload
    def click(*, xoffset=0, yoffset=0) -> Command[Element]: ...

    @staticmethod
    def click(element: Element | None = None, *, xoffset=0, yoffset=0):
        def func(element: Element):
            element.execute_script(
                '''
                const offsetX = arguments[0]
                const offsetY = arguments[1]
                const rect = element.getBoundingClientRect()

                function mouseEvent() {
                  if (typeof (Event) === 'function') {
                    return new MouseEvent('click', {
                      view: window,
                      bubbles: true,
                      cancelable: true,
                      clientX: rect.left + rect.width / 2 + offsetX,
                      clientY: rect.top + rect.height / 2 + offsetY
                    })
                  }
                  else {
                    const event = document.createEvent('MouseEvent')
                    event.initEvent('click', true, true)
                    event.type = 'click'
                    event.view = window
                    event.clientX = rect.left + rect.width / 2 + offsetX
                    event.clientY = rect.top + rect.height / 2 + offsetY
                    return event
                  }
                }
                element.dispatchEvent(mouseEvent())
                ''',
                xoffset,
                yoffset,
            )

        if isinstance(element, Element):
            # somebody passed command as `.perform(command.js.click)`
            # not as `.perform(command.js.click())`
            func(element)

        return Command(
            (
                'click'
                if (not xoffset and not yoffset)
                else f'click(xoffset={xoffset},yoffset={yoffset})'
            ),
            func,
        )

that can be kind of more KISS way to implement a command that can be called in 3 different syntax:

element.perform(command.js.click(xoffset=10, yoffset=0))
element.perform(command.js.click())
element.perform(command.js.click)

Right now, the "functional" way can hardly make autocompletion work after element.perform(command.js.click).HERE

There are also other examples where we have some problems with autocompletion because of too "strict" typing based on normal classes...