Lorem Ipsum
Lorem Ipsum

Reputation: 4554

Test that widget focus was set

When I set focus on a widget and then check whether it has focus, the result is False instead of True. When the application loads, however, the desired widget has focus.

from PySide2 import QtCore, QtWidgets, QtGui

app = QtWidgets.QApplication([])

widget = QtWidgets.QWidget()
button = QtWidgets.QPushButton()
line_edit = QtWidgets.QLineEdit()

layout = QtWidgets.QVBoxLayout()
layout.addWidget(button)
layout.addWidget(line_edit)

widget.setLayout(layout)
widget.show()

print(f"{line_edit.hasFocus()}")                # False
line_edit.setFocus(QtCore.Qt.OtherFocusReason)  # without this, nothing has focus
print(f"{line_edit.hasFocus()}")                # Expected True, actually False!

app.exec_()

How can I set focus in such a way that I could test it?

EDIT: I'm running these versions:

>>> import PySide2
>>> from PySide2 import QtCore, QtWidgets, QtGui
>>> print("Qt: ", PySide2.QtCore.__version__, " PySide2: ", PySide2.__version__)
Qt:  5.15.2  PySide2:  5.15.2

EDIT2: If I create a shortcut which prints the focus status, the result is True. I suspect that is because the app is executing. It still stands, how could I test this?

QtWidgets.QShortcut(QtGui.QKeySequence("F6"),  widget, lambda: print(f"{line_edit.hasFocus()}"))

Upvotes: 0

Views: 440

Answers (1)

Lorem Ipsum
Lorem Ipsum

Reputation: 4554

In order to test whether a widget has focus, the QCoreApplication event loop must be running and the widget must be visible. Focus is the result of an event and it only makes sense to talk about focus if the widget is showing (Qt seems to require the widget be visible for a focus event to occur). This presents a problem for running tests without user interaction: how do we automatically exit the main event loop? The answer is in the question; by using events.

QCoreApplication.exec_() starts the main event loop. Events are put on a queue and processed in turn (according to a priority). The loop goes on forever until it receives an event telling it to stop. Use postEvent to add an event to the queue. There are different handlers for different events. The QWidget.focusInEvent method is called when the widget takes focus. Other events pass through the general QObject.eventFilter method.

We can install a custom event filter on a QObject using its installEventFilter method. This method takes a QObject with a custom eventFilter:

class CustomEventFilterObject(QObject):
    def eventFilter(self, obj, event):
        if event.type() == QEvent.User:
            print("Handle QEvent types")
            return True
        else:
            # standard event processing
            return QObject.eventFilter(self, obj, event)

# ...

custom_event_filter = CustomEventFilterObject()
widget.installEventFilter(custom_event_filter)

Armed with this knowledge, we can contrive a solution (and is it ever contrived!).

Here is the original question, modified so that the show method is no longer called from this module:

# focus_problem.py
from PySide2 import QtCore, QtWidgets, QtGui

widget = QtWidgets.QWidget()
button = QtWidgets.QPushButton()
line_edit = QtWidgets.QLineEdit()

layout = QtWidgets.QVBoxLayout()
layout.addWidget(button)
layout.addWidget(line_edit)

widget.setLayout(layout)

# line_edit.setFocus(QtCore.Qt.OtherFocusReason)  # without this, nothing has focus

We know that the line_edit focus is only testable after app.exec_() has started the event loop. Create a custom focusInEvent function and attach it to the line_edit. This will get called if the line_edit takes focus. If that happens, exit the application with a return value indicating success. In case focus never lands on the line_edit, create a custom eventFilter on the widget. Post an event that closes the application with a return value indicating failure. Make sure this event is posted after the focus is set. Since focus is set on the line_edit when the module is imported, this won't be an issue for us.

Using unittest, the test looks like:

# test_focus_problem.py
#
# Run with:
#
#     python3 -m unittest discover focus_problem/ --failfast --quiet

import unittest
from PySide2 import QtCore, QtWidgets, QtGui, QtTest

# need a QApplication in order to define the widgets in focus_problem
# module
if not QtWidgets.QApplication.instance():
    QtWidgets.QApplication([])

import focus_problem


class TestFocusEvent(unittest.TestCase):

    # called before the body of test_application_starts_with_line_edit_in_focus is executed
    def setUp(self):

        def focusInEvent(event):
            QtWidgets.QApplication.instance().exit(0)  # Success

        # override the focus event handler
        focus_problem.line_edit.focusInEvent = focusInEvent


        class KillApplicationIfLineEditNeverTakesFocus(QtCore.QObject):

            def eventFilter(self, obj, event):
                if event.type() == QtCore.QEvent.User:
                    QtWidgets.QApplication.instance().exit(1)  # Fail
                    return True
                else:
                    return QtCore.QObject.eventFilter(self, obj, event)

        custom_filter = KillApplicationIfLineEditNeverTakesFocus()
        focus_problem.widget.installEventFilter(custom_filter)

        focus_problem.widget.show()
        QtWidgets.QApplication.instance().postEvent(focus_problem.widget, QtCore.QEvent(QtCore.QEvent.User))

        self.rv = QtWidgets.QApplication.instance().exec_()

    def test_application_starts_with_line_edit_in_focus(self):
        self.assertEqual(self.rv, 0)

The application is started using setUp on a dedicated test class. This allows test_application_starts_with_line_edit_in_focus to collect the return value and report it along with other tests. It checks to see if the application was killed by the focusInEvent (a success) or the eventFilter (a failure).

I'd be surprised if there wasn't an easier way to do all this. :)

Upvotes: 1

Related Questions