feat(ios): unify list cards and loading states

This commit is contained in:
chick
2026-09-10 01:35:12 +08:00
parent 995206ac23
commit c841748971
32 changed files with 1297 additions and 607 deletions
@@ -8,8 +8,47 @@ final class AIThemeTests: XCTestCase {
XCTAssertEqual(AITheme.buttonHeight, 44, accuracy: 0.1)
XCTAssertEqual(AITheme.compactButtonHeight, 40, accuracy: 0.1)
XCTAssertEqual(AITheme.largeButtonHeight, 50, accuracy: 0.1)
XCTAssertEqual(AITheme.toolbarButtonHeight, 38, accuracy: 0.1)
XCTAssertEqual(AITheme.buttonRadius, 12, accuracy: 0.1)
XCTAssertEqual(AITheme.iconButtonSize, 44, accuracy: 0.1)
XCTAssertEqual(AITheme.toolbarButtonMinWidth, 88, accuracy: 0.1)
XCTAssertEqual(AITheme.toolbarButtonWidth, 100, accuracy: 0.1)
XCTAssertEqual(AITheme.footerSecondaryButtonWidth, 100, accuracy: 0.1)
XCTAssertEqual(AITheme.listActionButtonWidth, 72, accuracy: 0.1)
XCTAssertEqual(AIButtonStyle.pressedScale, 0.97, accuracy: 0.01)
}
func testBottomActionsFillEqualWidths() {
let primary = AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true)
let secondary = AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true)
XCTAssertTrue(primary.fillsWidth)
XCTAssertTrue(secondary.fillsWidth)
}
func testFixedWidthDrawsInsideTheButtonStyle() {
let button = AIButtonStyle(
kind: .danger,
size: .compact,
fixedWidth: AITheme.listActionButtonWidth
)
XCTAssertTrue(button.fillsWidth == false)
XCTAssertEqual(AITheme.listActionButtonWidth, 72, accuracy: 0.1)
}
func testToolbarControlsUseUnifiedColorAndGeometry() {
let expected: [(Color, Color, CGFloat, CGFloat)] = [
(AITheme.surface, AITheme.ink, AITheme.toolbarButtonWidth, AITheme.toolbarButtonHeight),
(AITheme.primary, .white, AITheme.toolbarButtonWidth, AITheme.toolbarButtonHeight),
(AITheme.danger, .white, AITheme.toolbarButtonWidth, AITheme.toolbarButtonHeight),
]
for (background, foreground, width, height) in expected {
XCTAssertTrue(background == background)
XCTAssertTrue(foreground == foreground)
XCTAssertEqual(width, 100, accuracy: 0.1)
XCTAssertEqual(height, 38, accuracy: 0.1)
}
}
func testButtonKindsHaveDistinctBaseColors() {
@@ -21,6 +60,12 @@ final class AIThemeTests: XCTestCase {
XCTAssertEqual(Set(colors).count, 3)
}
func testSearchFieldExposesClearAction() throws {
let mirror = Mirror(reflecting: AISearchField("搜索", text: .constant("待清理")))
let text = try XCTUnwrap(mirror.children.first { $0.label == "_text" }?.value as? Binding<String>)
XCTAssertEqual(text.wrappedValue, "待清理")
}
func testDisabledKindsUseReducedButVisibleRendering() {
let disabledKinds = [
AIButtonStyle.Kind.primary,
@@ -0,0 +1,14 @@
import SwiftUI
import XCTest
@testable import MultiSimAdmin
final class ErrorMessageTests: XCTestCase {
func testNetworkFailureUsesPlainActionableCopy() {
XCTAssertEqual(ErrorMessage.network, "暂时无法连接实例,请稍后重试。")
}
func testUnauthorizedFailureUsesLoginGuidance() {
XCTAssertEqual(ErrorMessage.unauthorized, "实例登录状态已失效,请重新登录。")
}
}
@@ -0,0 +1,32 @@
import XCTest
@testable import MultiSimAdmin
final class FileCredentialStoreTests: XCTestCase {
func testSavesReadsAndRemovesPasswordInSandboxFile() throws {
let location = FileManager.default.temporaryDirectory
.appendingPathComponent("credential-store-\(UUID().uuidString).json")
let store = FileCredentialStore(location: location)
let instanceID = UUID()
XCTAssertNil(try store.get(instanceID: instanceID))
try store.set(instanceID: instanceID, password: "sandbox-secret")
XCTAssertEqual(try store.get(instanceID: instanceID), "sandbox-secret")
XCTAssertTrue(FileManager.default.fileExists(atPath: location.path))
try store.remove(instanceID: instanceID)
XCTAssertNil(try store.get(instanceID: instanceID))
}
func testCredentialFileIsPrivate() throws {
let location = FileManager.default.temporaryDirectory
.appendingPathComponent("credential-store-\(UUID().uuidString).json")
let store = FileCredentialStore(location: location)
try store.set(instanceID: UUID(), password: "private-secret")
let attributes = try FileManager.default.attributesOfItem(atPath: location.path)
XCTAssertEqual(attributes[.posixPermissions] as? Int, 0o600)
}
}
@@ -90,7 +90,7 @@ struct AlwaysOnlineClient: SimAdminClientProtocol {
}
}
final class NoopCredentialStore: CredentialStore {
final class NoopCredentialStore: @unchecked Sendable, CredentialStore {
private let lock = NSLock()
private var savedStorage: [UUID: String] = [:]
private var removedStorage: [UUID] = []
@@ -119,7 +119,7 @@ final class NoopCredentialStore: CredentialStore {
}
}
final class MemoryInstanceRepository: InstanceRepository {
final class MemoryInstanceRepository: @unchecked Sendable, InstanceRepository {
private let lock = NSLock()
private var itemsStorage: [Instance] = []
@@ -30,8 +30,11 @@ final class LogCenterTests: XCTestCase {
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.id, instance.id)
XCTAssertEqual(diagnostic.state, .online)
XCTAssertEqual(diagnostic.capabilities.total, 3)
XCTAssertEqual(diagnostic.capabilities.supported, 3)
XCTAssertEqual(diagnostic.featureStatus.map(\.state), [
.ready,
.ready,
.ready
])
XCTAssertEqual(diagnostic.connection?.total, 1)
XCTAssertEqual(diagnostic.connection?.availabilityPercent, 100)
}
@@ -54,8 +57,8 @@ final class LogCenterTests: XCTestCase {
XCTAssertEqual(snapshot.runtime.first?.message, "通知日志未同步:需要登录")
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.state, .online)
XCTAssertEqual(diagnostic.capabilities.authRequired, 1)
XCTAssertEqual(diagnostic.capabilities.supported, 2)
XCTAssertEqual(diagnostic.featureStatus.filter { $0.state == .requiresLogin }.count, 1)
XCTAssertEqual(diagnostic.featureStatus.filter { $0.state == .ready }.count, 2)
XCTAssertEqual(store.runtimeStorage.count, 1)
XCTAssertEqual(store.connectionStorage.count, 1)
}
@@ -76,8 +79,7 @@ final class LogCenterTests: XCTestCase {
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.state, .offline)
XCTAssertEqual(diagnostic.healthStatus, "超时")
XCTAssertEqual(diagnostic.capabilities.total, 3)
XCTAssertEqual(diagnostic.capabilities.unsupported, 3)
XCTAssertEqual(diagnostic.featureStatus.filter { $0.state == .unavailable }.count, 3)
}
@MainActor