Finding Table Cells by Header Text

Finding Table Cells by Header Text

Surely you have seen (screenshots of) huge Excel spreadsheets before. If not, just imagine a wall of numbers. Now imagine such a table in the application you need to test. Verifying a table cell by looking for the correct number? Not so easy.

What would be better than looking for the literal cell value? Well, we could use the cell’s coordinates. However, coordinates are fragile in cases where the table columns can be reordered. How about accessing the table like we humans do: by first looking at the table headers. For the sake of this example, we will be using Squish’s itemviews example application. There is a QTableView in it that looks like this:

Admittedly, that is not quite the wall of numbers we anticipated. It demonstrates the workflow well though. Next, we need to choose the table header views we will use to find the cell. Pick the QTableView by clicking into the empty space below the table rows. After expanding the picked object in the Application Objects view of the Squish IDE, you should see this:

Right-click both of the QHeaderViews and select “Add to Object Map”. Now open the object map script file from the Test Suite Resources section. It will be named names.py, names.js or otherwise depending on the scripting language you chose. For this article I will be using Python.

Among others, you should see these two entries:

o_QHeaderView = {"container": item_Views_QTableView, "orientation": 2, "type": "QHeaderView", "unnamed": 1, "visible": 1}
o_QHeaderView_2 = {"container": item_Views_QTableView, "orientation": 1, "type": "QHeaderView", "unnamed": 1, "visible": 1}

The names o_QHeaderView and o_QHeaderView_2 are not very helpful yet. We will be using them in a function shortly, so let’s give them better names: row_QHeaderView and column_QHeaderView respectively. (Also check out our tip on good object naming!)

Now it is time to wield the power of the Scripted Object Map. Add these two short functions to names.py:

import squish

def headerViewItemName(headerViewName, text):
    return {"container": headerViewName, "text": text, "type": "HeaderViewItem", "visible": True}

def cellFromHeaders(columnText, rowText):
    rowHeaderViewItem = squish.waitForObject(headerViewItemName(row_QHeaderView, rowText))
    columnHeaderViewItem = squish.waitForObject(headerViewItemName(column_QHeaderView, columnText))
    return {"column": columnHeaderViewItem.section, "container": item_Views_QTableView, "row": rowHeaderViewItem.section, "type": "QModelIndex"}

The first one is just a helper. It contains the real name template for the HeaderViewItem so that we don’t need to duplicate it. The second one is what we are after. Remember, we wanted to identify table cells not by content or coordinates but by the table headers. The cellFromHeaders() function takes the row and column header texts and returns the object name of the corresponding cell – or rather QModelIndex. How does it do that? First, it fetches the row and column header view items that have the texts given by the caller. These items can tell us which row and column (“section”) they are in. We then use the section indices to form the object name of the table cell.

If you want to see the result in action, try this test script:

import names

def main():
    startApplication("itemviews")
    cell = waitForObject(names.cellFromHeaders("Three", "1"))
    test.verify(cell.text, "Nitrogen")

That’s it! Now you can address your table cells by row and column and have more stable tests!

What about “waitForObjectItem()”?

Because you’re working with tables in Squish, you might also have stumbled upon the waitForObjectItem() function. Our helper can be easily adapted to work with that one, too:

def cellCoordsFromHeaders(columnText, rowText):
    rowHeaderViewItem = squish.waitForObject(headerViewItemName(row_QHeaderView, rowText))
    columnHeaderViewItem = squish.waitForObject(headerViewItemName(column_QHeaderView, columnText))
    return "{}/{}".format(columnHeaderViewItem.section, rowHeaderViewItem.section)

Calling this version works much the same way as using cellFromHeaders():

cell = waitForObjectItem(names.item_Views_QTableView, names.cellCoordsFromHeaders("Three", "1"))
test.verify(cell.text, "Nitrogen")

Because waitForObjectItem() is not Qt-specific, the approach shown above also works with other GUI toolkits. The only requirement is a way to map row/column captions to the respective indices.

3 Comments

  1. Michael Reding 9 months ago

    This doesn’t work for me. The parameters are apparently not being passed to the functions. How can that be?
    In the debugger, I put a breakpoint on the call to cellFromHeaders, and do a ‘step into’ that takes me to the header for cellFromHeaders. I do another ‘step into’, highlight either columnText or rowText, rightclick and select “Watch” from the popup menu. In the Expressions tab, the value for both variables is “.

    Has this been tested on more than one application? I don’t understand why the parameters are being lost in a simple function call…

  2. Michael Reding 9 months ago

    This isn’t working for me. I copied the functions almost exactly, but end up with Squish saying that it “Failed to find object:” Object ‘{container={container={container={container={container={container={container={name=’frame’ type=’QFrame’…

    • Author
      Marcel Kummer 9 months ago

      Hello Michael,
      a little more information (like the full object names and Squish logs) would be very helpful to solve the error you quoted. Please send an email to support@froglogic.com, we’re happy to take a look over there!

Leave a reply

Your email address will not be published. Required fields are marked *

*