fix: allow setting empty strings with TestElement.set(...) API, and remove properties only in case the value is null
diff --git a/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt b/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
index d6cb9ec..0ec7924 100644
--- a/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
+++ b/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
@@ -285,13 +285,13 @@
         getPropertyOrNull(property)?.stringValue ?: property.defaultValueAsString ?: ""
 
     /**
-     * Set property as string, or remove it if the given value is `null` or empty.
+     * Set property as string, or remove it if the given value is `null`.
      * @since 5.6
      */
     @JMeterPropertySchemaUnchecked
     @API(status = API.Status.EXPERIMENTAL, since = "5.6")
     public operator fun set(property: PropertyDescriptor<*, *>, value: String?) {
-        removeOrSet(value.isNullOrEmpty(), property.name) {
+        removeOrSet(value == null, property.name) {
             StringProperty(it, value)
         }
     }
diff --git a/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/PropertiesAccessor.kt b/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/PropertiesAccessor.kt
index 05c6f0b..2f2f4a7 100644
--- a/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/PropertiesAccessor.kt
+++ b/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/PropertiesAccessor.kt
@@ -84,7 +84,7 @@
     }
 
     // All properties can be set as strings
-    public operator fun set(property: PropertyDescriptor<Schema, *>, value: String) {
+    public operator fun set(property: PropertyDescriptor<Schema, *>, value: String?) {
         target[property] = value
     }
 
@@ -116,13 +116,13 @@
     public inline operator fun get(propertySelector: Schema.() -> BooleanPropertyDescriptor<Schema>): Boolean =
         target[propertySelector(schema)]
 
-    public operator fun set(property: BooleanPropertyDescriptor<Schema>, value: Boolean) {
+    public operator fun set(property: BooleanPropertyDescriptor<Schema>, value: Boolean?) {
         target[property] = value
     }
 
     public inline operator fun set(
         propertySelector: Schema.() -> BooleanPropertyDescriptor<Schema>,
-        value: Boolean
+        value: Boolean?
     ) {
         target[propertySelector(schema)] = value
     }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/testelement/property/JMeterElementSchemaTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/testelement/property/JMeterElementSchemaTest.kt
index 58f6869..143a03d 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/testelement/property/JMeterElementSchemaTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/testelement/property/JMeterElementSchemaTest.kt
@@ -29,10 +29,14 @@
 import org.junit.jupiter.api.Test
 
 class JMeterElementSchemaTest {
+    val warpDrive = WarpDriveElement()
+
     abstract class WarpDriveElementSchema : TestElementSchema() {
         companion object INSTANCE : WarpDriveElementSchema()
 
         val warpFactor by int("WarpDriveElement.warpFactor", default = 7)
+        val turbo by boolean("WarpDriveElement.turbo")
+        val description by string("WarpDriveElement.description")
     }
 
     open class WarpDriveElement : AbstractTestElement() {
@@ -44,7 +48,16 @@
 
     @Test
     fun `getPropertyOrNull returns null for unset props`() {
-        val warpDrive = WarpDriveElement()
+        assertGetWarpDescription(
+            null,
+            warpDrive,
+            "${WarpDriveElementSchema.warpFactor} should be null for newly created element"
+        )
+        assertGetWarpTurbo(
+            null,
+            warpDrive,
+            "${WarpDriveElementSchema.warpFactor} should be null for newly created element"
+        )
         assertNull(warpDrive.getPropertyOrNull(warpDrive.schema.warpFactor)) {
             "${WarpDriveElementSchema.warpFactor} should be null for newly created element, getPropertyOrNull(PropertyDescriptor)"
         }
@@ -54,22 +67,19 @@
     }
 
     @Test
-    fun `get returns default value`() {
-        val warpDrive = WarpDriveElement()
+    fun `get int returns default value`() {
         assertGetWarpFactor(7, warpDrive, "element is empty, so default value expected")
     }
 
     @Test
-    fun `set modifies value`() {
-        val warpDrive = WarpDriveElement()
+    fun `set int modifies value`() {
         warpDrive[warpDrive.schema.warpFactor] = 8
 
         assertGetWarpFactor(8, warpDrive, "value was modified with [warpFactor] = 8")
     }
 
     @Test
-    fun `props set modifies value`() {
-        val warpDrive = WarpDriveElement()
+    fun `props set int modifies value`() {
         warpDrive.props {
             it[warpFactor] = 8
         }
@@ -78,6 +88,74 @@
     }
 
     @Test
+    fun `set string modifies value`() {
+        var value = "new description"
+        warpDrive[warpDrive.schema.description] = value
+        assertGetWarpDescription(value, warpDrive, "value was modified with [description] = \"$value\"")
+
+        value = ""
+        warpDrive[warpDrive.schema.description] = value
+        assertGetWarpDescription(value, warpDrive, "value was modified with [description] = \"$value\"")
+
+        warpDrive[warpDrive.schema.description] = null
+        assertGetWarpDescription(null, warpDrive, "value should be removed after [description] = null")
+    }
+
+    @Test
+    fun `props set string modifies value`() {
+        var value = "new description"
+        warpDrive.props {
+            it[description] = value
+        }
+        assertGetWarpDescription(value, warpDrive, "value was modified with props { it[description] = \"$value\" }")
+
+        value = ""
+        warpDrive.props {
+            it[description] = value
+        }
+        assertGetWarpDescription(value, warpDrive, "value was modified with props { it[description] = \"$value\" }")
+
+        warpDrive.props {
+            it[description] = null
+        }
+        assertGetWarpDescription(null, warpDrive, "value should be removed after props { it[description] = null }")
+    }
+
+    @Test
+    fun `set boolean modifies value`() {
+        var value = true
+        warpDrive[warpDrive.schema.turbo] = value
+        assertGetWarpTurbo(value, warpDrive, "value was modified with [turbo] = \"$value\"")
+
+        value = false
+        warpDrive[warpDrive.schema.turbo] = value
+        assertGetWarpTurbo(value, warpDrive, "value was modified with [turbo] = \"$value\"")
+
+        warpDrive[warpDrive.schema.turbo] = null as Boolean?
+        assertGetWarpTurbo(null, warpDrive, "value should be removed after [turbo] = null")
+    }
+
+    @Test
+    fun `props set boolean modifies value`() {
+        var value = true
+        warpDrive.props {
+            it[turbo] = value
+        }
+        assertGetWarpTurbo(value, warpDrive, "value was modified with props { it[turbo] = \"$value\" }")
+
+        value = false
+        warpDrive.props {
+            it[turbo] = value
+        }
+        assertGetWarpTurbo(value, warpDrive, "value was modified with props { it[turbo] = \"$value\" }")
+
+        warpDrive.props {
+            it[turbo] = null as Boolean?
+        }
+        assertGetWarpTurbo(null, warpDrive, "value should be removed after props { it[turbo] = null }")
+    }
+
+    @Test
     fun `property descriptor equals`() {
         assertEquals(TestElementSchema.name, ThreadGroupSchema.name) {
             "TestElementSchema.name and ThreadGroupSchema.name should be equal"
@@ -90,7 +168,6 @@
 
     @Test
     fun `test string setter`() {
-        val warpDrive = WarpDriveElement()
         warpDrive.props {
             it[warpFactor] = "\${hello}"
         }
@@ -103,7 +180,7 @@
         assertEquals(expected, warpDrive[warpDrive.schema.warpFactor]) {
             "get(warpFactor): ${WarpDriveElementSchema.warpFactor}, $message"
         }
-        assertEquals(expected, warpDrive.props[ { warpDrive.schema.warpFactor }]) {
+        assertEquals(expected, warpDrive.props[ { warpFactor }]) {
             "props.get[{warpFactor}]: ${WarpDriveElementSchema.warpFactor}, $message"
         }
         assertEquals(expected.toString(), warpDrive.getString(warpDrive.schema.warpFactor)) {
@@ -111,6 +188,52 @@
         }
     }
 
+    private fun assertGetWarpDescription(expected: String?, warpDrive: WarpDriveElement, message: String) {
+        assertEquals(expected ?: "", warpDrive[warpDrive.schema.description]) {
+            "get(description): ${WarpDriveElementSchema.description}, $message"
+        }
+        assertEquals(expected ?: "", warpDrive.props[ { description }]) {
+            "props.get[{description}]: ${WarpDriveElementSchema.description}, $message"
+        }
+        assertEquals(expected ?: "", warpDrive.getString(warpDrive.schema.description)) {
+            "getString(description): ${WarpDriveElementSchema.description}, $message"
+        }
+        assertEquals(expected ?: "", warpDrive.getPropertyAsString(warpDrive.schema.description.name)) {
+            "getPropertyAsString(description): ${WarpDriveElementSchema.description}, $message"
+        }
+        if (expected == null) {
+            assertNull(warpDrive.getPropertyOrNull(warpDrive.schema.description)) {
+                "getPropertyOrNull(description) should return null for absent property, ${WarpDriveElementSchema.description}, $message"
+            }
+            assertNull(warpDrive.getPropertyOrNull(warpDrive.schema.description.name)) {
+                "getPropertyOrNull(description.name) should return null for absent property, ${WarpDriveElementSchema.description}, $message"
+            }
+        }
+    }
+
+    private fun assertGetWarpTurbo(expected: Boolean?, warpDrive: WarpDriveElement, message: String) {
+        assertEquals(expected ?: false, warpDrive[warpDrive.schema.turbo]) {
+            "get(turbo): ${WarpDriveElementSchema.turbo}, $message"
+        }
+        assertEquals(expected ?: false, warpDrive.props[ { turbo }]) {
+            "props.get[{turbo}]: ${WarpDriveElementSchema.turbo}, $message"
+        }
+        assertEquals((expected ?: false).toString(), warpDrive.getString(warpDrive.schema.turbo)) {
+            "getString(turbo): ${WarpDriveElementSchema.turbo}, $message"
+        }
+        assertEquals(expected?.toString() ?: "", warpDrive.getPropertyAsString(warpDrive.schema.turbo.name)) {
+            "getPropertyAsString(turbo): ${WarpDriveElementSchema.turbo}, $message"
+        }
+        if (expected == null) {
+            assertNull(warpDrive.getPropertyOrNull(warpDrive.schema.turbo)) {
+                "getPropertyOrNull(turbo) should return null for absent property, ${WarpDriveElementSchema.turbo}, $message"
+            }
+            assertNull(warpDrive.getPropertyOrNull(warpDrive.schema.turbo.name)) {
+                "getPropertyOrNull(turbo.name) should return null for absent property, ${WarpDriveElementSchema.turbo}, $message"
+            }
+        }
+    }
+
     @Suppress("UNUSED_VARIABLE", "ReplaceGetOrSet")
     fun `compilation succeeds`() {
         // Below code does not make much sense, and it tests different styles of using the properties