release(ios): 发布 0.2.1 并重构图标与界面细节
- 应用图标改用与 Web favicon 同源的扁平标记,金色 SIM 卡体加三格递增信号,29pt 下仍可读 - 标签统一为胶囊形状,提高深色主题下的描边权重,避免标签在暗色下消失 - 消息卡片与详情分区卡片收紧行距,单屏信息密度明显提升 - 详情头部指标精简为 4 行,数据模块改为单条读数 - 批量选择勾选框移出名称区域,工具栏控件统一为等高 34pt - 分组编辑器改为点选即生效,移除说明文字与常驻禁用按钮
This commit is contained in:
@@ -11,7 +11,8 @@ final class AIThemeTests: XCTestCase {
|
||||
XCTAssertEqual(AITheme.toolbarButtonHeight, 44, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.segmentedControlHeight, 44, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.buttonRadius, 16, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.iconButtonSize, 44, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.iconButtonSize, 34, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.iconButtonRadius, 11, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.toolbarButtonMinWidth, 96, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.toolbarButtonWidth, 104, accuracy: 0.1)
|
||||
XCTAssertEqual(AITheme.footerSecondaryButtonWidth, 104, accuracy: 0.1)
|
||||
|
||||
@@ -6,7 +6,7 @@ final class AppMetadataTests: XCTestCase {
|
||||
func testMetadataUsesIPAFoundationContract() {
|
||||
XCTAssertEqual(AppMetadata.name, "MultiSimAdmin")
|
||||
XCTAssertEqual(AppMetadata.bundleIdentifier, "fun.chick.multi-simadmin")
|
||||
XCTAssertEqual(AppMetadata.marketingVersion, "0.2.0")
|
||||
XCTAssertEqual(AppMetadata.buildVersion, "3")
|
||||
XCTAssertEqual(AppMetadata.marketingVersion, "0.2.1")
|
||||
XCTAssertEqual(AppMetadata.buildVersion, "6")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,18 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
final class FleetViewModelTests: XCTestCase {
|
||||
private func transientPreferences() -> UserDefaults {
|
||||
let suite = "FleetViewModelTests.\(UUID().uuidString)"
|
||||
return UserDefaults(suiteName: suite)!
|
||||
}
|
||||
|
||||
func testRefreshSortsByNameAndReportsStatus() async throws {
|
||||
let repository = MemoryInstanceRepository()
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore()
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
try await model.add(InstanceInput(
|
||||
name: "Zulu",
|
||||
@@ -36,7 +42,8 @@ final class FleetViewModelTests: XCTestCase {
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore()
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
try await model.add(InstanceInput(name: "Home", origin: URL(string: "http://192.168.1.70:3000")!, tags: ["home"]))
|
||||
try await model.add(InstanceInput(name: "Lab", origin: URL(string: "http://192.168.1.71:3000")!, tags: []))
|
||||
@@ -48,14 +55,80 @@ final class FleetViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(model.groupedRows.flatMap(\.rows).map(\.instance.name), ["Home"])
|
||||
|
||||
model.searchText = ""
|
||||
model.groupMode = .tags
|
||||
XCTAssertEqual(model.groupedRows.map(\.title), ["home", "未打标签"])
|
||||
XCTAssertEqual(model.groupedRows.map(\.rows.count), [1, 1])
|
||||
model.clearTagFilters()
|
||||
model.toggleTagFilter("Home")
|
||||
XCTAssertEqual(model.allTags, ["home"])
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Home"])
|
||||
XCTAssertEqual(model.countForTagFilter("home"), 1)
|
||||
model.clearTagFilters()
|
||||
|
||||
model.groupMode = .status
|
||||
XCTAssertEqual(model.groupedRows.map(\.title), ["在线"])
|
||||
}
|
||||
|
||||
func testTagFiltersCombineWithAndSemanticsAndPruneStaleSelections() async throws {
|
||||
let repository = MemoryInstanceRepository()
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
try await model.add(InstanceInput(
|
||||
name: "Both",
|
||||
origin: URL(string: "http://192.168.1.90:3000")!,
|
||||
tags: ["primary", "field"]
|
||||
))
|
||||
try await model.add(InstanceInput(
|
||||
name: "Single",
|
||||
origin: URL(string: "http://192.168.1.91:3000")!,
|
||||
tags: ["primary"]
|
||||
))
|
||||
await model.refresh()
|
||||
|
||||
model.toggleTagFilter("primary")
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Both", "Single"])
|
||||
|
||||
model.toggleTagFilter("FIELD")
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Both"])
|
||||
|
||||
model.toggleTagFilter("field")
|
||||
model.toggleTagFilter("primary")
|
||||
XCTAssertTrue(model.selectedTagFilters.isEmpty)
|
||||
|
||||
model.toggleTagFilter("vanished")
|
||||
XCTAssertTrue(model.isTagFilterActive("vanished"))
|
||||
await model.refresh()
|
||||
XCTAssertTrue(model.selectedTagFilters.isEmpty, "已不存在的标签筛选应当在刷新后自动清理")
|
||||
}
|
||||
|
||||
func testTagFilterSelectionSurvivesAcrossInstances() async throws {
|
||||
let suite = "FleetTagFilterTests.\(UUID().uuidString)"
|
||||
let repository = MemoryInstanceRepository()
|
||||
let first = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: UserDefaults(suiteName: suite)!
|
||||
)
|
||||
try await first.add(InstanceInput(
|
||||
name: "Relay",
|
||||
origin: URL(string: "http://192.168.1.92:3000")!,
|
||||
tags: ["relay"]
|
||||
))
|
||||
await first.refresh()
|
||||
first.toggleTagFilter("relay")
|
||||
|
||||
let second = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: UserDefaults(suiteName: suite)!
|
||||
)
|
||||
XCTAssertTrue(second.selectedTagFilters.contains("relay"))
|
||||
UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite)
|
||||
}
|
||||
|
||||
func testGroupModeKeepsUngroupedInstancesVisible() async throws {
|
||||
let repository = MemoryInstanceRepository()
|
||||
let model = FleetViewModel(
|
||||
|
||||
@@ -15,7 +15,8 @@ final class MessageServiceTests: XCTestCase {
|
||||
XCTAssertEqual(client.routes.first, .smsList(limit: 2, offset: 0, direction: nil))
|
||||
XCTAssertEqual(page.messages.count, 1)
|
||||
let message = try XCTUnwrap(page.messages.first)
|
||||
XCTAssertEqual(message.id, "42")
|
||||
XCTAssertEqual(message.upstreamID, "42")
|
||||
XCTAssertEqual(message.id, "\(instance.id.uuidString)#42")
|
||||
XCTAssertEqual(message.instanceID, instance.id)
|
||||
XCTAssertEqual(message.instanceName, "Message Node")
|
||||
XCTAssertEqual(message.phoneNumber, "13800138000")
|
||||
|
||||
@@ -11,10 +11,10 @@ final class SettingsViewModelTests: XCTestCase {
|
||||
await model.refresh()
|
||||
|
||||
XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin")
|
||||
XCTAssertEqual(model.runtime?.marketingVersion, "0.2.0")
|
||||
XCTAssertEqual(model.runtime?.buildVersion, "3")
|
||||
XCTAssertEqual(model.runtime?.marketingVersion, "0.2.1")
|
||||
XCTAssertEqual(model.runtime?.buildVersion, "6")
|
||||
XCTAssertEqual(model.runtime?.platform, "iOS")
|
||||
XCTAssertTrue(model.runtime?.deviceModel.isEmpty == false)
|
||||
XCTAssertTrue(model.runtime?.systemVersion.hasPrefix("iOS") ?? false)
|
||||
|
||||
XCTAssertNil(model.errorMessage)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import XCTest
|
||||
|
||||
@testable import MultiSimAdmin
|
||||
|
||||
final class TimestampTextTests: XCTestCase {
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
func testTodayOnlyShowsClockTime() {
|
||||
let output = TimestampText.short("2026-09-14 08:05:00", now: date(2026, 9, 14, 20, 0))
|
||||
XCTAssertEqual(output, "08:05")
|
||||
}
|
||||
|
||||
func testSameYearShowsMonthAndTime() {
|
||||
let output = TimestampText.short("2026-03-02 08:05:00", now: date(2026, 9, 14, 20, 0))
|
||||
XCTAssertEqual(output, "03-02 08:05")
|
||||
}
|
||||
|
||||
func testOtherYearFallsBackToDate() {
|
||||
let output = TimestampText.short("2025-12-31 23:59:00", now: date(2026, 9, 14, 20, 0))
|
||||
XCTAssertEqual(output, "2025-12-31")
|
||||
}
|
||||
|
||||
func testParsesEpochMilliseconds() {
|
||||
XCTAssertEqual(TimestampText.date(from: "1767484800000"), Date(timeIntervalSince1970: 1_767_484_800))
|
||||
}
|
||||
|
||||
func testParsesEpochSeconds() {
|
||||
XCTAssertEqual(TimestampText.date(from: "1767484800"), Date(timeIntervalSince1970: 1_767_484_800))
|
||||
}
|
||||
|
||||
func testZuluTimestampIsReadAsUTC() {
|
||||
let iso = try? XCTUnwrap(TimestampText.date(from: "2026-01-04T00:00:00Z"))
|
||||
XCTAssertEqual(iso, Date(timeIntervalSince1970: 1_767_484_800))
|
||||
}
|
||||
|
||||
func testUnparseableTextIsKeptAsIs() {
|
||||
XCTAssertEqual(TimestampText.short(" 刚刚 "), "刚刚")
|
||||
XCTAssertEqual(TimestampText.short(""), "")
|
||||
}
|
||||
|
||||
private func date(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int) -> Date {
|
||||
var components = DateComponents()
|
||||
components.year = year
|
||||
components.month = month
|
||||
components.day = day
|
||||
components.hour = hour
|
||||
components.minute = minute
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user