Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive testing patterns for Katalon Studio including codeless record-and-playback, Groovy scripted testing, custom keywords, data-driven testing, cross-browser execution, and CI/CD integration with Katalon TestOps.
.claude/skills/pramoddutta-katalon-studio-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 188% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 275% | 0% |
| case-15 | ✓→✓ | = Same ✓ | 207% | 0% |
| case-16 | ✓→✓ | = Same ✓ | 272% | 0% |
You are an expert in Katalon Studio test automation. When the user asks you to create test automation with Katalon, build hybrid codeless and scripted test suites, integrate with Katalon TestOps, or optimize Katalon test execution, follow these detailed instructions.
katalon-project/
Test Cases/
Smoke/
TC_Login_Valid_Credentials
TC_Homepage_Navigation
TC_Search_Basic
Regression/
Auth/
TC_Login_Multiple_Roles
TC_Password_Reset_Flow
TC_Session_Management
Checkout/
TC_Cart_Operations
TC_Payment_Processing
TC_Order_History
API/
TC_API_User_CRUD
TC_API_Authentication
TC_API_Error_Handling
Test Suites/
TS_Smoke_Suite
TS_Regression_Full
TS_API_Suite
TS_Cross_Browser
Object Repository/
Pages/
LoginPage/
input_Email
input_Password
button_Login
label_ErrorMessage
DashboardPage/
heading_Welcome
nav_MainMenu
link_Settings
Test Data/
login_credentials.xlsx
product_catalog.csv
api_payloads.json
Keywords/
com.qa.keywords/
WebUIKeywords.groovy
APIKeywords.groovy
DataKeywords.groovy
AssertionKeywords.groovy
Profiles/
default.glbl
staging.glbl
production.glbl
Reports/
.gitkeepgroovy// Keywords/com.qa.keywords/WebUIKeywords.groovy package com.qa.keywords import com.kms.katalon.core.annotation.Keyword import com.kms.katalon.core.testobject.TestObject import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI import com.kms.katalon.core.util.KeywordUtil import org.openqa.selenium.WebDriver import com.kms.katalon.core.webui.driver.DriverFactory public class WebUIKeywords { @Keyword def loginWithCredentials(String email, String password) { TestObject emailField = findTestObject('Pages/LoginPage/input_Email') TestObject passwordField = findTestObject('Pages/LoginPage/input_Password') TestObject loginButton = findTestObject('Pages/LoginPage/button_Login') WebUI.setText(emailField, email) WebUI.setEncryptedText(passwordField, password) WebUI.click(loginButton) WebUI.waitForPageLoad(30) KeywordUtil.logInfo("Login attempted with email: ${email}") } @Keyword def verifyElementTextContains(TestObject testObject, String expectedText) { String actualText = WebUI.getText(testObject) if (!actualText.contains(expectedText)) { KeywordUtil.markFailed("Expected text '${expectedText}' not found in '${actualText}'") } KeywordUtil.logInfo("Text verification passed: found '${expectedText}'") } @Keyword def waitForElementAndClick(TestObject testObject, int timeout = 30) { WebUI.waitForElementClickable(testObject, timeout) WebUI.click(testObject) KeywordUtil.logInfo("Clicked element after waiting: ${testObject.getObjectId()}") } @Keyword def takeScreenshotOnFailure(String testCaseName) { String screenshotPath = "Reports/Screenshots/${testCaseName}_${System.currentTimeMillis()}.png" WebUI.takeScreenshot(screenshotPath) KeywordUtil.logInfo("Screenshot saved: ${screenshotPath}") } @Keyword def switchToNewWindow() { WebDriver driver = DriverFactory.getWebDriver() String originalHandle = driver.getWindowHandle() Set<String> allHandles = driver.getWindowHandles() for (String handle : allHandles) { if (!handle.equals(originalHandle)) { driver.switchTo().window(handle) KeywordUtil.logInfo("Switched to new window") return } } KeywordUtil.markWarning("No new window found to switch to") } @Keyword def scrollToElement(TestObject testObject) { WebUI.scrollToElement(testObject, 10) WebUI.waitForElementVisible(testObject, 10) } @Keyword def handleAlert(boolean accept = true) { if (WebUI.waitForAlert(5)) { if (accept) { WebUI.acceptAlert() KeywordUtil.logInfo("Alert accepted") } else { WebUI.dismissAlert() KeywordUtil.logInfo("Alert dismissed") } } } @Keyword def verifyPageURL(String expectedURL) { String currentURL = WebUI.getUrl() if (!currentURL.contains(expectedURL)) { KeywordUtil.markFailed("Expected URL containing '${expectedURL}' but got '${currentURL}'") } } @Keyword def clearAndType(TestObject testObject, String text) { WebUI.clearText(testObject) WebUI.sendKeys(testObject, text) } }
groovy// Keywords/com.qa.keywords/APIKeywords.groovy package com.qa.keywords import com.kms.katalon.core.annotation.Keyword import com.kms.katalon.core.testobject.RequestObject import com.kms.katalon.core.testobject.ResponseObject import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS import com.kms.katalon.core.util.KeywordUtil import groovy.json.JsonSlurper import groovy.json.JsonOutput public class APIKeywords { @Keyword def sendRequestAndValidate(RequestObject request, int expectedStatus) { ResponseObject response = WS.sendRequest(request) WS.verifyResponseStatusCode(response, expectedStatus) KeywordUtil.logInfo("Response status: ${response.getStatusCode()}") KeywordUtil.logInfo("Response time: ${response.getElapsedTime()}ms") return response } @Keyword def extractJsonValue(ResponseObject response, String jsonPath) { String body = response.getResponseBodyContent() def json = new JsonSlurper().parseText(body) def value = evaluateJsonPath(json, jsonPath) KeywordUtil.logInfo("Extracted '${jsonPath}': ${value}") return value } @Keyword def verifyJsonSchema(ResponseObject response, Map expectedSchema) { String body = response.getResponseBodyContent() def json = new JsonSlurper().parseText(body) expectedSchema.each { key, type -> if (!json.containsKey(key)) { KeywordUtil.markFailed("Missing field: ${key}") } if (json[key] != null && !json[key].getClass().getSimpleName().equalsIgnoreCase(type)) { KeywordUtil.markWarning("Field '${key}' expected type '${type}' but got '${json[key].getClass().getSimpleName()}'") } } } @Keyword def verifyResponseTime(ResponseObject response, long maxTimeMs) { long elapsed = response.getElapsedTime() if (elapsed > maxTimeMs) { KeywordUtil.markWarning("Response time ${elapsed}ms exceeded threshold ${maxTimeMs}ms") } } @Keyword def buildRequestWithAuth(String url, String method, String token, Map body = null) { RequestObject request = new RequestObject() request.setRestUrl(url) request.setRestRequestMethod(method) request.getHttpHeaderProperties().add( new com.kms.katalon.core.testobject.TestObjectProperty('Authorization', 'HEADER', "Bearer ${token}") ) request.getHttpHeaderProperties().add( new com.kms.katalon.core.testobject.TestObjectProperty('Content-Type', 'HEADER', 'application/json') ) if (body != null) { request.setBodyContent(new com.kms.katalon.core.testobject.impl.HttpTextBodyContent(JsonOutput.toJson(body))) } return request } private def evaluateJsonPath(def json, String path) { def current = json path.split('\\.').each { segment -> if (segment.contains('[')) { def parts = segment.split('\\[') current = current[parts[0]] def index = parts[1].replace(']', '').toInteger() current = current[index] } else { current = current[segment] } } return current } }
groovy// Test Cases/Regression/Auth/TC_Login_Multiple_Roles.groovy import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI import com.kms.katalon.core.model.FailureHandling import com.qa.keywords.WebUIKeywords as CustomWebUI import internal.GlobalVariable // Read test data from external file def testData = findTestData('Test Data/login_credentials') def customUI = new CustomWebUI() for (int row = 1; row <= testData.getRowNumbers(); row++) { String email = testData.getValue('email', row) String password = testData.getValue('password', row) String expectedRole = testData.getValue('role', row) String expectedPage = testData.getValue('expected_page', row) WebUI.comment("Testing login for role: ${expectedRole}") // Navigate to login page WebUI.openBrowser('') WebUI.navigateToUrl(GlobalVariable.BASE_URL + '/login') WebUI.maximizeWindow() // Perform login customUI.loginWithCredentials(email, password) // Verify redirect based on role customUI.verifyPageURL(expectedPage) // Role-specific verifications switch (expectedRole) { case 'admin': WebUI.verifyElementPresent(findTestObject('Pages/DashboardPage/nav_AdminPanel'), 10) break case 'user': WebUI.verifyElementPresent(findTestObject('Pages/DashboardPage/heading_Welcome'), 10) break case 'guest': WebUI.verifyElementPresent(findTestObject('Pages/GuestPage/heading_LimitedAccess'), 10) break } // Cleanup WebUI.closeBrowser() }
groovy// Test Suites/TS_Cross_Browser configuration // This is configured via Katalon Studio UI but here is the equivalent settings /* Test Suite: TS_Cross_Browser Execution Information: - Run configuration: Parallel - Max concurrent instances: 3 Test Cases: 1. Test Cases/Smoke/TC_Login_Valid_Credentials 2. Test Cases/Smoke/TC_Homepage_Navigation 3. Test Cases/Smoke/TC_Search_Basic Environments: - Chrome (latest) - Firefox (latest) - Edge (latest) Retry: - Retry failed test cases: 1 time - Retry immediately Mail Settings: - Send email on: Failure - Recipients: qa-team@company.com */
groovy// Scripts/katalon-testops-upload.groovy import com.kms.katalon.core.configuration.RunConfiguration // Configure TestOps integration def projectDir = RunConfiguration.getProjectDir() def reportDir = "${projectDir}/Reports" // TestOps configuration is set in Project Settings // Project Settings > Katalon TestOps > Enable Integration // Organization: your-org // Project: your-project // Team: your-team // Execution results are automatically uploaded to TestOps // when the integration is enabled and API key is configured println "Report directory: ${reportDir}" println "TestOps integration: ${RunConfiguration.getExecutionProperty('katalon.testops.enabled', 'false')}"
yaml# .github/workflows/katalon-tests.yml name: Katalon Test Execution on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 6 * * *' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Katalon uses: katalon-studio/katalon-studio-github-action@v3 with: version: '9.0.0' projectPath: '${{ github.workspace }}' args: >- -testSuitePath="Test Suites/TS_Smoke_Suite" -browserType="Chrome (headless)" -retry=1 -statusDelay=30 -apiKey=${{ secrets.KATALON_API_KEY }} - name: Upload Reports if: always() uses: actions/upload-artifact@v4 with: name: katalon-reports path: Reports/
groovy// Test Listeners/TestListener.groovy import com.kms.katalon.core.annotation.AfterTestCase import com.kms.katalon.core.annotation.BeforeTestCase import com.kms.katalon.core.annotation.AfterTestSuite import com.kms.katalon.core.context.TestCaseContext import com.kms.katalon.core.context.TestSuiteContext import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI import com.kms.katalon.core.util.KeywordUtil class TestListener { @BeforeTestCase def beforeTestCase(TestCaseContext testCaseContext) { KeywordUtil.logInfo("Starting test: ${testCaseContext.getTestCaseId()}") // Set implicit wait for all tests WebUI.openBrowser('') WebUI.maximizeWindow() } @AfterTestCase def afterTestCase(TestCaseContext testCaseContext) { // Capture screenshot on failure if (testCaseContext.getTestCaseStatus() == 'FAILED') { String screenshotName = testCaseContext.getTestCaseId().replace('/', '_') String path = "Reports/Screenshots/${screenshotName}_${System.currentTimeMillis()}.png" WebUI.takeScreenshot(path) KeywordUtil.logInfo("Failure screenshot saved: ${path}") } // Always close browser after each test WebUI.closeBrowser() KeywordUtil.logInfo("Completed: ${testCaseContext.getTestCaseId()} - ${testCaseContext.getTestCaseStatus()}") } @AfterTestSuite def afterTestSuite(TestSuiteContext testSuiteContext) { KeywordUtil.logInfo("Suite completed: ${testSuiteContext.getTestSuiteId()}") KeywordUtil.logInfo("Total: ${testSuiteContext.getNumberOfTotalTestCases()}") KeywordUtil.logInfo("Passed: ${testSuiteContext.getNumberOfPassedTestCases()}") KeywordUtil.logInfo("Failed: ${testSuiteContext.getNumberOfFailedTestCases()}") } }
The Object Repository is the foundation of maintainable Katalon test automation. Organize test objects into folders that mirror your application's page structure. Each test object should have a meaningful name that describes the element's purpose, not its technical implementation.
For each test object, configure multiple locator strategies in priority order. Start with ID or name attributes, then add CSS selectors, and finally XPath as a fallback. Katalon's Self-Healing feature uses these alternative locators when the primary one fails, automatically adapting to UI changes.
Use parameterized test objects for dynamic elements. If you have a table with rows that can be identified by an index or a data value, create a parameterized object that accepts the identifier as a variable. This avoids creating separate test objects for each row.
Regularly audit the Object Repository for orphaned objects that are no longer referenced by any test case. Orphaned objects add clutter and confusion. Katalon does not automatically detect unused objects, so schedule quarterly cleanup sessions.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-15 | pass→pass | 16,834 | 23,375 | +39% | 1 | 1 | 0% | 2,463 | 7,553 | +207% | 0 | 0 | — |
case-16 | pass→pass | 17,983 | 21,792 | +21% | 1 | 1 | 0% | 1,969 | 7,331 | +272% | 0 | 0 | — |
case-17 | pass→pass | 10,365 | 8,296 | -20% | 1 | 1 | 0% | 1,534 | 5,961 | +289% | 0 | 0 | — |
case-03 | pass→pass | 15,812 | 18,599 | +18% | 1 | 1 | 0% | 1,838 | 7,308 | +298% | 0 | 0 | — |
case-01 | fail→fail | 27,423 | 27,159 | -1% | 1 | 1 | 0% | 4,086 | 9,184 | +125% | 0 | 0 | — |
case-02 | fail→pass | 21,914 | 19,041 | -13% | 1 | 1 | 0% | 3,384 | 7,344 | +117% | 0 | 0 | — |
case-04 | pass→pass | 14,882 | 13,218 | -11% | 1 | 1 | 0% | 2,529 | 7,301 | +189% | 0 | 0 | — |
case-05 | pass→pass | 23,029 | 24,273 | +5% | 1 | 1 | 0% | 3,029 | 8,242 | +172% | 0 | 0 | — |
case-06 | pass→pass | 34,997 | 15,674 | -55% | 1 | 1 | 0% | 2,241 | 6,486 | +189% | 0 | 0 | — |
case-07 | pass→pass | 22,053 | 19,283 | -13% | 1 | 1 | 0% | 2,309 | 7,007 | +203% | 0 | 0 | — |
case-08 | pass→pass | 14,736 | 19,670 | +33% | 1 | 1 | 0% | 1,995 | 7,467 | +274% | 0 | 0 | — |
case-09 | fail→pass | 13,529 | 18,040 | +33% | 1 | 1 | 0% | 2,342 | 6,743 | +188% | 0 | 0 | — |
case-10 | fail→pass | 10,375 | 15,163 | +46% | 1 | 1 | 0% | 1,740 | 6,527 | +275% | 0 | 0 | — |
case-11 | pass→pass | 7,001 | 10,257 | +47% | 1 | 1 | 0% | 924 | 5,468 | +492% | 0 | 0 | — |
case-12 | pass→pass | 12,899 | 18,957 | +47% | 1 | 1 | 0% | 2,313 | 7,329 | +217% | 0 | 0 | — |
case-13 | pass→pass | 13,305 | 17,396 | +31% | 1 | 1 | 0% | 2,005 | 7,618 | +280% | 0 | 0 | — |
case-14 | pass→pass | 14,395 | 19,150 | +33% | 1 | 1 | 0% | 2,394 | 7,038 | +194% | 0 | 0 | — |
case-18 | pass→pass | 9,524 | 7,282 | -24% | 1 | 1 | 0% | 528 | 5,688 | +977% | 0 | 0 | — |
case-19 | pass→pass | 6,753 | 5,585 | -17% | 1 | 1 | 0% | 954 | 5,539 | +481% | 0 | 0 | — |
case-20 | pass→pass | 21,064 | 19,972 | -5% | 1 | 1 | 0% | 2,777 | 7,318 | +164% | 0 | 0 | — |
case-21 | pass→pass | 9,499 | 4,441 | -53% | 1 | 1 | 0% | 577 | 5,427 | +841% | 0 | 0 | — |
case-22 | pass→pass | 17,157 | 15,514 | -10% | 1 | 1 | 0% | 2,145 | 6,296 | +194% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.