同步 Android 启动页到 iOS,并在冷启动期间静默恢复登录态。
新增 SplashView、SplashCoordinator 与 Launch Screen 资源,移除冷启动 Lottie;LoginView 加载 App 配置。同时将 token 存储迁移至 UserDefaults,并更新相关测试与文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
87
suixinkanTests/AppConfigTests.swift
Normal file
87
suixinkanTests/AppConfigTests.swift
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// AppConfigTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// App 配置接口测试,覆盖 API 请求路径和登录页配置加载行为。
|
||||
final class AppConfigTests: XCTestCase {
|
||||
/// 测试 App 配置接口使用正确路径并解析 enable_register 字段。
|
||||
func testAppConfigAPIRequestsConfigEndpoint() async throws {
|
||||
let response = """
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"enable_register": true
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let session = AppConfigURLSession(responses: [response])
|
||||
let api = AppConfigAPI(client: APIClient(session: session))
|
||||
|
||||
let config = try await api.fetchAppConfig()
|
||||
|
||||
XCTAssertTrue(config.enableRegister)
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "GET")
|
||||
XCTAssertEqual(request.url?.path, "/api/app/config")
|
||||
}
|
||||
|
||||
/// 测试登录页加载 App 配置成功时会更新 enableRegister。
|
||||
func testLoginViewModelLoadAppConfigUpdatesEnableRegister() async throws {
|
||||
let response = """
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"enable_register": true
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let session = AppConfigURLSession(responses: [response])
|
||||
let api = AppConfigAPI(client: APIClient(session: session))
|
||||
let viewModel = LoginViewModel()
|
||||
|
||||
await viewModel.loadAppConfig(appConfigAPI: api)
|
||||
|
||||
XCTAssertTrue(viewModel.enableRegister)
|
||||
}
|
||||
|
||||
/// 测试登录页加载 App 配置失败时保持默认值。
|
||||
func testLoginViewModelLoadAppConfigKeepsDefaultOnFailure() async {
|
||||
let session = AppConfigURLSession(responses: [Data()])
|
||||
let api = AppConfigAPI(client: APIClient(session: session))
|
||||
let viewModel = LoginViewModel()
|
||||
|
||||
await viewModel.loadAppConfig(appConfigAPI: api)
|
||||
|
||||
XCTAssertFalse(viewModel.enableRegister)
|
||||
}
|
||||
}
|
||||
|
||||
/// App 配置测试用 URLSession 替身,按顺序返回预设响应。
|
||||
private final class AppConfigURLSession: URLSessionProtocol {
|
||||
private var responses: [Data]
|
||||
private(set) var requests: [URLRequest] = []
|
||||
|
||||
/// 初始化顺序响应。
|
||||
init(responses: [Data]) {
|
||||
self.responses = responses
|
||||
}
|
||||
|
||||
/// 记录请求并返回下一份响应。
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
requests.append(request)
|
||||
let data = responses.isEmpty ? Data() : responses.removeFirst()
|
||||
return (
|
||||
data,
|
||||
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -191,7 +191,7 @@ final class ProfileSecondaryPagesTests: XCTestCase {
|
||||
/// 测试接口数据已存在时不再被本地缓存覆盖。
|
||||
func testApplyCachedProfileIgnoredAfterUserInfoLoaded() {
|
||||
let viewModel = ProfileViewModel()
|
||||
viewModel.userInfo = UserInfoResponse(nickname: "接口昵称", phone: "13900000000")
|
||||
viewModel.userInfo = UserInfoResponse(phone: "13900000000", nickname: "接口昵称")
|
||||
viewModel.applyCachedProfile(
|
||||
AccountProfile(userId: "101", displayName: "缓存昵称", phone: "13800000000")
|
||||
)
|
||||
|
||||
68
suixinkanTests/SplashCoordinatorTests.swift
Normal file
68
suixinkanTests/SplashCoordinatorTests.swift
Normal file
@ -0,0 +1,68 @@
|
||||
//
|
||||
// SplashCoordinatorTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 启动页协调器测试,覆盖最短展示时长、bootstrap 并行和 UI Test 跳过策略。
|
||||
final class SplashCoordinatorTests: XCTestCase {
|
||||
/// 测试 bootstrap 完成后仍需等待最短展示时长。
|
||||
func testStartWaitsForMinimumDisplayDuration() async {
|
||||
let coordinator = SplashCoordinator(minimumDisplayDuration: 0.2, skipsMinimumDuration: false)
|
||||
let startedAt = Date()
|
||||
|
||||
await coordinator.start {
|
||||
try? await Task.sleep(nanoseconds: 10_000_000)
|
||||
}
|
||||
|
||||
XCTAssertTrue(coordinator.isFinished)
|
||||
XCTAssertGreaterThanOrEqual(Date().timeIntervalSince(startedAt), 0.19)
|
||||
}
|
||||
|
||||
/// 测试 bootstrap 耗时超过最短展示时长时,以 bootstrap 完成时间为准。
|
||||
func testStartWaitsForSlowBootstrap() async {
|
||||
let coordinator = SplashCoordinator(minimumDisplayDuration: 0.05, skipsMinimumDuration: false)
|
||||
let startedAt = Date()
|
||||
|
||||
await coordinator.start {
|
||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||
}
|
||||
|
||||
XCTAssertTrue(coordinator.isFinished)
|
||||
XCTAssertGreaterThanOrEqual(Date().timeIntervalSince(startedAt), 0.14)
|
||||
}
|
||||
|
||||
/// 测试 UI Test 模式会跳过最短展示时长。
|
||||
func testStartSkipsMinimumDurationWhenRequested() async {
|
||||
let coordinator = SplashCoordinator(minimumDisplayDuration: 1.0, skipsMinimumDuration: true)
|
||||
let startedAt = Date()
|
||||
|
||||
await coordinator.start {
|
||||
try? await Task.sleep(nanoseconds: 10_000_000)
|
||||
}
|
||||
|
||||
XCTAssertTrue(coordinator.isFinished)
|
||||
XCTAssertLessThan(Date().timeIntervalSince(startedAt), 0.2)
|
||||
}
|
||||
|
||||
/// 测试重复调用 start 不会重置已完成状态。
|
||||
func testStartIsIdempotentAfterFinished() async {
|
||||
let coordinator = SplashCoordinator(minimumDisplayDuration: 0, skipsMinimumDuration: true)
|
||||
var bootstrapCount = 0
|
||||
|
||||
await coordinator.start {
|
||||
bootstrapCount += 1
|
||||
}
|
||||
await coordinator.start {
|
||||
bootstrapCount += 1
|
||||
}
|
||||
|
||||
XCTAssertTrue(coordinator.isFinished)
|
||||
XCTAssertEqual(bootstrapCount, 1)
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,29 @@ import XCTest
|
||||
|
||||
/// 本地缓存 Store 测试,覆盖 UserDefaults 中的偏好和账号快照。
|
||||
final class StorageTests: XCTestCase {
|
||||
/// 测试 SessionTokenStore 使用 UserDefaults 保存、读取、修剪和清空正式 token。
|
||||
func testSessionTokenStorePersistsTokenInUserDefaults() throws {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
let store = SessionTokenStore(defaults: defaults, key: "session.token.test")
|
||||
|
||||
try store.save(" token-123 ")
|
||||
XCTAssertEqual(store.load(), "token-123")
|
||||
|
||||
try store.clear()
|
||||
XCTAssertNil(store.load())
|
||||
}
|
||||
|
||||
/// 测试保存空 token 会清空本地正式 token。
|
||||
func testSessionTokenStoreClearsWhenSavingBlankToken() throws {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
let store = SessionTokenStore(defaults: defaults, key: "session.token.blank.test")
|
||||
|
||||
try store.save("token-123")
|
||||
try store.save(" \n")
|
||||
|
||||
XCTAssertNil(store.load())
|
||||
}
|
||||
|
||||
/// 测试 AppPreferencesStore 能保存并读取上次登录手机号和协议状态。
|
||||
func testAppPreferencesStorePersistsLoginPreferences() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
|
||||
Reference in New Issue
Block a user