You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by jo...@apache.org on 2021/09/10 11:01:20 UTC

[isis] 03/07: ISIS-2846 link tree diagram works

This is an automated email from the ASF dual-hosted git repository.

joergrade pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git

commit f848f38abbc4ec553bce83cae6315d483d48f4d4
Author: Jörg Rade <jo...@kuehne-nagel.com>
AuthorDate: Wed Sep 8 18:28:05 2021 +0200

    ISIS-2846 link tree diagram works
---
 .../kroviz/core/aggregator/AggregatorWithLayout.kt |   2 +
 .../client/kroviz/core/event/LogEntryDecorator.kt  | 156 ---------------------
 .../isis/client/kroviz/core/event/ResourceProxy.kt |  46 +++---
 .../client/kroviz/ui/diagram/LinkTreeDiagram.kt    | 131 +++++++----------
 .../apache/isis/client/kroviz/ui/diagram/Node.kt   |   7 +-
 .../isis/client/kroviz/ui/diagram/PumlCode.kt      |   5 +
 .../client/kroviz/ui/diagram/{Node.kt => Tree.kt}  |  28 +++-
 .../isis/client/kroviz/ui/dialog/EventLogDetail.kt |   8 +-
 .../isis/client/kroviz/core/event/LogEntryTest.kt  |   4 +-
 .../isis/client/kroviz/ui/diagram/TreeTest.kt      |  56 ++++++++
 10 files changed, 163 insertions(+), 280 deletions(-)

diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/aggregator/AggregatorWithLayout.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/aggregator/AggregatorWithLayout.kt
index 4bea388..32ed837 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/aggregator/AggregatorWithLayout.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/aggregator/AggregatorWithLayout.kt
@@ -6,11 +6,13 @@ import org.apache.isis.client.kroviz.layout.Layout
 import org.apache.isis.client.kroviz.to.Represention
 import org.apache.isis.client.kroviz.to.TObject
 import org.apache.isis.client.kroviz.ui.core.Constants
+import org.apache.isis.client.kroviz.ui.diagram.Tree
 
 abstract class AggregatorWithLayout : BaseAggregator() {
     // parentUrl is to be set in update
     // and to be used in subsequent invocations
     var parentUrl: String? = null
+    var tree: Tree? = null
 
     override fun update(logEntry: LogEntry, subType: String) {
         parentUrl = logEntry.url
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt
deleted file mode 100644
index f891eeb..0000000
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.client.kroviz.core.event
-
-import org.apache.isis.client.kroviz.to.Link
-import org.apache.isis.client.kroviz.to.Relation
-import org.apache.isis.client.kroviz.ui.core.Constants
-
-class LogEntryDecorator(val logEntry: LogEntry) {
-
-    val href: String = logEntry.selfHref()
-    val links: List<Link> = logEntry.getLinks()
-    val linked: List<LogEntry> = EventStore.getLinked()
-
-    fun findChildren(): Set<LogEntry> {
-        val children = findChildrenByUpRelation()
-        children.plus(findChildrenByReference())
-        children.plus(findChildrenByLinks())
-        return children
-    }
-
-    private fun findChildrenByUpRelation(): Set<LogEntry> {
-        val children = mutableSetOf<LogEntry>()
-        linked.forEach {
-            it.getLinks().forEach { l ->
-                if ((l.relation() == Relation.UP) && (l.href == href)) {
-                    children.add(it)
-                }
-            }
-        }
-        return children
-    }
-
-    private fun findChildrenByLinks(): Set<LogEntry> {
-        console.log("[LED.findChildrenByLinks]")
-        val children = mutableSetOf<LogEntry>()
-        links.forEach {
-            console.log(it.toString())
-            val rel = it.relation()
-            when {
-                (rel == Relation.UP) -> {
-                }
-                (rel == Relation.SELF) -> {
-                }
-                else -> {
-                    val rsj = ResourceSpecification(it.href, Constants.subTypeJson)
-                    var le = EventStore.findBy(rsj)
-                    if (le == null) {
-                        val rsx = ResourceSpecification(it.href, Constants.subTypeXml)
-                        le = EventStore.findBy(rsx)
-                    }
-                    console.log(le.toString())
-                    if (le != null) children.add(le)
-                }
-            }
-        }
-        return children
-    }
-
-    fun findChildrenOfLogEntry(): List<LogEntry> {
-        console.log("[LED.findChildrenOfLogEntry]")
-        val children = mutableListOf<LogEntry>()
-        val links = logEntry.getLinks()
-        console.log("[links] ->")
-        console.log(links)
-        console.log(links.size)
-        links.forEach {
-            console.log(it.toString())
-            when (it.relation()) {
-                Relation.UP -> {
-                }
-                Relation.SELF -> {
-                }
-                else -> {
-                    val url = it.href
-                    console.log(url)
-                    val rsj = ResourceSpecification(url, Constants.subTypeJson)
-                    var le = EventStore.findBy(rsj)
-                    if (le == null) {
-                        val rsx = ResourceSpecification(url, Constants.subTypeXml)
-                        le = EventStore.findBy(rsx)
-                    }
-                    console.log(le.toString())
-                    if (le != null) children.add(le)
-                }
-            }
-        }
-        console.log("[children] ->")
-        console.log(children)
-        return children
-    }
-
-    private fun findChildrenByReference(): Set<LogEntry> {
-        val str = logEntry.response
-        val children = mutableSetOf<LogEntry>()
-        linked.forEach {
-            if (it != logEntry && str.contains(it.url)) {
-                children.add(it)
-            }
-        }
-        return children
-    }
-
-    fun findChildrenIn(aggregatedList: List<LogEntry>): List<LogEntry> {
-        console.log("[LED.findChildrenIn]")
-        console.log(aggregatedList)
-        val selfUrl = href
-        val children = mutableListOf<LogEntry>()
-        aggregatedList.forEach {
-            if (it.url != selfUrl && it.response.contains(selfUrl)) {
-                children.add(it)
-            }
-        }
-        console.log(children)
-        return children
-    }
-
-    fun selfType(): String {
-        val selfLink = logEntry.selfLink()
-        return if (selfLink != null) {
-            selfLink.representation().type
-        } else {
-            console.log("[LED.selfType]")
-            console.log(logEntry)
-            ""
-        }
-    }
-
-    fun findParent(): LogEntry? {
-        val url = logEntry.url
-        linked.forEach {
-            when {
-                it.selfHref() == url -> return null
-                it.response.contains(url) -> return it
-            }
-        }
-        return null
-    }
-
-}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/ResourceProxy.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/ResourceProxy.kt
index 92d60a1..ad596a0 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/ResourceProxy.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/ResourceProxy.kt
@@ -27,6 +27,7 @@ import org.apache.isis.client.kroviz.to.Link
 import org.apache.isis.client.kroviz.to.TObject
 import org.apache.isis.client.kroviz.ui.core.Constants
 import org.apache.isis.client.kroviz.ui.diagram.Node
+import org.apache.isis.client.kroviz.ui.diagram.Tree
 
 /**
  * Facade for RoXmlHttpRequest. If a resource is being fetched, it:
@@ -51,18 +52,6 @@ class ResourceProxy {
         }
     }
 
-    private fun processCached(rs: ResourceSpecification, aggregator: BaseAggregator?) {
-        val le = EventStore.findBy(rs)!!
-        le.retrieveResponse()
-        if (aggregator == null) {
-            ResponseHandler.handle(le)
-        } else {
-            aggregator.update(le, le.subType)
-        }
-        le.setCached()
-        EventStore.updateStatus(le)
-    }
-
     fun fetch(link: Link,
               aggregator: BaseAggregator? = null,
               subType: String = Constants.subTypeJson,
@@ -75,25 +64,36 @@ class ResourceProxy {
         }
         when {
             isCached -> processCached(rs, aggregator)
-            !isCached && isRest -> {
-                if (aggregator is AggregatorWithLayout) {
-                    val child = Node(link.href)
-//FIXME                    aggregator.root.add(child)
-                }
-                RoXmlHttpRequest(aggregator).process(link, subType)
-            }
+            !isCached && isRest -> process(aggregator, link, subType, referrer)
             !isCached && !isRest -> RoXmlHttpRequest(aggregator).processNonREST(link, subType)
         }
     }
 
-    private fun isNotRenderedYet(aggregator: BaseAggregator?): Boolean {
-        if (aggregator != null && aggregator is AggregatorWithLayout) {
-            return !aggregator.dpm.isRendered
+    private fun process(aggregator: BaseAggregator?, link: Link, subType: String, referrer: String) {
+        if (aggregator is AggregatorWithLayout) {
+            if (aggregator.tree == null) {
+                val root = Node(referrer, null)
+                aggregator.tree = Tree(root)
+            }
+            aggregator.tree!!.addChildToParent(link.href, referrer)
+
+        }
+        RoXmlHttpRequest(aggregator).process(link, subType)
+    }
+
+    private fun processCached(rs: ResourceSpecification, aggregator: BaseAggregator?) {
+        val le = EventStore.findBy(rs)!!
+        le.retrieveResponse()
+        if (aggregator == null) {
+            ResponseHandler.handle(le)
         } else {
-            return false
+            aggregator.update(le, le.subType)
         }
+        le.setCached()
+        EventStore.updateStatus(le)
     }
 
+
     fun invokeKroki(pumlCode: String, aggregator: SvgDispatcher) {
         RoXmlHttpRequest(aggregator).invokeKroki(pumlCode)
     }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
index 9e6c480..bc3789d 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
@@ -18,10 +18,10 @@
  */
 package org.apache.isis.client.kroviz.ui.diagram
 
+import org.apache.isis.client.kroviz.core.aggregator.AggregatorWithLayout
 import org.apache.isis.client.kroviz.core.aggregator.BaseAggregator
 import org.apache.isis.client.kroviz.core.event.EventStore
 import org.apache.isis.client.kroviz.core.event.LogEntry
-import org.apache.isis.client.kroviz.core.event.LogEntryDecorator
 import org.apache.isis.client.kroviz.core.event.ResourceSpecification
 import org.apache.isis.client.kroviz.to.HasLinks
 import org.apache.isis.client.kroviz.ui.core.UiManager
@@ -29,102 +29,71 @@ import org.apache.isis.client.kroviz.utils.StringUtils
 
 object LinkTreeDiagram {
 
-    private const val NL = "\n"
-    private const val prolog = "@startmindmap$NL"
-    private const val epilog = "@endmindmap$NL"
     private val protocolHostPort = UiManager.getUrl()
 
     fun build(aggregator: BaseAggregator): String {
-        var code = prolog
-        val entryList: List<LogEntry> = EventStore.findAllBy(aggregator)
-        val root = findRoot(entryList)
-        if (root == null) {
-            code += "* / $NL"
-            code += createNodes(entryList, 2)
-        } else {
-            code += root.asPumlNode(1)
-            val led = LogEntryDecorator(root)
-            val childList = led.findChildrenOfLogEntry()
-            console.log(aggregator)
-            console.log(entryList)
-            code += createChildNodes(childList, 2)
-        }
-        code += epilog
-        return code
-    }
-
-    private fun createChildNodes(childList: List<LogEntry>, level: Int): String {
-        var code = ""
-        childList.forEach {
-            code += createNode(it, level)
-            val led = LogEntryDecorator(it)
-            val kidSet = led.findChildrenOfLogEntry()
-            code += createChildNodes(kidSet, level + 1)
-        }
-        return code
-    }
-
-    private fun createNode(le: LogEntry, level: Int): String {
-        var code = ""
-        if (isInEventStore(le.url)) {
-            code += le.asPumlNode(level)
-        }
-        return code
-    }
-
-    private fun createNodes(entryList: List<LogEntry>, level: Int): String {
-        var code = ""
-        entryList.forEach {
-            code += createNode(it, level)
-        }
-        return code
-    }
-
-    private fun findRoot(entryList: List<LogEntry>): LogEntry? {
-        entryList.forEach {
-            val led = LogEntryDecorator(it)
-            val parent = led.findParent()
-            if (parent != null && !entryList.contains(parent)) {
-                return parent
-            }
+        console.log("[LTD.build]")
+        val pc = PumlCode()
+        if (aggregator is AggregatorWithLayout) {
+            val tree = aggregator.tree!!
+            val root = tree.root
+            console.log(root)
+            pc.code += toPumlCode(root, 1)
         }
-        return null
+        pc.mindmap()
+        console.log(pc.code)
+        return pc.code
     }
 
-    private fun isInEventStore(url: String): Boolean {
+    private fun toPumlCode(node: Node, level: Int): String {
+        val url = node.key
         val rs = ResourceSpecification(url)
         val le = EventStore.findBy(rs)
-        return (le != null)
-    }
-
-    fun LogEntry.asPumlNode(level: Int): String {
-        val led = LogEntryDecorator(this)
-        val url = this.url
-        val title = StringUtils.shortTitle(url, protocolHostPort)
-        val type = led.selfType()
-        val depth = "*".repeat(level)
         val pc = PumlCode()
-        pc.add(depth).add(":")
-        pc.addStereotype(type)
-        pc.addLink(url, title)
-        pc.addHorizontalLine()
-        pc.add(traceInfo(this))
-        pc.addLine(";")
+        if (le != null) {
+            val title = StringUtils.shortTitle(url, protocolHostPort)
+            val type = le.selfType()
+            val depth = "*".repeat(level)
+            pc.add(depth).add(":")
+            pc.addStereotype(type)
+            pc.addLink(url, title)
+            pc.addHorizontalLine()
+            pc.add(traceInfo(le))
+            pc.addLine(";")
+            node.children.forEach {
+                val childCode = toPumlCode(it, level + 1)
+                pc.add(childCode)
+            }
+        }
         return pc.code
     }
 
     private fun traceInfo(logEntry: LogEntry): String {
-        val obj = logEntry.obj!!
-        val className = obj::class.simpleName!!
-        val pc = PumlCode().addClass(className)
-        if (obj is HasLinks) {
-            obj.links.forEach {
-                val url = it.href
-                val title = StringUtils.shortTitle(url, protocolHostPort)
-                pc.addLink(url, title)
+        val pc = PumlCode()
+        val obj = logEntry.obj
+        if (obj != null) {
+            val className = obj::class.simpleName!!
+            pc.addClass(className)
+            if (obj is HasLinks) {
+                obj.links.forEach {
+                    val url = it.href
+                    val title = StringUtils.shortTitle(url, protocolHostPort)
+                    pc.addLink(url, title)
+                }
             }
         }
         return pc.code
     }
 
+    private fun LogEntry.selfType(): String {
+        val selfLink = this.selfLink()
+        return if (selfLink != null) {
+            selfLink.representation().type
+        } else {
+            console.log("[LE.selfType]")
+            console.log(this)
+            ""
+        }
+    }
+
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt
index 19bc68c..79c0930 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt
@@ -18,13 +18,8 @@
  */
 package org.apache.isis.client.kroviz.ui.diagram
 
-class Node(val key: String) {
+class Node(val key: String, val parent: Node?) {
 
-    var parent: Node? = null
     val children = mutableListOf<Node>()
 
-    fun add(child: Node) {
-        children.add(child)
-    }
-
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/PumlCode.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/PumlCode.kt
index ac5ea8d..86410eb 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/PumlCode.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/PumlCode.kt
@@ -55,6 +55,11 @@ class PumlCode() {
         return this
     }
 
+    fun mindmap(): PumlCode {
+        code += "@startmindmap$NL" + code + "@endmindmap$NL"
+        return this
+    }
+
     private fun center(s: String): String {
         return ".." + s + ".."
     }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Tree.kt
similarity index 57%
copy from incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt
copy to incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Tree.kt
index 19bc68c..922bc94 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Node.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/Tree.kt
@@ -18,13 +18,31 @@
  */
 package org.apache.isis.client.kroviz.ui.diagram
 
-class Node(val key: String) {
+class Tree(val root: Node) {
 
-    var parent: Node? = null
-    val children = mutableListOf<Node>()
+    fun addChildToParent(childUrl: String, parentUrl: String) {
+        var p = find(parentUrl, root)
+        if (p == null) {
+            p = root
+        }
+        val c = Node(childUrl, p)
+        p.children.add(c)
+    }
 
-    fun add(child: Node) {
-        children.add(child)
+    fun find(url: String, node: Node): Node? {
+        if (node.key == url) {
+            return node
+        } else {
+            var answer: Node? = null
+            node.children.forEach {
+                if (it.key == url) {
+                    answer = it
+                } else {
+                    answer = find(url, it)
+                }
+            }
+            return answer
+        }
     }
 
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
index 6eb29f6..e72da92 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
@@ -20,7 +20,6 @@ package org.apache.isis.client.kroviz.ui.dialog
 
 import org.apache.isis.client.kroviz.core.event.EventStore
 import org.apache.isis.client.kroviz.core.event.LogEntry
-import org.apache.isis.client.kroviz.core.event.LogEntryDecorator
 import org.apache.isis.client.kroviz.core.event.ResourceSpecification
 import org.apache.isis.client.kroviz.to.ValueType
 import org.apache.isis.client.kroviz.to.bs3.Grid
@@ -41,7 +40,7 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
         // For a yet unknown reason, aggregators are not transmitted via tabulator.
         // As a WORKAROUND, we fetch the full blown LogEntry from the EventStore again.
         val rs = ResourceSpecification(logEntryFromTabulator.title)
-        logEntry = EventStore.findBy(rs)?: logEntryFromTabulator  // in case of xml, we use the entry passed in
+        logEntry = EventStore.findBy(rs) ?: logEntryFromTabulator  // in case of xml, we use the entry passed in
     }
 
     // callback parameter
@@ -55,15 +54,10 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
             XmlHelper.format(logEntry.response)
         }
 
-        val led = LogEntryDecorator(logEntry)
-        val children = led.findChildren()
-        var kids = ""
-        children.forEach { kids += it.url + "\n" }
         val formItems = mutableListOf<FormItem>()
         formItems.add(FormItem("Url", ValueType.TEXT, logEntry.title))
         formItems.add(FormItem("Response", ValueType.TEXT_AREA, responseStr, 10))
         formItems.add(FormItem("Aggregators", ValueType.TEXT, content = logEntry.aggregators))
-        formItems.add(FormItem("Children", ValueType.TEXT_AREA, kids, size = 5))
         formItems.add(FormItem("Link Tree Diagram", ValueType.BUTTON, null, callBack = this, callBackAction = LNK))
         formItems.add(FormItem("Console", ValueType.BUTTON, null, callBack = this, callBackAction = LOG))
 
diff --git a/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryTest.kt b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryTest.kt
index 9ff7b10..dea66af 100644
--- a/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryTest.kt
+++ b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryTest.kt
@@ -30,7 +30,7 @@ class LogEntryTest {
         val url = "https://kroki.io"
 
         // when
-        val le = LogEntry(url)
+        val le = LogEntry(ResourceSpecification(url))
 
         // then
         assertFalse(le.title.startsWith("/"))
@@ -39,7 +39,7 @@ class LogEntryTest {
     @Test
     fun testCalculate() {
         // given
-        val le = LogEntry("http://test/url")
+        val le = LogEntry(ResourceSpecification("http://test/url"))
 
         // when
         le.setSuccess()
diff --git a/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/ui/diagram/TreeTest.kt b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/ui/diagram/TreeTest.kt
new file mode 100644
index 0000000..712bc86
--- /dev/null
+++ b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/ui/diagram/TreeTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.isis.client.kroviz.ui.diagram
+
+import kotlin.test.*
+
+class TreeTest {
+
+    @BeforeTest
+    fun setup() {
+    }
+
+    @Test
+    fun testAddChildToParent() {
+        //given
+        val url_0 = "root"
+        val url_1 = "level_1"
+        val url_1_1 = "level_1_1"
+        val url_1_2 = "level_1_2"
+        val root = Node(url_0, null)
+        val tree = Tree(root)
+
+        //when
+        tree.addChildToParent(url_1, url_0)
+        tree.addChildToParent(url_1_1, url_1)
+        tree.addChildToParent(url_1_2, url_1)
+
+        //then
+        val r = tree.find(url_0, root)!!
+        assertEquals(1, r.children.size)
+        assertNull(r.parent)
+
+        val c = tree.find(url_1, root)!!
+        assertNotNull(c.parent)
+        assertEquals(2, c.children.size)
+        assertEquals(url_1_1, c.children.first().key)
+        assertEquals(url_1_2, c.children.last().key)
+    }
+
+}