diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
index f589f3ecb..0b056b0b8 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
@@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.junit.Test
@@ -2159,6 +2160,78 @@ class JsonTreeTest {
}
}
+ // Tests for concurrent tree modifications causing incorrect remote event paths.
+ @Test
+ fun test_concurrent_tree_style_and_insertion() {
+ withTwoClientsAndDocuments(syncMode = Client.SyncMode.Manual) { c1, c2, d1, d2, _ ->
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.setNewTree(
+ "t",
+ element("doc") {
+ element("p")
+ },
+ )
+ },
+ Updater(c2, d2),
+ )
+ assertTreesXmlEquals("""""", d1, d2)
+
+ val d1Events = mutableListOf()
+ val d2Events = mutableListOf()
+
+ val collectJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d1.events.filterIsInstance()
+ .map {
+ it.changeInfo.operations.filterIsInstance()
+ }
+ .collect(d1Events::addAll)
+ }
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d2.events.filterIsInstance()
+ .map {
+ it.changeInfo.operations.filterIsInstance()
+ }
+ .collect(d2Events::addAll)
+ }
+ }
+
+ listOf(
+ launch {
+ repeat(10) {
+ d1.updateAsync { root, _ ->
+ root.rootTree().style(0, 1, mapOf("style" to it.toString()))
+ }.await()
+ }
+ },
+ launch {
+ d2.updateAsync { root, _ ->
+ root.rootTree().edit(0, 0, element("p2"))
+ }.await()
+ },
+ ).joinAll()
+
+ c1.syncAsync().await()
+ c2.syncAsync().await()
+ c1.syncAsync().await()
+
+ // the resulting trees are the same.
+ assertEquals(d1.getRoot().rootTree().toXml(), d2.getRoot().rootTree().toXml())
+
+ // insert event is sent with path [0]
+ assertEquals(listOf(0), d1Events.single().fromPath)
+
+ // but here test fails.
+ // style event should be sent with path [1], but path [0] is given.
+ d2Events.forEach {
+ assertEquals(listOf(1), it.fromPath)
+ }
+
+ collectJob.cancel()
+ }
+ }
+
companion object {
fun JsonObject.rootTree() = getAs("t")