feat: webapp container tab state management
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package top.yeij.hearth.webapp
|
||||
|
||||
// 单个 WebView 标签:id 唯一标识,active 表示是否为当前激活标签
|
||||
// Single WebView tab: id is the unique key, active marks the currently-focused tab
|
||||
data class Tab(val id: String, val url: String, val active: Boolean = false)
|
||||
|
||||
// 多标签状态管理(纯 Kotlin 数据层,不依赖 WebView,可 JVM 单测)
|
||||
// 真实 WebView 导航(goBack/goForward/reload)留待 Task 13 集成时接入
|
||||
// Multi-tab state management (pure Kotlin data layer, WebView-free, JVM-testable)
|
||||
// Real WebView navigation (goBack/goForward/reload) is wired up in Task 13
|
||||
class WebAppContainer {
|
||||
private val tabs = mutableListOf<Tab>()
|
||||
|
||||
// 打开标签:已存在则激活它(不重复添加),否则新增并激活
|
||||
// Open a tab: if it already exists just activate it, otherwise add and activate
|
||||
fun open(id: String, url: String) {
|
||||
val existing = tabs.find { it.id == id }
|
||||
if (existing != null) {
|
||||
setActive(id)
|
||||
return
|
||||
}
|
||||
for (i in tabs.indices) tabs[i] = tabs[i].copy(active = false)
|
||||
tabs.add(Tab(id, url, active = true))
|
||||
}
|
||||
|
||||
// 关闭标签:若关闭后没有激活标签则激活第一个
|
||||
// Close a tab: if none remain active, activate the first one
|
||||
fun close(id: String) {
|
||||
tabs.removeAll { it.id == id }
|
||||
if (tabs.isNotEmpty() && tabs.none { it.active }) {
|
||||
tabs[0] = tabs[0].copy(active = true)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换激活标签
|
||||
// Switch the active tab
|
||||
fun switchTo(id: String) {
|
||||
setActive(id)
|
||||
}
|
||||
|
||||
// 返回标签列表副本(外部无法直接改动内部状态)
|
||||
// Return a defensive copy of the tab list
|
||||
fun tabs(): List<Tab> = tabs.toList()
|
||||
|
||||
private fun setActive(id: String) {
|
||||
for (i in tabs.indices) tabs[i] = tabs[i].copy(active = tabs[i].id == id)
|
||||
}
|
||||
|
||||
// 真实回退由 WebView 层实现,数据层仅占位返回 false
|
||||
// Real back navigation lives in the WebView layer; data layer is a stub
|
||||
fun goBack(): Boolean = false
|
||||
fun goForward(): Boolean = false
|
||||
fun reload() {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package top.yeij.hearth.webapp
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class WebAppContainerTest {
|
||||
@Test
|
||||
fun open_addsTab_andBecomesActive() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a")
|
||||
c.open("b", "http://x/b")
|
||||
assertEquals(2, c.tabs().size)
|
||||
assertEquals("b", c.tabs().first { it.active }.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun close_removesTab() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a"); c.open("b", "http://x/b")
|
||||
c.close("a")
|
||||
assertEquals(1, c.tabs().size)
|
||||
assertEquals("b", c.tabs()[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun goBack_noHistory_returnsFalse() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a")
|
||||
assertTrue(!c.goBack()) // 无历史
|
||||
}
|
||||
|
||||
@Test
|
||||
fun open_existingTab_activatesWithoutDuplicating() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a")
|
||||
c.open("b", "http://x/b")
|
||||
c.open("a", "http://x/a")
|
||||
assertEquals(2, c.tabs().size)
|
||||
assertEquals("a", c.tabs().first { it.active }.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun switchTo_activatesSpecifiedTab() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a")
|
||||
c.open("b", "http://x/b")
|
||||
c.switchTo("a")
|
||||
assertEquals("a", c.tabs().first { it.active }.id)
|
||||
assertEquals(1, c.tabs().count { it.active })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun close_activeTab_activatesFirstRemaining() {
|
||||
val c = WebAppContainer()
|
||||
c.open("a", "http://x/a")
|
||||
c.open("b", "http://x/b")
|
||||
c.close("b")
|
||||
assertEquals("a", c.tabs().first { it.active }.id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user