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.
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.
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')]
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)
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:
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.
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')]