2

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())

1 Answers1

3

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())

Update:

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)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • This worked to click on the object in my table, thanks! However, it doesn't seem to open the context menu. For some reason, the QPoint signal doesn't seem to be taken by the function. For reference, the function at the end of my original question is a function of the main widget class. Is it a problem of me sending the right-click to the table widget instead of the main gui, thereby never triggering the on_tablename_customContextMenuRequested() function? – Steven Czyz Aug 21 '21 at 03:04
  • @StevenCzyz How are you checking that the contextual menu does not open? You could provide a [MRE] to analyze the cause of the error. – eyllanesc Aug 21 '21 at 03:16
  • I am using a print statement inside the custom context menu function to see if the custom context menu is ever called in the testing process. When I right-click an option in the table manually, the print statement is called as expected. When I use the testing functionality, it does not. I have edited my original post to add a minimum reproducible example. – Steven Czyz Aug 23 '21 at 22:24