Steven Czyz
Steven Czyz

Reputation: 21

How to select an item in a QTableWidget using pytest-qt mouse click?

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

Answers (1)

eyllanesc
eyllanesc

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

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)

Upvotes: 3

Related Questions