1

I am writing a test automation framework for a GUI application and I wanted to use decorators as a way to catch pop-ups that are generated by methods within the class (for example login)

I have a _BaseWindow class that keeps track of the elements of the GUI that are in every window (eg: menu bar, popups), which is inherited by a MainWindow class. The MainWindow class keeps track of buttons on the main menu, as well the dialog generated when one of the buttons is clicked. For example, if you click the login button on the main menu, the login dialog is loaded.

class _BaseWindow(object):
    def __init__(self):
        self.window = "windowName"
        self.popup = None

    def _catch_popups(self, method):
        from functools import wraps
        @wraps(method)
        def wrapper(*args, **kwargs):
            # get a list of the open windows before the method is run
            before = getwindowlist()

            retval = method(*args, **kwargs)

            # get a list of the open windows after the method is run
            after = getwindowlist()

            for window in after:
                if window not in before:
                    self.popup = window
                    break

            return retval
        return wrapper

class MainWindow(_BaseWindow):
    def __init__(self):
        self.dialog = None
        self.logged_in = False

        # buttons
        self.login_button = "btnLogin"

        super(MainWindow, self).__init__()

    def click_login(self):
        if not self.dialog:
            mouseclick(self.window, self.login_button)
            self.dialog = LoginDialog()

class LoginDialog(_BaseWindow):
    def __init__(self):
        # buttons
        self.button_ok = "btnLoginOK"
        self.button_cancel = "btnLoginCancel"
        # text input
        self.input_username = "txtLoginUsername"
        self.input_password = "txtLoginPassword"

        super(LoginDialog, self).__init__()

    @MainWindow._catch_popups
    def perform_login(self, username, password):
        input_text(self.input_username, username)
        input_text(self.input_password, password)
        mouseclick(self.window, self.button_ok)

When I try to test this I get:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "objects/gui.py", line 185, in <module>
    class LoginDialog(_BaseWindow):
  File "objects/gui.py", line 236, in LoginDialog
    @MainWindow._catch_popups
TypeError: unbound method _catch_popups() must be called with 
MainWindow instance as first argument (got function instance instead)

When I try to specify the MainWindow instance as:

@MainWindow._catch_popups(self)

I get:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "objects/gui.py", line 185, in <module>
    class LoginDialog(_BaseWindow):
  File "objects/gui.py", line 236, in LoginDialog
    @MainWindow._catch_popups(self)
NameError: name 'self' is not defined

Any help in understanding this would be most appreciated.

OLIVER.KOO
  • 5,654
  • 3
  • 30
  • 62
AllenMoh
  • 476
  • 2
  • 13

1 Answers1

3

There's an error in your structure here. Instance methods that are defined in the class body are created at the same time as the class and, as such, are unbound methods. Thus, at the time of creation, there is no self to reference. Your _catch_popups method requires an instance.

The natural solution is to change _catch_popups to be a staticmethod that returns an instance method.

@staticmethod
def _catch_popups(method):
    from functools import wraps
    @wraps(method)
    def wrapper(self, *args, **kwargs):
        before = getwindowlist()
        retval = method(self, *args, **kwargs) # notice that self is passed
        after = getwindowlist()
        try: self.popup = next(w for w in after if w not in before)
        return retval
    return wrapper

Here, self is explicitly passed to method because it is still an unbound instance method when it is being passed to the decorator.


As suggested in your comment, you want to add the popup to the window instance. To do this you could update the following methods:

def click_login(self):
    if not self.dialog:
        mouseclick(self.window, self.login_button)
        self.dialog = LoginDialog(self) # pass window to dialog __init__

class LoginDialog(_BaseWindow):
    def __init__(self, parent_window):
        # original code
        self.parent = parent_window

@staticmethod
def _catch_popups(method):
    def wrapper(self, *args, **kwargs):
        # original code
        try: self.parent.popup = ... # add to parent rather than self
        return retval
    return wrapper
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • Thank you, this worked. The original problem is fixed, however when I try to access self.popup the value the same as initialized (None), but when I access self.dialog.popup the popup has been captured. – AllenMoh Aug 08 '17 at 21:47
  • 1
    @AllenMoh That is expected. Your `perform_login` method is on your dialog class, thus the dialog instance is being passed to the instance method created by `_catch_popups`. If you want to capture the popup on the window itself, you will have to give your dialog a reference to the window and then assign the attribute to the window. – Jared Goguen Aug 09 '17 at 14:53
  • 1
    @AllenMoh I've added some updated methods that will assign this property to the main window. – Jared Goguen Aug 09 '17 at 14:59
  • Thank you very much for taking the time to help me understand what I was doing wrong and showing me the right way to do it. With the updated methods everything is working exactly as I want it to. – AllenMoh Aug 09 '17 at 22:16