From f8639917f32495ac8e74277bed1a2176eacc01bc Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 16:30:45 +0800 Subject: [PATCH] feat: webapp container tab state management --- .../top/yeij/hearth/webapp/WebAppContainer.kt | 54 ++++++++++++++++ .../yeij/hearth/webapp/WebAppContainerTest.kt | 61 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 app/src/main/java/top/yeij/hearth/webapp/WebAppContainer.kt create mode 100644 app/src/test/java/top/yeij/hearth/webapp/WebAppContainerTest.kt diff --git a/app/src/main/java/top/yeij/hearth/webapp/WebAppContainer.kt b/app/src/main/java/top/yeij/hearth/webapp/WebAppContainer.kt new file mode 100644 index 0000000..220cded --- /dev/null +++ b/app/src/main/java/top/yeij/hearth/webapp/WebAppContainer.kt @@ -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() + + // 打开标签:已存在则激活它(不重复添加),否则新增并激活 + // 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 = 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() {} +} diff --git a/app/src/test/java/top/yeij/hearth/webapp/WebAppContainerTest.kt b/app/src/test/java/top/yeij/hearth/webapp/WebAppContainerTest.kt new file mode 100644 index 0000000..91037db --- /dev/null +++ b/app/src/test/java/top/yeij/hearth/webapp/WebAppContainerTest.kt @@ -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) + } +}