Issue
Foreword: I have seen this very similar post, as well as a few others. Unfortunately PyQt6 bindings do not seem to map to the PySide / Qt(C++) libraries very well, so I wasn't able to use the solutions in those questions.
I'm working on building a test library for an application I build in PyQt6. Very often my tests need to interact with a QMessageBox, however, execution of any QTest functions stops whenever a QMessageBox is spawned. The entire program seems to become blocked when a QMessageBox is spawned, using both the static functions API (msg = QMessageBox.critical(...)
) and both show()
and exec()
in the property-based API (msg = QMessageBox()
, msg.setText(...)
, msg.show()
). I've only been spawning them in the main thread, as doing so from a QThread seems to cause undefined behaviour.
I can't figure out how to access the active QMessageBox. All of my widgets are stored in a layout, with methods used by the layout accessing multiple objects within the layout, for example:
#for context, "repack" refers to 'packing' files into an archive
class RepackTray(QGridLayout):
def __init__(self):
super().__init__()
self.inputDirPath = QLineEdit()
self.repackButton = QPushButton("Start!")
self.repackButton.clicked.connect(lambda:self.repackTargetDir(self.repackButton))
def repackTargetDir(self, button: QPushButton):
sourceDir = self.inputDirPath.text()
...
#pre-repack validation logic, for example, can also spawn QMessageBoxes like so:
if not valid(sourceDir):
msg = QMessageBox.critical(None, "Error!", "Directory is not valid!")
#multiple QMessageBoxes can be spawned at once in a success case
#actual repack call takes place in a QThread, which returns
#results in a signal connected to handleRepackResultInfo()
def handleRepackResultInfo(self, result):
#specifically, I'm trying to click the default button on this QMessageBox
#neither method works, obviously used separately
#method 1 - self.messageBox doesn't exist to the tester
self.messageBox = QMessageBox()
self.messageBox.setText(result.text)
self.messageBox.exec()
#method 2 - test code can't access the QMessageBox later
msg = QMessageBox.information(None, "Title", result.text)
The function I'm using to automate a test looks like this:
app = QApplication(sys.argv)
def clientGUILib()
...
#defines self.repackPanel as the RepackTray instance, works in other functions
...
def start_repack(self):
QTest.mouseClick(self.repackPanel.repackButton, Qt.MouseButton.LeftButton)
#repack works properly, but then QMessageBox appears
#Now what?
I can't seem to figure out how to interact with the QMessageBox from here.
- Using method 1, python doesn't recognize the property self.repackpanel.messageBox
- Using method 2, I haven't been able to find a way to actually get the active QMessageBox
- I've tried app.activeModalWidget(), app.activePopupWidget(), app.activeWindow(), app.focusWidget(), app.topLevelWidgets(); none of them will fire until the QMessageBox is closed.
- I've even tried using external libraries to click after my mouse snaps to the default button or press the Enter key. These don't have any effect, but I'm not sure if it's because they're firing too quickly - I can't use
QWait()
to delay them as it doesn't seem to work until theQMessageBox
is closed, andtime.sleep()
doesn't work since it also pauses Qt execution and rendering.
I'm sure I'm doing quite a few things the wrong way as I don't have much experience with PyQt/Qt, and PyQt documentation isn't that great. If my code is set up in a way that makes automating this unfeasible, some pointers of what the flow should look like would be super appreciated. Even just kludgy ways to get past the dialog box (like what I tried with mouse/keyboard automating libraries) would be helpful at this point - test automation is something that'd be nice to have, but if it's not possible I can go without it. Thank you!
Edit 2: removed Edit 1 as it wasn't very helpful towards understanding the problem. The main issue is that execution of any code freezes when the main event loop spawns a QMessageBox. I'm not sure how I would approach changing the application code to allow my test code to run while a QMessageBox
is active; however, all I need to do is click "Ok" on each one (and verify the text, if possible). No matter how I actually go about creating and closing the boxes, I can't actually seem to execute any Qt code while the main event loop is blocked due to the QMessageBox, and infinite while loops cause the application to hang.
Solution
Solved my own question by finding a similar question (which used PyQt5 and a different testing framework). The idea is to schedule a call to a function that gets the activewindow()
, then if that window is a QMessageBox
, click on the default button.
For posterity, here's the test code that worked for me:
#Within testing class declaration
def close_active_modal(self):
w = QApplication.activeWindow()
if isinstance(w, QMessageBox):
closeBtn = w.defaultButton()
QTest.mouseClick(closeBtn, Qt.MouseButton.LeftButton)
def start_repack(self, delay_time=1):
#Must be scheduled to occur AFTER the messagebox appears
threading.Timer(delay_time, self.close_active_modal).start()
QTest.mouseClick(self.repackPanel.repackButton, Qt.MouseButton.LeftButton)
Answered By - lucs100
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.