Can't click a specific button in a table row (No unique IDs)

I’ve successfully recorded a test where I need to download a statement from my Transaction History table. Everything works until I get to the actual table rows.

The problem is the “Download PDF” button. There are about 50 rows in the table, and every single download button has the exact same class and properties. When I captured the object using the Spy Web tool, it just saved it as button_Download. When I run the test, Katalon either clicks the very first button at the top of the table or just fails because it can’t find a “unique” element.

2 Likes

Hi there, and thanks for posting in the Katalon community! :hugs:

To help you faster, please review our guide on Spy Web Utitliy here: Spy Web utility in Katalon Studio | Katalon Docs. Double-checking the steps and configurations might resolve the issue.

If the doc doesn’t help, feel free to provide more details, and a community member will assist you soon.

Thanks for being a part of our community!
Best,
Elly Tran

In professional automation, we avoid “hardcoded” indices or generic selectors. Instead, we use Parameterized XPaths or Relative Locators.

The issue is that your recorded object is too broad. To solve this reliably in Katalon Studio, we need to define the object’s path relative to its row. By using a dynamic variable in your XPath, you can target any row you want (1st, 3rd, or 50th) using a single Test Object.

The Solution: Dynamic XPaths

Instead of a fixed selector, we use a placeholder like ${row} inside the XPath.

  1. Modify the Object: Change the XPath of your button in the Object Repository to something like: //table[@id='transactions']/tbody/tr[${row}]//button[contains(@class, 'download')]

  2. Pass the Variable: When calling the click keyword, pass the specific row number as a parameter.

Custom Keyword Helper

To make this scalable for your entire team, I recommend creating a Custom Keyword. This abstracts the complexity and allows you to simply say “Click the button in Row X.”

Groovy

import com.kms.katalon.core.annotation.Keyword
import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

class TableHelper {

    /**
     * Clicks a button inside a specific table row
     * @param rowNumber The index of the row (1-based)
     */
    @Keyword
    def clickDownloadButtonByRow(int rowNumber) {
        // Create a dynamic Test Object on the fly
        TestObject dynamicBtn = new TestObject("dynamicDownloadBtn")
        
        // Define the XPath using the row variable
        String xpath = "//table//tbody/tr[" + rowNumber + "]//button[contains(text(), 'Download')]"
        
        dynamicBtn.addProperty("xpath", ConditionType.EQUALS, xpath)
        
        WebUI.waitForElementVisible(dynamicBtn, 5)
        WebUI.click(dynamicBtn)
    }
}

How to use it in your Script:

Instead of fighting with the recorded object, just call your new keyword: CustomKeywords.'TableHelper.clickDownloadButtonByRow'(3)

2 Likes

hi @rkozey

the row index approach works but is fragile if the table sorts or filters change. a more reliable method is to locate the row by its unique content first, then click the button relative to that row.

for example, if each row has a unique transaction reference or date, build your XPath to find the row containing that text, then traverse to the button:

TestObject btn = new TestObject('downloadBtn')
String xpath = "//tr[td[contains(text(), 'TXN-12345')]]//button[contains(@class, 'download')]"
btn.addProperty('xpath', ConditionType.EQUALS, xpath)
WebUI.click(btn)

if you need to parameterize the transaction identifier, pass it as a variable in your test case and concatenate it into the XPath string the same way.
this way your test survives row reordering and pagination changes, unlike a hardcoded row index.

Identical buttons in table rows need row-index XPath—not generic Spy capture.

Object Repo Fix

button_Download → XPath:

//table[@id='transactions']//tr[3]//button[contains(@class,'download')]  // Row 3

Dynamic: //table//tr[${rowNum}]//button[contains(text(),'Download')]

Ready Keyword

@Keyword
void clickRowButton(int row, String tableId='transactions') {
    TestObject btn = new TestObject('dynamic')
    btn.addProperty('xpath', ConditionType.EQUALS, "//table[@id='${tableId}']/tbody/tr[${row}]//button[contains(@class,'download')]")
    WebUI.click(btn)
}

Call: CustomKeywords.clickRowButton(5)—targets row 5 download.

Pro: Find row by text first: //tr[.//td[text()='2026-04-01']]//button

@rkozey

Have you ever studied XPath at all? If not, read the following tutorial.

You have to learn XPath or CSS Selector, and edit the locator of each Test Objects manually.

The Spy tool is not helpful enough for your case.

These generative tools tend to prevent you from learning the fundamental technologies: XPath and CSS Selector. I believe, these technologies are the “must learn” for all Web UI testers. Spend some days learning XPath. Then you will be much more confident of your capability. Without learning the basics, you will stay unable to solve your issues for yourself.

IN each table rows there would be some other items which you can create parameter and target the download pdf button of the specific row.
something like the below //table[@id='transactions']/tbody/tr[${row}]//button[contains(@class, 'download pdf')]

Hmm this will surley help