Functional Test for QMessageBox... why does not work? - python

I would develop some functional tests for a pyqt application that uses PyQt (or PySide) as GUI library. The tests use Unittest and Qttest library, as reported in many resources, for example this stackoverflow question: Unit and functional testing a PySide-based application?
For the main window all works fine, and the code simulate perfectly Keyboard Types and Mouse Clicks and Movements, but the "devil is in the details"... and this method does not work for a QMessageBox.
In the class of the Main Window, for manage a IOError on opening a file, I initialize a QMessageBox:
self.IOErrMsgBox = QtGui.QMessageBox()
self.IOErrMsgBox.setText("<b>Error</b>")
self.IOErrMsgBox.setInformativeText("""
<p>There was an error opening
the project file:
%s.</p>"""%(path,))
self.IOErrMsgBox.setStandardButtons(QtGui.QMessageBox.Ok)
self.IOErrMsgBox.setDefaultButton(QtGui.QMessageBox.Ok)
self.IOErrMsgBox.exec_()
To test how it works, in functional test I have:
def test__open_project(self):
self.MainWin._project_open(wrong_path, flag='c')
# the function that handles the exception
# and initializes the QMessageBox.
IOErrMsgBox = self.MainWin.IOErrMsgBox
# Reference to the initialized QMessageBox.
self.assertIsInstance(IOErrMsgBox, QMessageBox)
okWidget = self.MainWin.IOErrMsgBox.button(IOErrMsgBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
or, in altenative:
def test__open_project(self):
#... some code, exactly like previous example except for last row...
QTest.keyClick(okWidget, 'o', Qt.AltModifier)
but No one works... and the Ok button is not clicked and I can do it with my mouse pointer :(
Any suggestions?

The question is in general about how to test modal dialogs.
Any modal dialog including QMessageBox will not return from exec_() until it is closed, so the test code in your second code box probably never gets executed.
You could just show() it (making it non-modal) and then follow your code but don't forget to close and delete the dialog afterwards.
Or you use a Timer and schedule a click on the OK button (similar to Test modal dialog with Qt Test). Here is an example:
from PySide import QtGui, QtCore
app = QtGui.QApplication([])
box = QtGui.QMessageBox()
box.setStandardButtons(QtGui.QMessageBox.Ok)
button = box.button(QtGui.QMessageBox.Ok)
QtCore.QTimer.singleShot(0, button.clicked)
box.exec_()

Related

PyQt different windows during a script

I developed a script for pcba testing and it works fine.
Now I want to implement a very simple flow like this:
Window popup just to let the user start the test
Run the test without any window (the terminal is just fine)
Window popup to show pass/fail result of the test
I already developed the two windows in two other .py files, which run without any problems when "standalone".
If I implement these two within the script, the first appears normally, but the second opens and closes istantly.
Without being too complicated, the test function is defined in serial.py, the first window in infoBox.py and the final window in getResult.py.
Inside the serial.py I tried:
import infoBox
import getResult
[...]
def work():
[...]
if __name__ == '__main__':
app1 = QApplication([])
win1 = infoBox.infoBox("""Turn on 24V alim and press OK.""")
win1.show()
app.exec()
[...]
work()
[...]
app2 = QApplication([])
generateWindow(esiti=lst_esiti, info=lst_info) # <------- this calls getResult.getResult class and .show() the widget
win2 = getResult.getResult(lst_esito=lst_esiti, lst_info=lst_info)
win2.show()
app.exec()
So, when serial.py is lounched by terminal, the infoBox instance appears, the work() function does what it has to, and the getResult instance (which has a OK button to be closed) is instantly closed.
Maybe the error is the definition of two QApplication? I tried a lot but I'm not able to manage it.
Thank you very much.
Finally I came up with this.
It was easier than expected...
Solved opening a unique QApplication and call app.exec() after each subclass calling.

PyQt5 MainWindow with flag instantly goes out of scope

I created UI using Qt Designer. Then I converted the ui file to a .py file (pyuic -x) - it works fine if launched directly. Then I tried to subclass my ui in a separate file to implement additional logic. And this is where things start to go wrong. Inheriting from QMainWindow and my Qt Designer file works OK with no issues, as expected. However, the moment I set any WindowFlag for my QMainWindow (any flag - I tried these: StaysOnTop, FramelessWindowHint) and run the file, the window appears and instantly disappears. The program continues to run in a console window, or as a PyCharm process, but the window is gone. It looks to me like it is getting out of scope - but why setting a simple flag would make any difference to the garbage collector? Could someone explain this behaviour?
Minimum code required to reproduce this phenomenon:
from ui import Ui_MainWindow
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
class Logic(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setupUi(self)
self.show()
# self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = Logic()
sys.exit(app.exec_())
The window should appear and stay on the screen until one (or more) of the flags are uncommented. I use Python 3.8 (32-bit) with PyQt5. Run environment provided by PyCharm. Windows 10.
From the documentation of setWindowFlags():
Note: This function calls setParent() when changing the flags for a window, causing the widget to be hidden. You must call show() to make the widget visible again..
So, just move self.show() after setting the flags, or call it from outside the __init__ (after the instance is created), which is the most common and suggested way to do so, as it's considered good practice to show a widget only after it has been instanciated.

pywinauto -how to handle alert window control

I have done my form in VB. I cannot access the child window controls. For instance, the alert box appears after submit button is clicked. Here is my code:
# used backend="uia"
import sys
import pyautogui
from pywinauto.application import Application
import time
print("test")
app=Application().start()
app.Form1.Edit4.type_keys("go")
app.Form1.Edit3.type_keys("12")
app.Form1.Male.click()
app.Form1.ComboBox.type_keys("in")
app.Form1.Edit2.type_keys("33")
app.Form1.Submit.click()
app.Form1.Submit.print_control_identifiers()
app.Success.print_control_identifiers()
app.Form1.Success.click()
Success is the name of child window.
you write that you've used backend="uia" but the code Application().start() uses default backend which is "win32". You have to use Application(backend="uia").start() to choose "uia".
Note: for "win32" backend the alert window is a top-level window. So you need app.Success.OK.click() to click OK button on it. For backend="uia" alert window will be child of "Form1".
EDIT: this code should work:
app.Form1.Success.OKButton.click() # alias of .invoke();
# see IsInvokePatternSupported == True in Inspect.exe
# or
app.Form1.Success.OKButton.click_input() # real click
EDIT2: It may be timing issue. pywinauto has default timeout 5 seconds waiting for dialog appearance. If dialog appears after more than 5 seconds, you need something like that: app.Form1.Success.wait('visible', timeout=20).
Another possible issue is blocking behavior of app.Form1.Submit.click() which calls InvokePattern. Sometimes this pattern implementation waits for dialog closing (this is app side issue though). You might have to change this to app.Form1.Submit.click_input().

How to open a popup window with a spinner in python + Gtk

I have a python Gtk application, with the GUI designed in Glade. It has a "scan" feature which will scan the network for a few seconds and then report its results to the user. During the scanning I want a popup window to appear stealing the focus from the parent until scanning is done.
I use a threading.Lock to synchronize the GUI and the scan thread, which makes the popup to last exactly the right time I want (see scanLock.acquire() ). It seems straightforward to me to implement something like a show() and hide() call before and after the scanLock.acquire(). I did use waitPopupShow and waitPopupHide instead of just calling the window.show() and window.hide() because I also may want to set the Label in the popup or start/stop the GtkSpinner. Here is some code from the GUI class:
def scan(self):
sT = scannerThread(self,self.STagList)
self.dataShare.threadsList.append(sT)
sT.start() # start scanning
self.waitPopupShow('Scanning... Please Wait')
self.scanLock.acquire() # blocks here until scan is finished
self.waitPopupHide()
def waitPopupShow(self, msg): # shows a GtkSpinner until the semaphore is cleared
self.waitDialogLabel.set_text(msg)
self.waitDialogBox.show_all()
self.waitDialog.show()
self.waitDialogSpinner.start()
def waitPopupHide(self):
# how to get the handle to the spinner and stop it?
self.waitDialogSpinner.stop()
self.waitDialog.hide()
def getAll(self):
# GUI
self.builder = Gtk.Builder()
self.builder.add_from_file(path to main GUI)
# ... getting stuff from a first glade file
# getting stuff from the waitDialog glade file
self.builder.add_from_file(path to waitDialog GUI)
self.waitDialog = self.builder.get_object("waitDialog") # GtkWindow
self.waitDialogBox = self.builder.get_object("waitDialogBox") # GtkBox
self.waitDialogLabel = self.builder.get_object("waitDialogLabel") # GtkLabel
self.waitDialogSpinner = self.builder.get_object("waitDialogSpinner") # GtkSpinner
self.waitDialog.hide()
I'm trying hardly since a couple of days to show a dialog with a label and a Gtk.Spinner. The best I obtain at the moment is to have the window showing up with no content. Please note that the self.waitDialog.hide() right after getting it with self.builder.get_object is needed because I set the property of the waitDialog Gtkwindow to Visibile. If I stop with the debugger before .hide() the waitDialog shows up perfectly. Afterwards its broken.
This is the waitDialog GUI file: http://pastebin.com/5enDQg3g
So my best guess is that I'm dooing something wrong, and I could find nothing on creating a new Gtk window over the main one, only basic examples and dialogs. A pointer to the documentation saying a bit about this would be a good starting point...

How to repeatedly show a Dialog with PyGTK / Gtkbuilder?

I have created a PyGTK application that shows a Dialog when the user presses a button.
The dialog is loaded in my __init__ method with:
builder = gtk.Builder()
builder.add_from_file("filename")
builder.connect_signals(self)
self.myDialog = builder.get_object("dialog_name")
In the event handler, the dialog is shown with the command self.myDialog.run(), but this only works once, because after run() the dialog is automatically destroyed. If I click the button a second time, the application crashes.
I read that there is a way to use show() instead of run() where the dialog is not destroyed, but I feel like this is not the right way for me because I would like the dialog to behave modally and to return control to the code only after the user has closed it.
Is there a simple way to repeatedly show a dialog using the run() method using gtkbuilder? I tried reloading the whole dialog using the gtkbuilder, but that did not really seem to work, the dialog was missing all child elements (and I would prefer to have to use the builder only once, at the beginning of the program).
[SOLUTION] (edited)
As pointed out by the answer below, using hide() does the trick. I first thought you still needed to catch the "delete-event", but this in fact not necessary. A simple example that works is:
import pygtk
import gtk
class DialogTest:
def rundialog(self, widget, data=None):
self.dia.show_all()
result = self.dia.run()
self.dia.hide()
def destroy(self, widget, data=None):
gtk.main_quit()
def __init__(self):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("destroy", self.destroy)
self.dia = gtk.Dialog('TEST DIALOG', self.window,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
self.dia.vbox.pack_start(gtk.Label('This is just a Test'))
self.button = gtk.Button("Run Dialog")
self.button.connect("clicked", self.rundialog, None)
self.window.add(self.button)
self.button.show()
self.window.show()
if __name__ == "__main__":
testApp = DialogTest()
gtk.main()
Actually, read the documentation on Dialog.run(). The dialog isn't automatically destroyed. If you hide() it when the run() method exits, then you should be able to run() it as many times as you want.
Alternatively, you can set the dialog to be modal in your builder file, and then just show() it. This will achieve an effect that's similar, but not quite the same as run() - because run() creates a second instance of the main GTK loop.
EDIT
The reason you are getting a segmentation fault if you don't connect to the delete-event signal is that you are clicking the close button twice. Here is what happens:
You click "Run Dialog", this calls the dialog's run() method.
The modal dialog appears, and starts its own main loop.
You click the close button. The dialog's main loop exits, but since run() overrides the normal behavior of the close button, the dialog is not closed. It is also not hidden, so it hangs around.
You wonder why the dialog is still there and click the close button again. Since run() is not active anymore, the normal behavior of the close button is triggered: the dialog is destroyed.
You click "Run Dialog" again, which tries to call the run() method of the destroyed dialog. Crash!
So if you make sure to hide() the dialog after step 3, then everything should work. There's no need to connect to the delete-event signal.
Your dialog should only need to run once. Assuming a menu item triggers the dialog, the code should look something like this:
def on_menu_item_clicked(self, widget, data=None):
dialog = FunkyDialog()
response = dialog.run()
if response = gtk.RESPONSE_OK:
// do something with the dialog data
dialog.destroy()
dialog.run() is a blocking main-loop that returns when the dialog send a response. This is normally done via the Ok and Cancel buttons. When this happens, the dialog is finished and needs to be destroyed.
To show the dialog repeatedly, the user should follow the same workflow (in the example above, that would be clicking on a menu item). The dialog is responsible, in __init__, for setting itself up. If you hide() the dialog, you have the problem of communicating with that dialog so it stays up-to-date with the rest of the application even when it's hidden.
One of the reasons some people want to "run the dialog repeatedly" is because the user has entered invalid information, and you want to give the user the opportunity to correct it. This must be dealt with in the dialog's response signal handler. The order of events in a dialog is:
User physically pushes the Ok button
Dialog sends the response gtk.RESPONSE_OK (-5)
Dialog calls the handler for the response signal
Dialog calls the handler for the Ok button
Dialog run() method returns the response
To prevent steps 4 and 5 from happening, the response handler must suppress the response signal. This is achieved as follows:
def on_dialog_response(self, dialog, response, data=None:
if response == gtk.RESPONSE_OK:
if data_is_not_valid:
# Display an error message to the user
# Suppress the response
dialog.emit_stop_by_name('response')
I just spent some time figuring this out. Re-fetching the same object from a builder will not create a new instance of the object, but only return a reference to the old (destroyed) object. If you create a new builder instance, however, and load your file into the new builder, it will create a new instance.
So my dialog creation function looks something like this:
def create():
builder = gtk.Builder()
builder.add_from_file('gui/main.ui')
dlg = builder.get_object('new_dialog')
def response_function(dialog, response_id):
... do stuff ...
dialog.destroy()
dlg.connect('response', response_function)
dlg.show_all()
Note that I am not blocking for a response with run() in this case because I'm using twisted, but it should be equivalent.

Categories

Resources