Working Script Code will be auto formating and than doesn't work

The following script Code is working after switching between script manual view for and back the script code will be formating an doesen’t work after.
BEFORE:

 import static com.kms.katalon.core.checkpoint.CheckpointFactory.findCheckpoint
import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase
import static com.kms.katalon.core.testdata.TestDataFactory.findTestData
import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject
import static com.kms.katalon.core.testobject.ObjectRepository.findWindowsObject
import com.github.javafaker.Faker as Faker
import com.kms.katalon.core.checkpoint.Checkpoint as Checkpoint
import com.kms.katalon.core.cucumber.keyword.CucumberBuiltinKeywords as CucumberKW
import com.kms.katalon.core.mobile.keyword.MobileBuiltInKeywords as Mobile
import com.kms.katalon.core.model.FailureHandling as FailureHandling
import com.kms.katalon.core.testcase.TestCase as TestCase
import com.kms.katalon.core.testdata.TestData as TestData
import com.kms.katalon.core.testng.keyword.TestNGBuiltinKeywords as TestNGKW
import com.kms.katalon.core.testobject.TestObject as TestObject
import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.kms.katalon.core.windows.keyword.WindowsBuiltinKeywords as Windows
import internal.GlobalVariable as GlobalVariable
import org.openqa.selenium.Keys as Keys
import org.openqa.selenium.WebElement as WebElement
import com.kms.katalon.core.testobject.ConditionType as ConditionType
import com.kms.katalon.core.util.KeywordUtil as KeywordUtil
import groovy.json.JsonSlurper
import groovy.json.JsonOutput

// Funktion zur Extraktion der Werte als Key-Value-Paare
def extractValues(Map jsonMap, String parentKey = "") {
	def result = [:]
	jsonMap.each { key, value ->
		def newKey = parentKey ? "${parentKey}.${key}" : key
		if (value instanceof Map && value.containsKey("value")) {
			result[newKey] = value.value
		} else if (value instanceof Map) {
			result.putAll(extractValues(value, newKey))
		} else {
			result[newKey] = value
		}
	}
	
	return result
}

// Funktion zum Reconstruck Json
void buildNestedMap(Map target, List<String> keys, def value) {
	if (keys.size() == 1) {
		target[keys[0]] = value
		return
	}
	if (!target.containsKey(keys[0]) || !(target[keys[0]] instanceof Map)) {
		target[keys[0]] = [:]
	}
	buildNestedMap(target[keys[0]], keys[1..-1], value)
}

// Rekursive Vergleichsfunktion
boolean compareJson(def input, def output) {
	if (input == null || output == null) {
		if (input != output) {
			KeywordUtil.markWarning("Fehler: Null-Werte stimmen nicht überein. Erwartet: $input, Gefunden: $output")
			return false
		}
		return true
	}
 
	// Wenn input eine Map ist
	if (input instanceof Map) {
		if (!(output instanceof Map)) {
			// Wenn output keine Map ist, vergleiche die Werte direkt
			if (input != output) {
				WebUI.comment("Fehler: Erwartete Map oder gleichen Wert, aber gefunden: ${output.getClass()} mit Wert $output")
				return false
			}
			return true
		}
		input.each { key, value ->
			if (!output.containsKey(key)) {
				WebUI.comment("Fehlender Schlüssel in Output: $key")
				return false
			}
			if (!compareJson(value, output[key])) return false
		}
	}
	// Wenn input eine Liste ist
	else if (input instanceof List) {
		if (!(output instanceof List)) {
			// Wenn output keine Liste ist, vergleiche die Werte direkt
			if (input != output) {
				WebUI.comment("Fehler: Erwartete Liste oder gleichen Wert, aber gefunden: ${output.getClass()} mit Wert $output")
				return false
			}
			return true
		}
		if (input.size() > output.size()) {
			WebUI.comment("Listenlängen stimmen nicht überein: Erwartet mindestens ${input.size()}, aber gefunden ${output.size()}")
			return false
		}
		input.eachWithIndex { item, index ->
			if (!compareJson(item, output[index])) {
				WebUI.comment("Listenwert an Index $index stimmt nicht überein: Erwartet $item, aber gefunden ${output[index]}")
				return false
			}
		}
	}
	// Wenn input ein primitiver Wert ist (String, Zahl, Boolean usw.)
	else {
		// Sicherstellen, dass Zahlen als String verglichen werden
		def inputNormalized = input instanceof Number ? input.toString() : input
		def outputNormalized = output instanceof Number ? output.toString() : output
 
		if (inputNormalized != outputNormalized) {
			WebUI.comment("Wert stimmt nicht überein: Erwartet $inputNormalized, aber gefunden $outputNormalized")
			return false
		}
	}
	return true
}
 
WebUI.openBrowser('')
 
WebUI.callTestCase(findTestCase('workplace/TC- Generell Steps/Step001 - Login TA1 - Natural'), [:], FailureHandling.STOP_ON_FAILURE)
 
WebUI.navigateToUrl(GlobalVariable.poURL)
 
WebUI.waitForElementPresent(findTestObject('Page_Process Orchestrator/input_PO_Search'), 0)
 
WebUI.setText(findTestObject('Page_Process Orchestrator/input_PO_Search'), selectDeleagate)
 
WebUI.sendKeys(findTestObject('Page_Process Orchestrator/input_PO_Search'), Keys.chord(Keys.ENTER))
 
WebUI.delay(2)
 
WebUI.waitForElementPresent(findTestObject('Page_Process Orchestrator/table_Delegate', [('Delegate') : selectDeleagate]),
    0)
 
WebUI.clickOffset(findTestObject('Page_Process Orchestrator/table_Delegate', [('Delegate') : selectDeleagate]), 0, 0)
 
WebUI.click(findTestObject('Page_Process Orchestrator/button Open DetailsSectionAccessButton'))
 
WebUI.waitForElementPresent(findTestObject('Object Repository/Page_Process Orchestrator/span_BPMN'), 0)
 
WebUI.click(findTestObject('Page_Process Orchestrator/span_Test'))
 
WebUI.waitForElementPresent(findTestObject('Object Repository/Page_Process Orchestrator/span_Input'), 0)
 
// Initialisiere Faker für generierte Testdaten
Faker faker = new Faker(new Locale('de'))
 
// XPath für alle Eingabefelder/Ausgabefelder in der ersten Spalte
String xpathFirstColumnInputs = '//span[contains(text(), "Input")]/ancestor::div[3]//tbody//tr/td[1]//input'
 
String xpathFirstColumnOutputs = '//span[contains(text(), "Output")]/ancestor::div[3]//tbody//td[1]'
 
// Erstelle ein dynamisches TestObject für die Eingabefelder/Ausgabefelder der ersten Spalte
TestObject dynamicFirstColumnInputs = new TestObject()
 
TestObject dynamicFirstColumnOutputs = new TestObject()
 
dynamicFirstColumnInputs.addProperty('xpath', ConditionType.EQUALS, xpathFirstColumnInputs)
 
dynamicFirstColumnOutputs.addProperty('xpath', ConditionType.EQUALS, xpathFirstColumnOutputs)
 
// Sammle alle WebElements der Eingabefelder in der ersten Spalte
List<String> firstColumnInputs = WebUI.findWebElements(dynamicFirstColumnInputs, 30)
 
//############################################################################
//
// Dieser Bereich muss für jedes Deligate angepasst werden
//
//############################################################################
// Liste der Suchwerte für Spalte 1 (Pflichtfelder)
List<String> searchValues = ['offerDrafts']
 
String id = 'null'
 
// Generierte Testdaten mit Faker
String NeuerWertLegalPersonID
 
String NeuerWertOfferId
 
if (GlobalVariable.OfferDelegateID == 'null') {
    NeuerWertOfferId = '133901'
} else {
    NeuerWertOfferId = GlobalVariable.OfferDelegateID
}
 
String NeuerWertProjectID
 
if (GlobalVariable.ProjectDelegateID == 'null') {
    NeuerWertProjectID = '53108'
} else {
    NeuerWertProjectID = GlobalVariable.ProjectDelegateID
}
 
if (GlobalVariable.LegalPersonDelegateID == 'null') {
    NeuerWertLegalPersonID = '61309'
} else {
    NeuerWertLegalPersonID = GlobalVariable.LegalPersonDelegateID
}
 
String NeuerWertOfferDraftValues = "[{\"variables\":{"+
									"\"offerId\":{\"value\":\""+NeuerWertOfferId+"\"},"+
									"\"entityName\":{\"value\":\"Project\"},"+
									"\"formTechnicalName\":{\"value\":\"offer_project_to_legal\"},"+
									"\"entityId\":{\"value\":"+NeuerWertProjectID+"},"+
									"\"entityType\":{\"value\": \"Project\"},"+
									"\"offerDraftLinks\": {"+
									   "\"value\":[{\"linkTypeTechName\": null,"+
										   "\"children\":[{\"offerId\":\""+NeuerWertOfferId+"\","+
											   "\"entityName\":\"LegalPerson\","+
											   "\"formTechnicalName\":\"offer_project_to_legal\","+
											   "\"entityType\":\"LegalPerson\","+
											   "\"entityId\": "+NeuerWertLegalPersonID+","+
											   "\"offerDraftLinks\":[{\"linkTypeTechName\":\"project_2_legal_person\","+
												   "\"children\": [],"+
												   "\"existingLinkId\":null}]}],"+
										   "\"existingLinkId\":null}]}},"+
									"\"withVariablesInReturn\":true}]"
 
// Liste der neuen Werte, die in die dritte Spalte geschrieben werden sollen (Strings)
List<String> newValuesForThirdColumn = [NeuerWertOfferDraftValues]
 
//############################################################################
//
// Fertig ;)
//
//############################################################################
// Liste zur Speicherung der gesetzten Werte in der dritten Spalte
List<String> setThirdColumnValues = []
 
// Schleife über alle Suchwerte
for (int searchIndex = 0; searchIndex < searchValues.size(); searchIndex++) {
    String currentSearchValue = searchValues[searchIndex]
 
    String newValueForThirdColumn = newValuesForThirdColumn[searchIndex]
 
    boolean valueFound = false
 
    // Schleife über alle Eingabefelder in der ersten Spalte
    for (int i = 0; i < firstColumnInputs.size(); i++) {
        // Hole den Wert des aktuellen Input-Felds
        String inputValue = (firstColumnInputs[i]).getAttribute('value')
 
        // Überprüfen, ob der Wert dem aktuellen Suchwert entspricht (z.B. 'street', 'housenumber', 'city')
        if (inputValue.equals(currentSearchValue)) {
            valueFound = true
 
            // XPath für das Eingabefeld in der dritten Spalte der passenden Zeile
            String xpathThirdColumnInput = ('//tbody//tr[' + (i + 1)) + ']/td[3]//input'
 
            // Erstelle ein dynamisches TestObject für die dritte Spalte
            TestObject thirdColumnInput = new TestObject()
 
            thirdColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathThirdColumnInput)
 
            // Setze den neuen Wert in die dritte Spalte
            WebUI.setText(thirdColumnInput, newValueForThirdColumn)
 
            // Speichere den gesetzten Wert zur späteren Überprüfung
            setThirdColumnValues.add(newValueForThirdColumn)
 
            // Beende die Schleife, da der Wert gefunden und aktualisiert wurde
            break
        }
    }
    
    // Wenn der Wert nicht gefunden wurde, füge eine neue Zeile hinzu
    if (!(valueFound)) {
        // Klicke auf die Schaltfläche "Field hinzufügen", um eine neue Zeile hinzuzufügen
        WebUI.click(findTestObject('Object Repository/Page_Process Orchestrator/a_Field hinzufugen'))
 
        // Warte kurz, bis die neue Zeile hinzugefügt wurde (Wartezeit anpassen, falls nötig)
        WebUI.delay(2)
 
        // Aktualisiere die Liste der Eingabefelder in der ersten Spalte (nachdem eine neue Zeile hinzugefügt wurde)
        firstColumnInputs = WebUI.findWebElements(dynamicFirstColumnInputs, 30)
 
        // XPath für das neue Eingabefeld in der ersten Spalte (letzte Zeile)
        String xpathNewFirstColumnInput = ('//tbody//tr[' + firstColumnInputs.size()) + ']/td[1]//input'
 
        // Erstelle ein dynamisches TestObject für das Eingabefeld in der neuen Zeile, Spalte 1
        TestObject newFirstColumnInput = new TestObject()
 
        newFirstColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathNewFirstColumnInput)
 
        // Setze den aktuellen Suchwert (z.B. 'street', 'housenumber') in die erste Spalte der neuen Zeile
        WebUI.setText(newFirstColumnInput, currentSearchValue)
 
        // XPath für das Eingabefeld in der dritten Spalte der neuen Zeile
        String xpathNewThirdColumnInput = ('//tbody//tr[' + firstColumnInputs.size()) + ']/td[3]//input'
 
        // Erstelle ein dynamisches TestObject für das Eingabefeld in der dritten Spalte der neuen Zeile
        TestObject newThirdColumnInput = new TestObject()
 
        newThirdColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathNewThirdColumnInput)
 
        // Setze den neuen Wert in die dritte Spalte der neuen Zeile
        WebUI.setText(newThirdColumnInput, newValueForThirdColumn)
 
        // Speichere den gesetzten Wert zur späteren Überprüfung
        setThirdColumnValues.add(newValueForThirdColumn)
    }
}
 
// Klicke auf den "Test" Button, um die Ausgabewerte zu generieren
WebUI.click(findTestObject('Page_Process Orchestrator/button_Speichern'))
 
WebUI.click(findTestObject('Object Repository/Page_Process Orchestrator/button_Test'))
 
// Warte, bis die Ausgabewerte vollständig geladen sind (anpassen, falls nötig)
WebUI.delay(5 // Anpassung je nach Ladezeit
    )
 
// Aktualisiere die Liste der Ausgabefelder in der ersten Spalte (nachdem der Button geklickt wurde)
List<String> firstColumnOutputs = WebUI.findWebElements(dynamicFirstColumnOutputs, 30)
 
searchValues.add('id')
 
String delegateID
 
inputString = NeuerWertOfferDraftValues.replace('[', '').replace(']', '')
 
// Original JSON-String
String inputJson = inputString
 
println(inputString)
println(inputJson)
def jsonSlurper = new JsonSlurper()
 
inputJson = inputJson.replace(", }", "}")  // Korrigiert leere Objekte
					.replace(",]", "]")   // Korrigiert leere Arrays
					.replace(": ,", ": null,") // Setzt null für fehlerhafte leere Werte
					.replace(": ,", ": null") // Falls letztes Element betroffen ist
					
// JSON parsen (nach der Korrektur)
	//jsonObject = new groovy.json.JsonSlurper().parseText(inputJson)
	//println(jsonObject)

// JSON parsen
jsonObject = jsonSlurper.parseText(inputJson)
println(jsonObject)
  
// Extrahiere die Key-Value-Paare
def extractedData = extractValues(jsonObject)
 
// Ausgabe als Tabelle
println "===================input Tabelle ========================="
println "| Parameter                              | Value         |"
println "|----------------------------------------|---------------|"
extractedData.each { key, value ->
	println "| ${key.padRight(38)} | ${value.toString().padRight(12)} |"
}
println "=========================================================="
 
// Umwandlung zurück in JSON
def reconstructedJson = [:]
 
extractedData.each { key, value ->
	if (key == null || key.trim().isEmpty()) {
		println("⚠️ WARNUNG: Leerer oder null-Key erkannt!")
		return
	}	
	
	def keys = key.split("\\.").toList()
	if (keys.isEmpty()) {
		println("⚠️ WARNUNG: `split()` hat eine leere Liste zurückgegeben für key: ${key}")
		return
	}
	
	buildNestedMap(reconstructedJson, keys.toList(), value)
}
 
println("✅ Rekonstruiertes JSON: " + reconstructedJson)
 
 
// Speichern als JSON-Datei
inputJson = JsonOutput.prettyPrint(JsonOutput.toJson(reconstructedJson))
println "========================== inputJson ========================="
println inputJson
println "|------------------------------------------------------------|"
 
def matcher
 
String outputValue2
 
String outputString
 
String outputJson
 
 
// Schleife über alle Suchwerte  in der Ausgabe
for (int searchIndex = 0; searchIndex < searchValues.size(); searchIndex++) {
    String currentSearchValue = searchValues[searchIndex]
 
    String newValueForThirdColumn = newValuesForThirdColumn[searchIndex]
 
    // Schleife über alle Eingabefelder in der ersten Spalte
    for (int i = 0; i < firstColumnOutputs.size(); i++) {
        // Hole den Wert des aktuellen Output-Felds
        String outputValue = (firstColumnOutputs[i]).getText()
 
        // Überprüfen, ob der Wert dem aktuellen Suchwert entspricht (z.B. 'street', 'housenumber', 'city')
        // XPath für das Eingabefeld in der dritten Spalte der passenden Zeile
        // Erstelle ein dynamisches TestObject für die dritte Spalte
        // Beende die Schleife, da der Wert gefunden und aktualisiert wurde
        if (outputValue.equals(currentSearchValue)) {
            String xpathThirdColumnOutput = ('//span[contains(text(), "Output")]/ancestor::div[3]//tbody//tr[' + (i + 1)) +
            ']/td[3]'
 
            TestObject thirdColumnOutput = new TestObject()
 
            thirdColumnOutput.addProperty('xpath', ConditionType.EQUALS, xpathThirdColumnOutput)
 
            outputValue2 = WebUI.findWebElement(thirdColumnOutput).getText()
			outputString = outputValue2
			// DelegateID extrahieren
			matcher = (outputString =~ '\\{(\\d+)=\\{')
 
			delegateID = matcher.find() ? matcher.group(1) : null
			//outputString= outputString.replace("{"+delegateID+"=","")
			
			println("DelegateID: $delegateID")
			
			// Original JSON-String
			outputJson = outputString
			
			println(outputString)
			println(outputJson)
					
			// JSON-String korrigieren
			String correctedJson = outputString.replaceAll("(\\d+)=\\{", '"$1": {')       // Zahlenschlüssel als Strings
											    .replaceAll("(\\w+)=", '"$1": ')         // Schlüssel als Strings
											    .replaceAll(": ([^\"{}\\[\\d][^,}\\]]+)", ': "$1"')  // Werte ohne Anführungszeichen in Strings umwandeln
											    .replaceAll(": (\\d+)\\b", ': $1')        // Zahlenwerte korrekt setzen
											    .replaceAll(": \"(null)\"", ': null')     // "null" als echten null-Wert setzen
											    .replaceAll(": ,", ': null,')           // Fehlende Werte durch null ersetzen
											    .replaceAll(": }", ': null }')          // Falls letztes Element betroffen ist
											    .replace(", }", "}")                    // Leere Objekte korrigieren
											    .replace(",]", "]")                     // Leere Arrays korrigieren
											    .replaceAll(": (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z)", ': "$1"') // Datumswerte als Strings setzen
											    .replaceAll("\"entityGroupUUID\": ([^\",}{\\]]+)", '"entityGroupUUID": "$1"')  // UUID als String
											    .replaceAll("\"linkTypeTechName\": ([^\",}{\\]]+)", '"linkTypeTechName": "$1"') // Strings sichern
											    .replaceAll("\"relationEntity\": ([^\",}{\\]]+)", '"relationEntity": "$1"') // Strings sichern
								 
// Korrektur für spezifische Werte in fieldValue
correctedJson = correctedJson.replaceAll(
    " Assess the organization's technology infrastructure and implement upgrades or enhancements to improve efficiency\", productivity, and cybersecurity. This might involve upgrading software systems, implementing cloud solutions, or enhancing network security measures.,",
    " Assess the organization's technology infrastructure and implement upgrades or enhancements to improve efficiency, productivity, and cybersecurity. This might involve upgrading software systems, implementing cloud solutions, or enhancing network security measures.\","
)
 
println("Korrigiertes JSON:")
println(correctedJson)
 
// JSON parsen
jsonSlurper = new groovy.json.JsonSlurper()
jsonObject = jsonSlurper.parseText(correctedJson)
 
println("Parsed JSON Object:")
println(jsonObject)
			// JSON schön formatieren
			outputJson = JsonOutput.prettyPrint(JsonOutput.toJson(jsonObject))
			println("Finales JSON:")
			println(outputJson)
			
			
			// Extrahiere die Key-Value-Paare
			extractedData = extractValues(jsonObject)
			
			// Ausgabe als Tabelle
			println "================= Output Tabelle ========================="
			println "| Parameter                              | Value         |"
			println "|----------------------------------------|---------------|"
			extractedData.each { key, value ->
				println "| ${key.padRight(38)} | ${value.toString().padRight(12)} |"
			}
			println "=========================================================="
						
			println(inputJson)
			
			println(outputJson)
            break
        }
    }
}
def inputJsonObjekt = jsonSlurper.parseText(inputJson)
def outputJsonObjekt = jsonSlurper.parseText(outputJson)
  
// Sicherstellen, dass inputJson tatsächlich ein JSON-Objekt ist
if (!(inputJsonObjekt instanceof Map)) {
    KeywordUtil.markWarning("inputJsonObjekt wurde nicht als Map geparst!")
}
if (!(inputJsonObjekt.variables instanceof Map)) {
    KeywordUtil.markWarning("inputJsonObjekt.variables ist keine Map, sondern ${inputJsonObjekt.variables.getClass()}")
}
 
// Sicherstellen, dass outputJson tatsächlich ein JSON-Objekt ist
if (!(outputJsonObjekt instanceof Map)) {
    KeywordUtil.markWarning("outputJsonObjekt wurde nicht als Map geparst!")
}
 
if (!outputJsonObjekt.containsKey(delegateID)) {
    KeywordUtil.markFailed("Fehler: delegateID '$delegateID' nicht in outputJsonObjekt gefunden!")
    return false
}
 
// **Handling für den Vergleich**: Falls `outputJsonObjekt[delegateID]` eine Liste ist, wird die erste Map aus der Liste verwendet.
def outputData = outputJsonObjekt[delegateID]
if (outputData instanceof List) {
    outputData = outputData.find { it instanceof Map } // Nimm die erste Map aus der Liste
}
 
if (compareJson(inputJsonObjekt.variables, outputData)) {
    KeywordUtil.markPassed("Alle Werte sind enthalten und korrekt!")
} else {
    KeywordUtil.markFailed("Abweichungen in der JSON-Struktur gefunden!")
}
 
WebUI.navigateToUrl('https://po-client-dev-core-qa.services.fintus.io/')
 
GlobalVariable.OfferDraftFromExistingDataDelegateID = delegateID
 
WebUI.comment(GlobalVariable.OfferDraftFromExistingDataDelegateID)
 
WebUI.verifyNotEqual(findTestObject(GlobalVariable.OfferDraftFromExistingDataDelegateID), 'null')
 
//WebUI.comment(NeuerWertOfferDraftValues)
//WebUI.closeBrowser()

AFTER:

import static com.kms.katalon.core.checkpoint.CheckpointFactory.findCheckpoint
import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase
import static com.kms.katalon.core.testdata.TestDataFactory.findTestData
import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject
import static com.kms.katalon.core.testobject.ObjectRepository.findWindowsObject
import com.github.javafaker.Faker as Faker
import com.kms.katalon.core.checkpoint.Checkpoint as Checkpoint
import com.kms.katalon.core.cucumber.keyword.CucumberBuiltinKeywords as CucumberKW
import com.kms.katalon.core.mobile.keyword.MobileBuiltInKeywords as Mobile
import com.kms.katalon.core.model.FailureHandling as FailureHandling
import com.kms.katalon.core.testcase.TestCase as TestCase
import com.kms.katalon.core.testdata.TestData as TestData
import com.kms.katalon.core.testng.keyword.TestNGBuiltinKeywords as TestNGKW
import com.kms.katalon.core.testobject.TestObject as TestObject
import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.kms.katalon.core.windows.keyword.WindowsBuiltinKeywords as Windows
import internal.GlobalVariable as GlobalVariable
import org.openqa.selenium.Keys as Keys
import org.openqa.selenium.WebElement as WebElement
import com.kms.katalon.core.testobject.ConditionType as ConditionType
import com.kms.katalon.core.util.KeywordUtil as KeywordUtil
import groovy.json.JsonSlurper as JsonSlurper
import groovy.json.JsonOutput as JsonOutput

WebUI.openBrowser('')

WebUI.callTestCase(findTestCase('workplace/TC- Generell Steps/Step001 - Login TA1 - Natural'), [:], FailureHandling.STOP_ON_FAILURE)

WebUI.navigateToUrl(GlobalVariable.poURL)

WebUI.waitForElementPresent(findTestObject('Page_Process Orchestrator/input_PO_Search'), 0)

WebUI.setText(findTestObject('Page_Process Orchestrator/input_PO_Search'), selectDeleagate)

WebUI.sendKeys(findTestObject('Page_Process Orchestrator/input_PO_Search'), Keys.chord(Keys.ENTER))

WebUI.delay(2)

WebUI.waitForElementPresent(findTestObject('Page_Process Orchestrator/table_Delegate', [('Delegate') : selectDeleagate]), 
    0)

WebUI.clickOffset(findTestObject('Page_Process Orchestrator/table_Delegate', [('Delegate') : selectDeleagate]), 0, 0)

WebUI.click(findTestObject('Page_Process Orchestrator/button Open DetailsSectionAccessButton'))

WebUI.waitForElementPresent(findTestObject('Object Repository/Page_Process Orchestrator/span_BPMN'), 0)

WebUI.click(findTestObject('Page_Process Orchestrator/span_Test'))

WebUI.waitForElementPresent(findTestObject('Object Repository/Page_Process Orchestrator/span_Input'), 0)

// Initialisiere Faker für generierte Testdaten
Faker faker = new Faker(new Locale('de'))

// XPath für alle Eingabefelder/Ausgabefelder in der ersten Spalte
String xpathFirstColumnInputs = '//span[contains(text(), "Input")]/ancestor::div[3]//tbody//tr/td[1]//input'

String xpathFirstColumnOutputs = '//span[contains(text(), "Output")]/ancestor::div[3]//tbody//td[1]'

// Erstelle ein dynamisches TestObject für die Eingabefelder/Ausgabefelder der ersten Spalte
TestObject dynamicFirstColumnInputs = new TestObject()

TestObject dynamicFirstColumnOutputs = new TestObject()

dynamicFirstColumnInputs.addProperty('xpath', ConditionType.EQUALS, xpathFirstColumnInputs)

dynamicFirstColumnOutputs.addProperty('xpath', ConditionType.EQUALS, xpathFirstColumnOutputs)

// Sammle alle WebElements der Eingabefelder in der ersten Spalte
List<String> firstColumnInputs = WebUI.findWebElements(dynamicFirstColumnInputs, 30)

//############################################################################
//
// Dieser Bereich muss für jedes Deligate angepasst werden
//
//############################################################################
// Liste der Suchwerte für Spalte 1 (Pflichtfelder)
List<String> searchValues = ['offerDrafts']

String id = 'null'

// Generierte Testdaten mit Faker
String NeuerWertLegalPersonID

String NeuerWertOfferId

if (GlobalVariable.OfferDelegateID == 'null') {
    NeuerWertOfferId = '133901'
} else {
    NeuerWertOfferId = GlobalVariable.OfferDelegateID
}

String NeuerWertProjectID

if (GlobalVariable.ProjectDelegateID == 'null') {
    NeuerWertProjectID = '53108'
} else {
    NeuerWertProjectID = GlobalVariable.ProjectDelegateID
}

if (GlobalVariable.LegalPersonDelegateID == 'null') {
    NeuerWertLegalPersonID = '61309'
} else {
    NeuerWertLegalPersonID = GlobalVariable.LegalPersonDelegateID
}

String NeuerWertOfferDraftValues = (((((((((((((((((((((((('[{"variables":{' + '"offerId":{"value":"') + NeuerWertOfferId) + 
'"},') + '"entityName":{"value":"Project"},') + '"formTechnicalName":{"value":"offer_project_to_legal"},') + '"entityId":{"value":') + 
NeuerWertProjectID) + '},') + '"entityType":{"value": "Project"},') + '"offerDraftLinks": {') + '"value":[{"linkTypeTechName": null,') + 
'"children":[{"offerId":"') + NeuerWertOfferId) + '",') + '"entityName":"LegalPerson",') + '"formTechnicalName":"offer_project_to_legal",') + 
'"entityType":"LegalPerson",') + '"entityId": ') + NeuerWertLegalPersonID) + ',') + '"offerDraftLinks":[{"linkTypeTechName":"project_2_legal_person",') + 
'"children": [],') + '"existingLinkId":null}]}],') + '"existingLinkId":null}]}},') + '"withVariablesInReturn":true}]'

// Liste der neuen Werte, die in die dritte Spalte geschrieben werden sollen (Strings)
List<String> newValuesForThirdColumn = [NeuerWertOfferDraftValues]

//############################################################################
//
// Fertig ;)
//
//############################################################################
// Liste zur Speicherung der gesetzten Werte in der dritten Spalte
List<String> setThirdColumnValues = []

// Schleife über alle Suchwerte
for (int searchIndex = 0; searchIndex < searchValues.size(); searchIndex++) {
    String currentSearchValue = searchValues[searchIndex]

    String newValueForThirdColumn = newValuesForThirdColumn[searchIndex]

    boolean valueFound = false

    // Schleife über alle Eingabefelder in der ersten Spalte
    for (int i = 0; i < firstColumnInputs.size(); i++) {
        // Hole den Wert des aktuellen Input-Felds
        String inputValue = (firstColumnInputs[i]).getAttribute('value')

        // Überprüfen, ob der Wert dem aktuellen Suchwert entspricht (z.B. 'street', 'housenumber', 'city')
        if (inputValue.equals(currentSearchValue)) {
            valueFound = true

            // XPath für das Eingabefeld in der dritten Spalte der passenden Zeile
            String xpathThirdColumnInput = ('//tbody//tr[' + (i + 1)) + ']/td[3]//input'

            // Erstelle ein dynamisches TestObject für die dritte Spalte
            TestObject thirdColumnInput = new TestObject()

            thirdColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathThirdColumnInput)

            // Setze den neuen Wert in die dritte Spalte
            WebUI.setText(thirdColumnInput, newValueForThirdColumn)

            // Speichere den gesetzten Wert zur späteren Überprüfung
            setThirdColumnValues.add(newValueForThirdColumn)

            // Beende die Schleife, da der Wert gefunden und aktualisiert wurde
            break
        }
    }
    
    // Wenn der Wert nicht gefunden wurde, füge eine neue Zeile hinzu
    if (!(valueFound)) {
        // Klicke auf die Schaltfläche "Field hinzufügen", um eine neue Zeile hinzuzufügen
        WebUI.click(findTestObject('Object Repository/Page_Process Orchestrator/a_Field hinzufugen'))

        // Warte kurz, bis die neue Zeile hinzugefügt wurde (Wartezeit anpassen, falls nötig)
        WebUI.delay(2)

        // Aktualisiere die Liste der Eingabefelder in der ersten Spalte (nachdem eine neue Zeile hinzugefügt wurde)
        firstColumnInputs = WebUI.findWebElements(dynamicFirstColumnInputs, 30)

        // XPath für das neue Eingabefeld in der ersten Spalte (letzte Zeile)
        String xpathNewFirstColumnInput = ('//tbody//tr[' + firstColumnInputs.size()) + ']/td[1]//input'

        // Erstelle ein dynamisches TestObject für das Eingabefeld in der neuen Zeile, Spalte 1
        TestObject newFirstColumnInput = new TestObject()

        newFirstColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathNewFirstColumnInput)

        // Setze den aktuellen Suchwert (z.B. 'street', 'housenumber') in die erste Spalte der neuen Zeile
        WebUI.setText(newFirstColumnInput, currentSearchValue)

        // XPath für das Eingabefeld in der dritten Spalte der neuen Zeile
        String xpathNewThirdColumnInput = ('//tbody//tr[' + firstColumnInputs.size()) + ']/td[3]//input'

        // Erstelle ein dynamisches TestObject für das Eingabefeld in der dritten Spalte der neuen Zeile
        TestObject newThirdColumnInput = new TestObject()

        newThirdColumnInput.addProperty('xpath', ConditionType.EQUALS, xpathNewThirdColumnInput)

        // Setze den neuen Wert in die dritte Spalte der neuen Zeile
        WebUI.setText(newThirdColumnInput, newValueForThirdColumn)

        // Speichere den gesetzten Wert zur späteren Überprüfung
        setThirdColumnValues.add(newValueForThirdColumn)
    }
}

// Klicke auf den "Test" Button, um die Ausgabewerte zu generieren
WebUI.click(findTestObject('Page_Process Orchestrator/button_Speichern'))

WebUI.click(findTestObject('Object Repository/Page_Process Orchestrator/button_Test'))

// Warte, bis die Ausgabewerte vollständig geladen sind (anpassen, falls nötig)
WebUI.delay(5 // Anpassung je nach Ladezeit
    )

// Aktualisiere die Liste der Ausgabefelder in der ersten Spalte (nachdem der Button geklickt wurde)
List<String> firstColumnOutputs = WebUI.findWebElements(dynamicFirstColumnOutputs, 30)

searchValues.add('id')

String delegateID

inputString = NeuerWertOfferDraftValues.replace('[', '').replace(']', '')

// Original JSON-String
String inputJson = inputString

println(inputString)

println(inputJson)

def jsonSlurper = new JsonSlurper()

// Korrigiert leere Objekte
// Korrigiert leere Arrays
// Setzt null für fehlerhafte leere Werte
// Falls letztes Element betroffen ist
inputJson = inputJson.replace(', }', '}').replace(',]', ']').replace(': ,', ': null,').replace(': ,', ': null')

// JSON parsen
jsonObject = jsonSlurper.parseText(inputJson)

//println(jsonObject)
println(jsonObject)

//##############################################################################
// Funktion zur Extraktion der Werte als Key-Value-Paare
// Funktion zum Reconstruck Json
// Rekursive Vergleichsfunktion
// Wenn input eine Map ist
// Wenn output keine Map ist, vergleiche die Werte direkt
// Wenn input eine Liste ist
// Wenn output keine Liste ist, vergleiche die Werte direkt
// Wenn input ein primitiver Wert ist (String, Zahl, Boolean usw.)
// Sicherstellen, dass Zahlen als String verglichen werden
//############################################################################## 
// Extrahiere die Key-Value-Paare
def extractedData = extractValues(jsonObject)

// Ausgabe als Tabelle
println('===================input Tabelle =========================')

println('| Parameter                              | Value         |')

println('|----------------------------------------|---------------|')

extractedData.each({ def key, def value ->
        println("| $key.padRight(38) | $value.toString().padRight(12) |")
    })

println('==========================================================')

// Umwandlung zurück in JSON
def reconstructedJson = [:]

extractedData.each({ def key, def value ->
        if ((key == null) || key.trim().isEmpty()) {
            println('⚠️ WARNUNG: Leerer oder null-Key erkannt!')

            return null
        }
        
        def keys = key.split('\\.').toList()

        if (keys.isEmpty()) {
            println("⚠️ WARNUNG: `split()` hat eine leere Liste zurückgegeben für key: $key")

            return null
        }
        
        buildNestedMap(reconstructedJson, keys.toList(), value)
    })

println('✅ Rekonstruiertes JSON: ' + reconstructedJson)

// Speichern als JSON-Datei
inputJson = JsonOutput.prettyPrint(JsonOutput.toJson(reconstructedJson))

println('========================== inputJson =========================')

println(inputJson)

println('|------------------------------------------------------------|')

def matcher

String outputValue2

String outputString

String outputJson

// Schleife über alle Suchwerte  in der Ausgabe
for (int searchIndex = 0; searchIndex < searchValues.size(); searchIndex++) {
    String currentSearchValue = searchValues[searchIndex]

    String newValueForThirdColumn = newValuesForThirdColumn[searchIndex]

    // Schleife über alle Eingabefelder in der ersten Spalte
    for (int i = 0; i < firstColumnOutputs.size(); i++) {
        // Hole den Wert des aktuellen Output-Felds
        String outputValue = (firstColumnOutputs[i]).getText()

        // Überprüfen, ob der Wert dem aktuellen Suchwert entspricht (z.B. 'street', 'housenumber', 'city')
        // XPath für das Eingabefeld in der dritten Spalte der passenden Zeile
        // Erstelle ein dynamisches TestObject für die dritte Spalte
        // Beende die Schleife, da der Wert gefunden und aktualisiert wurde
        if (outputValue.equals(currentSearchValue)) {
            String xpathThirdColumnOutput = ('//span[contains(text(), "Output")]/ancestor::div[3]//tbody//tr[' + (i + 1)) + 
            ']/td[3]'

            TestObject thirdColumnOutput = new TestObject()

            thirdColumnOutput.addProperty('xpath', ConditionType.EQUALS, xpathThirdColumnOutput)

            outputValue2 = WebUI.findWebElement(thirdColumnOutput).getText()

            outputString = outputValue2

            // DelegateID extrahieren
            matcher = (outputString =~ '\\{(\\d+)=\\{')

            delegateID = matcher.find() ? matcher.group(1) : null

            //outputString= outputString.replace("{"+delegateID+"=","")
            println("DelegateID: $delegateID")

            // Original JSON-String
            outputJson = outputString

            println(outputString)

            println(outputJson)

            // JSON-String korrigieren
            String correctedJson = outputString.replaceAll('(\\d+)=\\{', '"$1": {' // Zahlenschlüssel als Strings
                ).replaceAll('(\\w+)=', '"$1": ' // Schlüssel als Strings
                ).replaceAll(': ([^"{}\\[\\d][^,}\\]]+)', ': "$1"' // Werte ohne Anführungszeichen in Strings umwandeln
                ).replaceAll(': (\\d+)\\b', ': $1' // Zahlenwerte korrekt setzen
                ).replaceAll(': "(null)"', ': null' // "null" als echten null-Wert setzen
                ).replaceAll(': ,', ': null,' // Fehlende Werte durch null ersetzen
                ).replaceAll(': }', ': null }' // Falls letztes Element betroffen ist
                ).replace(', }', '}' // Leere Objekte korrigieren
                ).replace(',]', ']' // Leere Arrays korrigieren
                ).replaceAll(': (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z)', ': "$1"' // Datumswerte als Strings setzen
                ).replaceAll('"entityGroupUUID": ([^",}{\\]]+)', '"entityGroupUUID": "$1"' // UUID als String
                ).replaceAll('"linkTypeTechName": ([^",}{\\]]+)', '"linkTypeTechName": "$1"' // Strings sichern
                ).replaceAll('"relationEntity": ([^",}{\\]]+)', '"relationEntity": "$1"' // Strings sichern
                ).replaceAll('"fieldValue": ",', '"fieldValue": null,').replaceAll('"fieldType": CUSTOM"', '"fieldType": "CUSTOM"')

            if (GlobalVariable.LegalPersonDelegateName == 'null') {
                println('Debug Run') // Dynamischer Teil mit Regex erfassen (alles zwischen "LegalPerson " und ', "fieldType":')
            } else {
                correctedJson = correctedJson.replaceAll('("fieldValue": "LegalPerson )(.*?)(", "fieldType":)', ('$1' + 
                    GlobalVariable.LegalPersonDelegateName) + '$3')
            }
            
            // Korrektur für spezifische Werte in fieldValue
            correctedJson = correctedJson.replaceAll(' Assess the organization\'s technology infrastructure and implement upgrades or enhancements to improve efficiency", productivity, and cybersecurity. This might involve upgrading software systems, implementing cloud solutions, or enhancing network security measures.,', 
                ' Assess the organization\'s technology infrastructure and implement upgrades or enhancements to improve efficiency, productivity, and cybersecurity. This might involve upgrading software systems, implementing cloud solutions, or enhancing network security measures.",')

            println('Korrigiertes JSON:')

            println(correctedJson)

            // JSON parsen
            jsonSlurper = new JsonSlurper()

            jsonObject = jsonSlurper.parseText(correctedJson)

            println('Parsed JSON Object:')

            println(jsonObject)

            // JSON schön formatieren
            outputJson = JsonOutput.prettyPrint(JsonOutput.toJson(jsonObject))

            println('Finales JSON:')

            println(outputJson)

            // Extrahiere die Key-Value-Paare
            extractedData = extractValues(jsonObject)

            // Ausgabe als Tabelle
            println('================= Output Tabelle =========================')

            println('| Parameter                              | Value         |')

            println('|----------------------------------------|---------------|')

            extractedData.each({ def key, def value ->
                    println("| $key.padRight(38) | $value.toString().padRight(12) |")
                })

            println('==========================================================')

            println(inputJson)

            println(outputJson)

            break
        }
    }
}

def inputJsonObjekt = jsonSlurper.parseText(inputJson)

def outputJsonObjekt = jsonSlurper.parseText(outputJson)

// Sicherstellen, dass inputJson tatsächlich ein JSON-Objekt ist
if (!(inputJsonObjekt instanceof Map)) {
    KeywordUtil.markWarning('inputJsonObjekt wurde nicht als Map geparst!')
}

if (!(inputJsonObjekt.variables instanceof Map)) {
    KeywordUtil.markWarning("inputJsonObjekt.variables ist keine Map, sondern $inputJsonObjekt.variables.getClass()")
}

// Sicherstellen, dass outputJson tatsächlich ein JSON-Objekt ist
if (!(outputJsonObjekt instanceof Map)) {
    KeywordUtil.markWarning('outputJsonObjekt wurde nicht als Map geparst!')
}

if (!(outputJsonObjekt.containsKey(delegateID))) {
    KeywordUtil.markFailed("Fehler: delegateID '$delegateID' nicht in outputJsonObjekt gefunden!")

    return false
}

// **Handling für den Vergleich**: Falls `outputJsonObjekt[delegateID]` eine Liste ist, wird die erste Map aus der Liste verwendet.
def outputData = outputJsonObjekt[delegateID]

if (outputData instanceof List) {
    outputData = outputData.find({ 
            it instanceof Map // Nimm die erste Map aus der Liste
        })
}

if (compareJson(inputJsonObjekt.variables, outputData)) {
    KeywordUtil.markPassed('Alle Werte sind enthalten und korrekt!')
} else {
    KeywordUtil.markFailed('Abweichungen in der JSON-Struktur gefunden!')
}

WebUI.navigateToUrl('https://po-client-dev-core-qa.services.fintus.io/')

GlobalVariable.OfferDraftFromExistingDataDelegateID = delegateID

newValuesForThirdColumn.add(delegateID)

GlobalVariable.OfferDraftFromExistingDataDelegateSearchList = searchValues

GlobalVariable.OfferDraftFromExistingDataDelegateResultList = newValuesForThirdColumn

WebUI.comment(GlobalVariable.OfferDraftFromExistingDataDelegateSearchList.toString())

WebUI.comment(GlobalVariable.OfferDraftFromExistingDataDelegateResultList.toString())

WebUI.comment(GlobalVariable.OfferDraftFromExistingDataDelegateID)

WebUI.verifyNotEqual(findTestObject(GlobalVariable.OfferDraftFromExistingDataDelegateID), 'null')

not_run: WebUI.closeBrowser()

def extractValues(Map jsonMap, String parentKey = '') {
    def result = [:]

    jsonMap.each({ def key, def value ->
            def newKey = parentKey ? "$parentKey.$key" : key

            if ((value instanceof Map) && value.containsKey('value')) {
                (result[newKey]) = value.value
            } else if (value instanceof Map) {
                result.putAll(extractValues(value, newKey))
            } else {
                (result[newKey]) = value
            }
        })

    return result
}

void buildNestedMap(Map target, List<String> keys, def value) {
    if (keys.size() == 1) {
        (target[(keys[0])]) = value

        return null
    }
    
    if (!(target.containsKey(keys[0])) || !((target[(keys[0])]) instanceof Map)) {
        (target[(keys[0])]) = [:]
    }
    
    buildNestedMap(target[(keys[0])], keys[(1..-1)], value)
}

boolean compareJson(def input, def output) {
    if ((input == null) || (output == null)) {
        if (input != output) {
            KeywordUtil.markWarning("Fehler: Null-Werte stimmen nicht überein. Erwartet: $input, Gefunden: $output")

            return false
        }
        
        return true
    }
    
    if (input instanceof Map) {
        if (!(output instanceof Map)) {
            if (input != output) {
                WebUI.comment("Fehler: Erwartete Map oder gleichen Wert, aber gefunden: $output.getClass() mit Wert $output")

                return false
            }
            
            return true
        }
        
        input.each({ def key, def value ->
                if (!(output.containsKey(key))) {
                    WebUI.comment("Fehlender Schlüssel in Output: $key")

                    return false
                }
                
                if (!(compareJson(value, output[key]))) {
                    return false
                }
            })
    } else if (input instanceof List) {
        if (!(output instanceof List)) {
            if (input != output) {
                WebUI.comment("Fehler: Erwartete Liste oder gleichen Wert, aber gefunden: $output.getClass() mit Wert $output")

                return false
            }
            
            return true
        }
        
        if (input.size() > output.size()) {
            WebUI.comment("Listenlängen stimmen nicht überein: Erwartet mindestens $input.size(), aber gefunden $output.size()")

            return false
        }
        
        input.eachWithIndex({ def item, def index ->
                if (!(compareJson(item, output[index]))) {
                    WebUI.comment("Listenwert an Index $index stimmt nicht überein: Erwartet $item, aber gefunden $output[index]")

                    return false
                }
            })
    } else {
        def inputNormalized = input instanceof Number ? input.toString() : input

        def outputNormalized = output instanceof Number ? output.toString() : output

        if (inputNormalized != outputNormalized) {
            WebUI.comment("Wert stimmt nicht überein: Erwartet $inputNormalized, aber gefunden $outputNormalized")

            return false
        }
    }
    
    return true
}
1 Like

I suppose, you primarily use the Script view of Test Case editor. Then you should NEVER switch to the Manual view.

When you switch the Test Case editor from the Script view to the Manual view, Katalon Studio will parse your code into an Abstract Syntax Tree object. When you change your test, Katalon Studio will update the AST object. When you turn the editor back to the Script view, KS will serialize the AST object into a Groovy source. The source will be formatted as KS thinks appropriate. KS does not preserve your original source format. Consequently the generated code could become different from what you previously had.

In worst cases, the generated code could get broken. See also

This bug is known for more than 8 years; left unfixed.

How to workround? — Don’t switch the Test Case Editor : Script > Manual > Script.

normaly the test are all in manuel mode only this test need a specical logic to pars the output in a way to validate the input to the output. any idee how i could change the code that it don’t change?

No way to stop autoformatting.


Then you can edit this test using only the Script view; you should NOT switch this test to the Manual view and make any change.

Reformatting is done file-by-file. A source file is reformatted when you make any change in the Manual view. If you do not change the test, the source won’t be reformatted.

Please examine:

  1. When you switch a test script from the Script view to the Manual view, make no change, and switch back to the Script view, then you will find no re-formatting of the source will be made.

  2. When you switch a test script from the Script view to the Manual view, make some change + save it, and switch back to the Script view, then will find the source is reformattend.

Please note that this way leaves a risk that you forget this CAUSION. In future, you might carelessly do the Script => Manual => Script switching and break the source.

Do you want to continue using the Manual view of Test Case editor mainly?

OK, then, you should change your test case script.

I think that the Manual view is designed with an assumption that a test case script comprises with a sequence of simple one-liners only. The Manual view does not expect any complex def function(...) {...} statements as you did in the BEFORE:

def extractValues(Map jsonMap, String parentKey = "") {
	...
}

// Funktion zum Reconstruck Json
void buildNestedMap(Map target, List<String> keys, def value) {
	...
}

// Rekursive Vergleichsfunktion
boolean compareJson(def input, def output) {
	..
}

As you noticed, these function declarations in the BEFORE were completely reformated (removed?) in the AFTER. This proves that Katalon Studio is NOT implemented so that it preserve the original source you manually wrote.


How to minimize the risk of problems caused by the autoformatting?

You should refactor “this test” into two modules:

  1. a new Groovy class which implements your business logic
  2. a Test Case that uses the new class

You want to encapsulate “the special logic” into a Groovy class and move the class into another file outside the Test Case script.

You can place it under the Keywords folder or the Include/source/groovy folder of your Katalon project. Possibly you would want to revise the souce code of the class repeatedly. You can safely do it using the plain text editor for Keywords or Include/source/groovy. The plain text editor never performs troublesome autoformatting.

Even more drastically, if you want to, you can develop the class outside your katalon project. You want to make a jar which contains the class files. And you want to import the jar file into the Drivers folder of your katalon project. Then the class will become accessible for your Test Cases just the same as Groovy classes in the Keywords folder.

Isolating your souce of complex business logic out of Test Case would eliminate the risk of corruption.

See Katalon’s docs how to develop Groovy classes in the Keywords folder:


You can add any type of custom Groovy classes under the Include/source/groovy folder. See Katalons doc:


You can add any type of custom Groovy classes under the Keywords folder or the Include/source/groovy folder. These 2 folders has no difference as a location to save the source of your custom Groovy classes: See the following post for further explanation:

Let me suggest a small tip of Groovy coding.

You had this statement in the BEFORE.

String NeuerWertOfferDraftValues = "[{\"variables\":{"+
									"\"offerId\":{\"value\":\""+NeuerWertOfferId+"\"},"+
									"\"entityName\":{\"value\":\"Project\"},"+
									"\"formTechnicalName\":{\"value\":\"offer_project_to_legal\"},"+
									"\"entityId\":{\"value\":"+NeuerWertProjectID+"},"+
									"\"entityType\":{\"value\": \"Project\"},"+
									"\"offerDraftLinks\": {"+
									   "\"value\":[{\"linkTypeTechName\": null,"+
										   "\"children\":[{\"offerId\":\""+NeuerWertOfferId+"\","+
											   "\"entityName\":\"LegalPerson\","+
											   "\"formTechnicalName\":\"offer_project_to_legal\","+
											   "\"entityType\":\"LegalPerson\","+
											   "\"entityId\": "+NeuerWertLegalPersonID+","+
											   "\"offerDraftLinks\":[{\"linkTypeTechName\":\"project_2_legal_person\","+
												   "\"children\": [],"+
												   "\"existingLinkId\":null}]}],"+
										   "\"existingLinkId\":null}]}},"+
									"\"withVariablesInReturn\":true}]"

This string declaration is too noisy.

Alternatively you can write a JSON string in a triple double quoted GString of Groovy language with string interpolation by ${...} , like this:

String NeuerWertOfferId = 'foo'

String NeuerWertProjectID = 'bar'

String NeuerWertLegalPersonID = 'baz'

String NeuerWertOfferDraftValues = """
[
  {
    "variables": {
      "offerId": {
        "value": "${NeuerWertOfferId}"
      },
      "entityName": {
        "value": "Project"
      },
      "formTechnicalName": {
        "value": "offer_project_to_legal"
      },
      "entityId": {
        "value": "${NeuerWertProjectID}"
      },
      "entityType": {
        "value": "Project"
      },
      "offerDraftLinks": {
        "value": [
          {
            "linkTypeTechName": null,
            "children":[
              {
                "offerId": "${NeuerWertOfferId}",
                "entityName": "LegalPerson",
                "formTechnicalName": "offer_project_to_legal",
                "entityType": "LegalPerson",
                "entityId": "${NeuerWertLegalPersonID}",
                "offerDraftLinks": [
                  {
                    "linkTypeTechName": "project_2_legal_person",
                    "children": [],
                    "existingLinkId": null
                  }
                ]
              }
            ],
            "existingLinkId": null
          }
        ]
      }
    },
    "withVariablesInReturn":true
  }
]
"""

println NeuerWertOfferDraftValues

When I run this script, I got the following output in the console:

2025-06-19 08:33:07.179 INFO  c.k.katalon.core.main.TestCaseExecutor   - --------------------
2025-06-19 08:33:07.181 INFO  c.k.katalon.core.main.TestCaseExecutor   - START Test Cases/TC2

[
  {
    "variables": {
      "offerId": {
        "value": "foo"
      },
      "entityName": {
        "value": "Project"
      },
      "formTechnicalName": {
        "value": "offer_project_to_legal"
      },
      "entityId": {
        "value": "bar"
      },
      "entityType": {
        "value": "Project"
      },
      "offerDraftLinks": {
        "value": [
          {
            "linkTypeTechName": null,
            "children":[
              {
                "offerId": "foo",
                "entityName": "LegalPerson",
                "formTechnicalName": "offer_project_to_legal",
                "entityType": "LegalPerson",
                "entityId": "baz",
                "offerDraftLinks": [
                  {
                    "linkTypeTechName": "project_2_legal_person",
                    "children": [],
                    "existingLinkId": null
                  }
                ]
              }
            ],
            "existingLinkId": null
          }
        ]
      }
    },
    "withVariablesInReturn":true
  }
]

2025-06-19 08:33:07.567 INFO  c.k.katalon.core.main.TestCaseExecutor   - END Test Cases/TC2

This way is much easier to read and write, isn’t it?

1 Like

Unfortunately, Katalon Studio does not respect the “triple double quoted string” in a Test Case script. It dumbly autoformats the above code as this:

String NeuerWertOfferId = 'foo'

String NeuerWertProjectID = 'bar'

String NeuerWertOfferDraftValues = "
[
  {
    "variables": {
      "offerId": {
        "value": "${NeuerWertOfferId}"
      },
      "entityName": {
        "value": "Project"
      },
      "formTechnicalName": {
        "value": "offer_project_to_legal"
      },
      "entityId": {
        "value": "${NeuerWertProjectID}"
      },
      "entityType": {
        "value": "Project"
      },
      "offerDraftLinks": {
        "value": [
          {
            "linkTypeTechName": null,
            "children":[
              {
                "offerId": "${NeuerWertOfferId}",
                "entityName": "LegalPerson",
                "formTechnicalName": "offer_project_to_legal",
                "entityType": "LegalPerson",
                "entityId": "${NeuerWertLegalPersonID}",
                "offerDraftLinks": [
                  {
                    "linkTypeTechName": "project_2_legal_person",
                    "children": [],
                    "existingLinkId": null
                  }
                ]
              }
            ],
            "existingLinkId": null
          }
        ]
      }
    },
    "withVariablesInReturn":true
  }
]
"

Katalon Studio changed a triple double quotes """ into a single double quote ". Consequently this code results in a lot of Syntax Errors.

So the goodness of triple double quoted string is not applicable to Test Case scripts.

Let me repeat: If you want to use the Manual view of Test Case editor, you should keep your Test Cases dumb simple. Therefore you need to implement serious business logics outside the Test Case script.

@hendrik1

If you are going to develop a Groovy class that implments your business logic in the Keywords folder, I suppose you would want to perform “unit-testing” using some unit-testing framework such as JUnit.

In that case, the following post of mine would be interesting:

@hendrik1

Recently I did a study how to use IntelliJ IDEA together with Gradle to develop a custom Groovy library which is to be imported into a katalon project. See the following post:

Java/Groovy programming in IDEA is most comfortable for me.

I suppose you, @hendrik1, are a well-trained Java programmer, and you would feel like to do the same.