Reputation: 21
I have a table in my main GUI. I want to test my ability to delete items in the table using a menu that comes up upon right-clicking on an item. I'm using pytest-qt to conduct testing. Using qtbot.mouseClick seems to work well when clicking on widgets (such as pushbuttons), but when I try to pass it a table item it gives me a type error (due to the table item not being a widget). The line of code that's giving me the error is as follows:
qtbot.mouseClick(maingui.tablename.item(row, col), Qt.RightButton)
with the error:
TypeError: arguments did not match any overloaded call:
mouseClick(QWidget, Qt.MouseButton, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay: int = -1): argument 1 has unexpected type 'QTableWidgetItem'
Given the documentation, this error makes sense to me. My question is, is there a way that this can be done?
I don't think it should be relevant to the question, but the function that gets called by a right-click on a table item uses a QPoint decorator. My code reacts to right-clicks as follows:
@pyqtSlot(QPoint)
def on_tablename_customContextMenuRequested(self, point):
current_cell = self.tablename.itemAt(point)
if current_cell:
row = current_cell.row()
deleteAction = QAction('Delete item', self)
editAction = QAction('Edit item', self)
menu.addAction(deleteAction)
menu.addAction(editAction)
action = menu.exec_(self.tablename.mapToGlobal(point))
if action == deleteAction:
# <do delete stuff>
elif action == editAction:
# <do edit stuff>
Edit: I was able to select an item in the table using the suggestion of eyllanesc, but the right click on that item does not bring up the custom context menu. Here is a minimum reproducible example of my issue, using a two column table with a custom context menu. I need to be able to automatically select the "Delete Item" option during testing:
from time import sleep
import pytest
from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSlot
from PyQt5.QtWidgets import QMainWindow, QTableWidgetItem, QMenu, QAction, QAbstractItemView
from tests.test_ui_generated import ui_minimum_main
pytest.main(['-s'])
class TestTable(ui_minimum_main.Ui_minimum_table, QMainWindow):
def __init__(self, args):
QMainWindow.__init__(self)
self.setupUi(self)
self.table_minimum.setContextMenuPolicy(Qt.CustomContextMenu)
self.table_minimum.setColumnCount(2)
self.detectorHorizontalHeaderLabels = ['Col A', 'Col B']
self.table_minimum.setHorizontalHeaderLabels(self.detectorHorizontalHeaderLabels)
self.table_minimum.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.table_minimum.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table_minimum.setRowCount(1)
self.table_minimum.setRowHeight(0, 22)
item = QTableWidgetItem('test_col_a')
item.setData(Qt.UserRole, 'test_col_a')
self.table_minimum.setItem(0, 0, item)
item = QTableWidgetItem('test_col_b')
item.setData(Qt.UserRole, 'test_col_b')
self.table_minimum.setItem(0, 1, item)
self.table_minimum.resizeRowsToContents()
@pyqtSlot(QPoint)
def on_table_minimum_customContextMenuRequested(self, point):
print('context_menu_requested')
current_cell = self.table_minimum.itemAt(point)
if current_cell:
deleteAction = QAction('Option A- Delete Row', self)
nothingAction = QAction('Option B- Nothing', self)
menu = QMenu(self.table_minimum)
menu.addAction(deleteAction)
menu.addAction(nothingAction)
action = self.menu.exec_(self.table_minimum.mapToGlobal(point))
if action == deleteAction:
self.table_minimum.setRowCount(0)
return
def test_detector_create_delete_gui(qtbot):
w = TestTable([])
qtbot.addWidget(w)
w.show()
qtbot.waitForWindowShown(w)
sleep(.5)
item = w.table_minimum.item(0, 0)
assert item is not None
def interact_with_menu():
# ???????
pass
rect = w.table_minimum.visualItemRect(item)
QTimer.singleShot(100, interact_with_menu)
qtbot.mouseClick(w.table_minimum.viewport(), Qt.RightButton, pos=rect.center())
Upvotes: 2
Views: 1333
Reputation: 244301
QTableWidgetItem are not widgets so you cannot use it directly, instead you must obtain the position of the cell associated with QTableWidgetItem and use that information for the mouseClick.
item = maingui.tablename.item(row, col)
assert item is not None
rect = maingui.tablename.visualItemRect(item)
qtbot.mouseClick(maingui.tablename.viewport(), Qt.RightButton, pos=rect.center())
It should be noted that there may be cells that are not associated with a QTableWidgetItem so if you want to test that case then you must use QModelIndex:
index = maingui.tablename.model().index(row, col)
assert index.isValid()
rect = maingui.tablename.visualRect(index)
qtbot.mouseClick(maingui.tablename.viewport(), Qt.RightButton, pos=rect.center())
The position is with respect to the viewport of the QTableWidget so you must change it to:
@pyqtSlot(QPoint)
def on_table_minimum_customContextMenuRequested(self, point):
print("context_menu_requested")
current_cell = self.table_minimum.itemAt(point)
if current_cell:
deleteAction = QAction("Option A- Delete Row", self)
nothingAction = QAction("Option B- Nothing", self)
menu = QMenu(self.table_minimum)
menu.addAction(deleteAction)
menu.addAction(nothingAction)
action = menu.exec_(self.table_minimum.viewport().mapToGlobal(point))
if action is deleteAction:
self.table_minimum.setRowCount(0)
return
On the other hand, the event that opens the contextual menu is not the click, but rather the OS detects that you want to open the contextual menu, so in Qt you have to emulate that event through QContextMenuEvent as I show below:
class Helper(QObject):
finished = pyqtSignal()
def test_detector_create_delete_gui(qtbot):
helper = Helper()
w = TestTable([])
qtbot.addWidget(w)
w.show()
qtbot.waitForWindowShown(w)
helper = Helper()
def assert_row_count():
assert w.table_minimum.rowCount() == 0
helper.finished.emit()
def handle_timeout():
menu = None
for tl in QApplication.topLevelWidgets():
if isinstance(tl, QMenu):
menu = tl
break
assert menu is not None
delete_action = None
for action in menu.actions():
if action.text() == "Option A- Delete Row":
delete_action = action
break
assert delete_action is not None
rect = menu.actionGeometry(delete_action)
QTimer.singleShot(100, assert_row_count)
qtbot.mouseClick(menu, Qt.LeftButton, pos=rect.center())
with qtbot.waitSignal(helper.finished, timeout=10 * 1000):
QTimer.singleShot(1000, handle_timeout)
item = w.table_minimum.item(0, 0)
assert item is not None
rect = w.table_minimum.visualItemRect(item)
event = QContextMenuEvent(QContextMenuEvent.Mouse, rect.center())
QApplication.postEvent(w.table_minimum.viewport(), event)
Upvotes: 3