package renderers.html
import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon
import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.jsoup.nodes.Element
import org.junit.jupiter.api.Test
import utils.TestOutputWriterPlugin
import utils.navigationHtml
import kotlin.test.assertEquals
import kotlin.test.assertNull
class NavigationTest : BaseAbstractTest() {
private val configuration = dokkaConfiguration {
sourceSets {
sourceSet {
sourceRoots = listOf("src/")
}
}
}
@Test
fun `should sort alphabetically ignoring case`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/com/example/Sequences.kt
|package com.example
|
|fun sequence(): Sequence
|
|fun Sequence(): Sequence
|
|fun Sequence.any() {}
|
|interface Sequence
""".trimMargin(),
configuration,
pluginOverrides = listOf(writerPlugin)
) {
renderingStage = { _, _ ->
val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart")
assertEquals(6, content.size)
// Navigation menu should be the following:
// - root
// - com.example
// - any()
// - Sequence interface
// - Sequence()
// - sequence()
content[0].assertNavigationLink(
id = "root-nav-submenu",
text = "root",
address = "index.html",
)
content[1].assertNavigationLink(
id = "root-nav-submenu-0",
text = "com.example",
address = "root/com.example/index.html",
)
content[2].assertNavigationLink(
id = "root-nav-submenu-0-0",
text = "any()",
address = "root/com.example/any.html",
icon = NavigationNodeIcon.FUNCTION
)
content[3].assertNavigationLink(
id = "root-nav-submenu-0-1",
text = "Sequence",
address = "root/com.example/-sequence/index.html",
icon = NavigationNodeIcon.INTERFACE_KT
)
content[4].assertNavigationLink(
id = "root-nav-submenu-0-2",
text = "Sequence()",
address = "root/com.example/-sequence.html",
icon = NavigationNodeIcon.FUNCTION
)
content[5].assertNavigationLink(
id = "root-nav-submenu-0-3",
text = "sequence()",
address = "root/com.example/sequence.html",
icon = NavigationNodeIcon.FUNCTION
)
}
}
}
@Test
fun `should strike deprecated class link`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/com/example/SimpleDeprecatedClass.kt
|package com.example
|
|@Deprecated("reason")
|class SimpleDeprecatedClass {}
""".trimIndent(),
configuration,
pluginOverrides = listOf(writerPlugin)
) {
renderingStage = { _, _ ->
val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart")
assertEquals(3, content.size)
// Navigation menu should be the following:
// - root
// - com.example
// - SimpleDeprecatedClass
content[0].assertNavigationLink(
id = "root-nav-submenu",
text = "root",
address = "index.html",
)
content[1].assertNavigationLink(
id = "root-nav-submenu-0",
text = "com.example",
address = "root/com.example/index.html",
)
content[2].assertNavigationLink(
id = "root-nav-submenu-0-0",
text = "SimpleDeprecatedClass",
address = "root/com.example/-simple-deprecated-class/index.html",
icon = NavigationNodeIcon.CLASS_KT,
isStrikethrough = true
)
}
}
}
@Test
fun `should not strike pages where only one of N documentables is deprecated`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/com/example/File.kt
|package com.example
|
|/**
| * First
| */
|@Deprecated("reason")
|fun functionWithCommonName()
|
|/**
| * Second
| */
|fun functionWithCommonName()
""".trimIndent(),
configuration,
pluginOverrides = listOf(writerPlugin)
) {
renderingStage = { _, _ ->
val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart")
assertEquals(3, content.size)
// Navigation menu should be the following:
// - root
// - com.example
// - functionWithCommonName
content[0].assertNavigationLink(
id = "root-nav-submenu",
text = "root",
address = "index.html",
)
content[1].assertNavigationLink(
id = "root-nav-submenu-0",
text = "com.example",
address = "root/com.example/index.html",
)
content[2].assertNavigationLink(
id = "root-nav-submenu-0-0",
text = "functionWithCommonName()",
address = "root/com.example/function-with-common-name.html",
icon = NavigationNodeIcon.FUNCTION,
isStrikethrough = false
)
}
}
}
@Test
fun `should have expandable classlikes`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/com/example/WithInner.kt
|package com.example
|
|class WithInner {
| // in-class functions should not be in navigation
| fun a() {}
| fun b() {}
| fun c() {}
|
| class InnerClass {}
| interface InnerInterface {}
| enum class InnerEnum {}
| object InnerObject {}
| annotation class InnerAnnotation {}
| companion object CompanionObject {}
|}
""".trimIndent(),
configuration,
pluginOverrides = listOf(writerPlugin)
) {
renderingStage = { _, _ ->
val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart")
assertEquals(9, content.size)
// Navigation menu should be the following, sorted by name:
// - root
// - com.example
// - WithInner
// - CompanionObject
// - InnerAnnotation
// - InnerClass
// - InnerEnum
// - InnerInterface
// - InnerObject
content[0].assertNavigationLink(
id = "root-nav-submenu",
text = "root",
address = "index.html",
)
content[1].assertNavigationLink(
id = "root-nav-submenu-0",
text = "com.example",
address = "root/com.example/index.html",
)
content[2].assertNavigationLink(
id = "root-nav-submenu-0-0",
text = "WithInner",
address = "root/com.example/-with-inner/index.html",
icon = NavigationNodeIcon.CLASS_KT
)
content[3].assertNavigationLink(
id = "root-nav-submenu-0-0-0",
text = "CompanionObject",
address = "root/com.example/-with-inner/-companion-object/index.html",
icon = NavigationNodeIcon.OBJECT
)
content[4].assertNavigationLink(
id = "root-nav-submenu-0-0-1",
text = "InnerAnnotation",
address = "root/com.example/-with-inner/-inner-annotation/index.html",
icon = NavigationNodeIcon.ANNOTATION_CLASS_KT
)
content[5].assertNavigationLink(
id = "root-nav-submenu-0-0-2",
text = "InnerClass",
address = "root/com.example/-with-inner/-inner-class/index.html",
icon = NavigationNodeIcon.CLASS_KT
)
content[6].assertNavigationLink(
id = "root-nav-submenu-0-0-3",
text = "InnerEnum",
address = "root/com.example/-with-inner/-inner-enum/index.html",
icon = NavigationNodeIcon.ENUM_CLASS_KT
)
content[7].assertNavigationLink(
id = "root-nav-submenu-0-0-4",
text = "InnerInterface",
address = "root/com.example/-with-inner/-inner-interface/index.html",
icon = NavigationNodeIcon.INTERFACE_KT
)
content[8].assertNavigationLink(
id = "root-nav-submenu-0-0-5",
text = "InnerObject",
address = "root/com.example/-with-inner/-inner-object/index.html",
icon = NavigationNodeIcon.OBJECT
)
}
}
}
@Test
fun `should be able to have deeply nested classlikes`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/com/example/DeeplyNested.kt
|package com.example
|
|class DeeplyNested {
| class FirstLevelClass {
| interface SecondLevelInterface {
| object ThirdLevelObject {
| annotation class FourthLevelAnnotation {}
| }
| }
| }
|}
""".trimIndent(),
configuration,
pluginOverrides = listOf(writerPlugin)
) {
renderingStage = { _, _ ->
val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart")
assertEquals(7, content.size)
// Navigation menu should be the following
// - root
// - com.example
// - DeeplyNested
// - FirstLevelClass
// - SecondLevelInterface
// - ThirdLevelObject
// - FourthLevelAnnotation
content[0].assertNavigationLink(
id = "root-nav-submenu",
text = "root",
address = "index.html",
)
content[1].assertNavigationLink(
id = "root-nav-submenu-0",
text = "com.example",
address = "root/com.example/index.html",
)
content[2].assertNavigationLink(
id = "root-nav-submenu-0-0",
text = "DeeplyNested",
address = "root/com.example/-deeply-nested/index.html",
icon = NavigationNodeIcon.CLASS_KT
)
content[3].assertNavigationLink(
id = "root-nav-submenu-0-0-0",
text = "FirstLevelClass",
address = "root/com.example/-deeply-nested/-first-level-class/index.html",
icon = NavigationNodeIcon.CLASS_KT
)
content[4].assertNavigationLink(
id = "root-nav-submenu-0-0-0-0",
text = "SecondLevelInterface",
address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/index.html",
icon = NavigationNodeIcon.INTERFACE_KT
)
content[5].assertNavigationLink(
id = "root-nav-submenu-0-0-0-0-0",
text = "ThirdLevelObject",
address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" +
"-third-level-object/index.html",
icon = NavigationNodeIcon.OBJECT
)
content[6].assertNavigationLink(
id = "root-nav-submenu-0-0-0-0-0-0",
text = "FourthLevelAnnotation",
address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" +
"-third-level-object/-fourth-level-annotation/index.html",
icon = NavigationNodeIcon.ANNOTATION_CLASS_KT
)
}
}
}
private fun Element.assertNavigationLink(
id: String, text: String, address: String, icon: NavigationNodeIcon? = null, isStrikethrough: Boolean = false
) {
assertEquals(id, this.id())
val link = this.selectFirst("a")
checkNotNull(link)
assertEquals(text, link.text())
assertEquals(address, link.attr("href"))
if (icon != null) {
val iconStyles =
this.selectFirst("div.overview span.nav-link-grid")?.child(0)?.classNames()?.toList() ?: emptyList()
assertEquals(3, iconStyles.size)
assertEquals("nav-link-child", iconStyles[0])
assertEquals(icon.style(), "${iconStyles[1]} ${iconStyles[2]}")
}
if (isStrikethrough) {
val textInsideStrikethrough = link.selectFirst("strike")?.text()
assertEquals(text, textInsideStrikethrough)
} else {
assertNull(link.selectFirst("strike"))
}
}
}