模块化 AppStore 并完善素材管理与个人空间设置。

将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 11:01:08 +08:00
parent ceca780ab3
commit 005349f8e6
128 changed files with 2953 additions and 1123 deletions

View File

@ -26,14 +26,14 @@ final class AMapBootstrapTests: XCTestCase {
}
func testConfigureReturnsFalseWhenPrivacyNotAccepted() {
appStore.privacyAgreementAccepted = false
appStore.session.privacyAgreementAccepted = false
XCTAssertFalse(AMapBootstrap.configureIfNeeded(appStore: appStore))
XCTAssertFalse(AMapBootstrap.isConfigured)
}
func testConfigureIsIdempotentWhenPrivacyAccepted() {
appStore.privacyAgreementAccepted = true
appStore.session.privacyAgreementAccepted = true
XCTAssertTrue(AMapBootstrap.configureIfNeeded(appStore: appStore))
XCTAssertTrue(AMapBootstrap.isConfigured)
@ -42,7 +42,7 @@ final class AMapBootstrapTests: XCTestCase {
}
func testRequireConfiguredThrowsWhenPrivacyNotAccepted() {
appStore.privacyAgreementAccepted = false
appStore.session.privacyAgreementAccepted = false
XCTAssertThrowsError(try AMapBootstrap.requireConfigured(appStore: appStore)) { error in
XCTAssertEqual(error as? LocationProviderError, .privacyNotAccepted)

View File

@ -96,4 +96,59 @@ final class APIClientTests: XCTestCase {
await fulfillment(of: [expectation], timeout: 1)
}
func testMultipartUploadBuildsAndroidCompatibleRequest() async throws {
let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
let client = APIClient(
environment: .testing,
session: session,
appVersion: "9.0.0",
osType: "iOS"
)
client.bindAuthTokenProvider { "avatar-token" }
let bytes = Data([0x01, 0x02, 0xFE, 0xFF])
let _: EmptyPayload = try await client.sendMultipart(
path: "/api/yf-handset-app/photog/userinfo-update-avatar",
fileName: "avatar.jpg",
mimeType: "image/jpeg",
data: bytes
)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.timeoutInterval, 60)
XCTAssertEqual(request.value(forHTTPHeaderField: "token"), "avatar-token")
XCTAssertEqual(request.value(forHTTPHeaderField: "X-APP-VERSION"), "9.0.0")
XCTAssertEqual(request.value(forHTTPHeaderField: "X-OS-TYPE"), "iOS")
XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=") == true)
let body = try XCTUnwrap(request.httpBody)
let bodyText = String(decoding: body, as: UTF8.self)
XCTAssertTrue(bodyText.contains("name=\"file\"; filename=\"avatar.jpg\""))
XCTAssertTrue(bodyText.contains("Content-Type: image/jpeg"))
XCTAssertNotNil(body.range(of: bytes))
}
func testMultipartUploadMapsBusinessFailure() async {
let session = MockURLSession(responses: [try! TestJSON.errorEnvelope(code: 100001, msg: "头像上传失败")])
let client = APIClient(environment: .testing, session: session)
do {
let _: EmptyPayload = try await client.sendMultipart(
path: "/api/yf-handset-app/photog/userinfo-update-avatar",
fileName: "avatar.jpg",
mimeType: "image/jpeg",
data: Data([1])
)
XCTFail("业务失败时应抛出 serverCode")
} catch let error as APIError {
guard case let .serverCode(code, message) = error else {
return XCTFail("期望 serverCode实际为 \(error)")
}
XCTAssertEqual(code, 100001)
XCTAssertEqual(message, "头像上传失败")
} catch {
XCTFail("期望 APIError实际为 \(error)")
}
}
}

View File

@ -19,9 +19,9 @@ final class AllFunctionsViewModelTests: XCTestCase {
defaults.removePersistentDomain(forName: "AllFunctionsViewModelTests")
appStore = AppStore(defaults: defaults)
menuStore = HomeCommonMenuStore(defaults: defaults)
appStore.userId = "1001"
appStore.accountType = "scenic_user"
appStore.roleCode = AppRoleCode.photographer.rawValue
appStore.session.userId = "1001"
appStore.session.accountType = .scenicUser
appStore.session.roleCode = AppRoleCode.photographer.rawValue
}
override func tearDown() {
@ -30,7 +30,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
}
func testLoadFunctionsUsesDefaultFirstFourWhenNoSavedURIs() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
@ -47,23 +47,23 @@ final class AllFunctionsViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["message_center"])
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
),
["wallet", "cloud_management", "task_management", "system_settings"]
)
}
func testLoadFunctionsUsesSavedURIsOverDefault() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
])
menuStore.saveCommonURIs(
["cloud_management"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
@ -74,14 +74,14 @@ final class AllFunctionsViewModelTests: XCTestCase {
}
func testAddToCommonMovesMenuAndPersists() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
])
menuStore.saveCommonURIs(
["wallet"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
@ -94,22 +94,22 @@ final class AllFunctionsViewModelTests: XCTestCase {
XCTAssertTrue(viewModel.didCustomize)
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
),
["wallet", "cloud_management"]
)
}
func testRemoveFromCommonMovesMenuAndPersists() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
])
menuStore.saveCommonURIs(
["wallet", "cloud_management"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
@ -123,14 +123,14 @@ final class AllFunctionsViewModelTests: XCTestCase {
}
func testPhotographerReportMenuVisibleAndCanBeAddedToCommon() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "位置上报", uri: "location_report"),
HomePermissionItem(id: "2", name: "举报摄影师", uri: "report_photographer"),
])
menuStore.saveCommonURIs(
["location_report"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)

View File

@ -0,0 +1,134 @@
//
// AppStoreModuleTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// AppStore
final class AppStoreModuleTests: XCTestCase {
private var suiteName: String!
private var defaults: UserDefaults!
private var appStore: AppStore!
override func setUp() {
super.setUp()
suiteName = "AppStoreModuleTests.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)
defaults.removePersistentDomain(forName: suiteName)
appStore = AppStore(defaults: defaults)
}
override func tearDown() {
defaults.removePersistentDomain(forName: suiteName)
appStore = nil
defaults = nil
suiteName = nil
super.tearDown()
}
func testAccountTypeKnownAndUnknownRawValueRoundTrips() {
XCTAssertEqual(AppAccountType(rawValue: "scenic_user"), .scenicUser)
XCTAssertEqual(AppAccountType(rawValue: "store_user"), .storeUser)
XCTAssertEqual(AppAccountType(rawValue: "future_account"), .unknown("future_account"))
XCTAssertEqual(AppAccountType.scenicUser.rawValue, "scenic_user")
XCTAssertEqual(AppAccountType.storeUser.rawValue, "store_user")
XCTAssertEqual(AppAccountType.unknown("future_account").rawValue, "future_account")
}
func testSessionUsesExistingKeysAndPreservesUnknownAccountType() {
defaults.set("legacy-token", forKey: "key_in_token")
defaults.set("future_account", forKey: "key_in_account_type")
XCTAssertEqual(appStore.session.token, "legacy-token")
XCTAssertEqual(appStore.session.accountType, .unknown("future_account"))
appStore.session.userId = "1001"
appStore.session.currentScenicId = 22
XCTAssertEqual(defaults.string(forKey: "key_in_user_id"), "1001")
XCTAssertEqual(defaults.string(forKey: "key_in_current_scenic_id"), "22")
XCTAssertEqual(appStore.session.accountCachePrefix, "1001_future_account")
}
func testFeatureStoresAreIsolatedByAccountAndDecodeSnapshots() {
selectAccount(userId: "1001", type: .scenicUser)
let permissions = [HomePermissionItem(id: "1", name: "钱包", uri: "wallet")]
let scenicList = [ScenicInfo(id: 22, name: "测试景区", status: 1)]
var queueSettings = ScenicQueueSettingsSnapshot()
queueSettings.shootMinute = 8
appStore.permissions.savePermissionItems(permissions)
appStore.permissions.saveRoleScenicList(scenicList)
appStore.location.onlineStatus = true
appStore.location.lastReportTime = 123_456
appStore.location.reminderMinutes = 10
appStore.payment.isOpenReceiveVoice = true
appStore.scenicQueue.punchSpotId = "33"
appStore.scenicQueue.punchSpotName = "东门"
appStore.scenicQueue.saveSettingsSnapshot(queueSettings)
appStore.scenicQueue.savePresetVoices([" A ", "", "B"])
selectAccount(userId: "2002", type: .storeUser)
XCTAssertTrue(appStore.permissions.permissionItems().isEmpty)
XCTAssertTrue(appStore.permissions.roleScenicList().isEmpty)
XCTAssertFalse(appStore.location.onlineStatus)
XCTAssertEqual(appStore.location.lastReportTime, 0)
XCTAssertEqual(appStore.location.reminderMinutes, 0)
XCTAssertFalse(appStore.payment.isOpenReceiveVoice)
XCTAssertEqual(appStore.scenicQueue.punchSpotId, "")
XCTAssertNil(appStore.scenicQueue.settingsSnapshot())
selectAccount(userId: "1001", type: .scenicUser)
XCTAssertEqual(appStore.permissions.permissionItems(), permissions)
XCTAssertEqual(appStore.permissions.roleScenicList(), scenicList)
XCTAssertTrue(appStore.location.onlineStatus)
XCTAssertEqual(appStore.location.lastReportTime, 123_456)
XCTAssertEqual(appStore.location.reminderMinutes, 10)
XCTAssertTrue(appStore.payment.isOpenReceiveVoice)
XCTAssertTrue(appStore.scenicQueue.hasPunchSpotSaved())
XCTAssertEqual(appStore.scenicQueue.punchSpotName, "东门")
XCTAssertEqual(appStore.scenicQueue.settingsSnapshot(), queueSettings)
XCTAssertEqual(appStore.scenicQueue.presetVoices(), ["A", "B"])
}
func testLogoutClearsCurrentAccountModulesAndPreservesLoginPreferences() {
appStore.session.lastLoginUsername = "13800000000"
appStore.session.privacyAgreementAccepted = true
selectAccount(userId: "other", type: .storeUser)
appStore.scenicQueue.punchSpotId = "99"
selectAccount(userId: "current", type: .scenicUser)
appStore.session.token = "token"
appStore.permissions.legacyRoleId = 41
appStore.permissions.savePermissionItems([HomePermissionItem(id: "1", name: "钱包", uri: "wallet")])
appStore.location.onlineStatus = true
appStore.payment.isOpenReceiveVoice = true
appStore.scenicQueue.punchSpotId = "33"
appStore.logout()
XCTAssertFalse(appStore.session.isLoggedIn)
XCTAssertEqual(appStore.session.userId, "")
XCTAssertEqual(appStore.session.lastLoginUsername, "13800000000")
XCTAssertTrue(appStore.session.privacyAgreementAccepted)
selectAccount(userId: "current", type: .scenicUser)
XCTAssertEqual(appStore.permissions.legacyRoleId, 0)
XCTAssertTrue(appStore.permissions.permissionItems().isEmpty)
XCTAssertFalse(appStore.location.onlineStatus)
XCTAssertFalse(appStore.payment.isOpenReceiveVoice)
XCTAssertEqual(appStore.scenicQueue.punchSpotId, "")
selectAccount(userId: "other", type: .storeUser)
XCTAssertEqual(appStore.scenicQueue.punchSpotId, "99")
}
private func selectAccount(userId: String, type: AppAccountType) {
appStore.session.userId = userId
appStore.session.accountType = type
}
}

View File

@ -16,7 +16,7 @@ final class CooperationAcquirerViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "CooperationAcquirerViewModelTests")!
defaults.removePersistentDomain(forName: "CooperationAcquirerViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.currentStoreId = 5
appStore.session.currentStoreId = 5
}
func testSaveRemarkWithValidLengthCallsAPI() async throws {

View File

@ -10,8 +10,8 @@ import XCTest
final class DepositOrderListViewModelTests: XCTestCase {
override func tearDown() {
AppStore.shared.currentStoreId = 0
AppStore.shared.currentScenicId = 0
AppStore.shared.session.currentStoreId = 0
AppStore.shared.session.currentScenicId = 0
super.tearDown()
}
@ -34,7 +34,7 @@ final class DepositOrderListViewModelTests: XCTestCase {
}
func testStoreIdZeroAbortsWithMessage() async {
AppStore.shared.currentStoreId = 0
AppStore.shared.session.currentStoreId = 0
let viewModel = DepositOrderListViewModel()
var message: String?
viewModel.onShowMessage = { message = $0 }

View File

@ -16,8 +16,8 @@ final class HomeLocationStateStoreTests: XCTestCase {
defaults = UserDefaults(suiteName: "HomeLocationStateStoreTests")!
defaults.removePersistentDomain(forName: "HomeLocationStateStoreTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "1001"
appStore.accountType = "scenic_user"
appStore.session.userId = "1001"
appStore.session.accountType = .scenicUser
}
override func tearDown() {
@ -27,21 +27,21 @@ final class HomeLocationStateStoreTests: XCTestCase {
func testExpiredOnlineSessionIsClearedOnRestore() {
let expiredTimestamp = Int64(Date().timeIntervalSince1970 * 1000) - HomeLocationStateStore.onlineDurationMillis - 1_000
appStore.onlineStatus = true
appStore.lastLocationReportTime = expiredTimestamp
appStore.location.onlineStatus = true
appStore.location.lastReportTime = expiredTimestamp
let stateStore = HomeLocationStateStore(store: appStore)
stateStore.restoreStateIfNeeded()
XCTAssertFalse(stateStore.isOnline)
XCTAssertFalse(appStore.onlineStatus)
XCTAssertEqual(appStore.lastLocationReportTime, 0)
XCTAssertFalse(appStore.location.onlineStatus)
XCTAssertEqual(appStore.location.lastReportTime, 0)
}
func testActiveOnlineSessionRestoresCountdown() {
let recentTimestamp = Int64(Date().timeIntervalSince1970 * 1000) - 60_000
appStore.onlineStatus = true
appStore.lastLocationReportTime = recentTimestamp
appStore.location.onlineStatus = true
appStore.location.lastReportTime = recentTimestamp
let stateStore = HomeLocationStateStore(store: appStore)
stateStore.restoreStateIfNeeded()
@ -56,7 +56,7 @@ final class HomeLocationStateStoreTests: XCTestCase {
}
func testShouldTriggerTimeoutReminderWithinWindow() {
appStore.locationReminderMinutes = 10
appStore.location.reminderMinutes = 10
let stateStore = HomeLocationStateStore(store: appStore)
stateStore.startLocationReport(at: Int64(Date().timeIntervalSince1970 * 1000))
stateStore.updateReminderMinutes(10)
@ -124,22 +124,22 @@ final class HomeLocationStateStoreTests: XCTestCase {
}
func testRestoreClearsOnlineStateWhenStoredLastReportTimeIsMissing() {
appStore.onlineStatus = true
appStore.lastLocationReportTime = 0
appStore.location.onlineStatus = true
appStore.location.lastReportTime = 0
let stateStore = HomeLocationStateStore(store: appStore)
stateStore.restoreStateIfNeeded()
XCTAssertFalse(stateStore.isOnline)
XCTAssertFalse(appStore.onlineStatus)
XCTAssertEqual(appStore.lastLocationReportTime, 0)
XCTAssertFalse(appStore.location.onlineStatus)
XCTAssertEqual(appStore.location.lastReportTime, 0)
XCTAssertEqual(stateStore.nextReportCountdownSeconds, 0)
}
func testRestoreClearsRuntimeStateWhenPersistedOffline() {
let stateStore = HomeLocationStateStore(store: appStore)
stateStore.startLocationReport(at: Int64(Date().timeIntervalSince1970 * 1000))
appStore.onlineStatus = false
appStore.location.onlineStatus = false
stateStore.restoreStateIfNeeded()

View File

@ -18,12 +18,12 @@ final class HomeViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "HomeViewModelTests")!
defaults.removePersistentDomain(forName: "HomeViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "2001"
appStore.accountType = "scenic_user"
appStore.roleCode = AppRoleCode.photographer.rawValue
appStore.roleName = "摄影师"
appStore.currentScenicId = 10
appStore.currentScenicName = "测试景区"
appStore.session.userId = "2001"
appStore.session.accountType = .scenicUser
appStore.session.roleCode = AppRoleCode.photographer.rawValue
appStore.session.roleName = "摄影师"
appStore.session.currentScenicId = 10
appStore.session.currentScenicName = "测试景区"
}
override func tearDown() {
@ -32,12 +32,12 @@ final class HomeViewModelTests: XCTestCase {
}
func testMinimalTopRoleFlag() {
appStore.roleCode = AppRoleCode.storeAdmin.rawValue
appStore.session.roleCode = AppRoleCode.storeAdmin.rawValue
let viewModel = HomeViewModel(appStore: appStore)
viewModel.refreshLocalDisplayState()
XCTAssertTrue(viewModel.isMinimalTopRole)
appStore.roleCode = AppRoleCode.photographer.rawValue
appStore.session.roleCode = AppRoleCode.photographer.rawValue
viewModel.refreshLocalDisplayState()
XCTAssertFalse(viewModel.isMinimalTopRole)
}
@ -77,7 +77,7 @@ final class HomeViewModelTests: XCTestCase {
}
func testRebuildCommonMenusFiltersUnknownURIs() {
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "未知", uri: "unknown_uri"),
])
@ -88,7 +88,7 @@ final class HomeViewModelTests: XCTestCase {
func testRebuildCommonMenusPersistsDefaultURIsOnFirstLoad() {
let menuStore = HomeCommonMenuStore(defaults: defaults)
appStore.savePermissionItems([
appStore.permissions.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
@ -104,8 +104,8 @@ final class HomeViewModelTests: XCTestCase {
])
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
),
["wallet", "cloud_management", "task_management", "system_settings"]
)
@ -124,7 +124,7 @@ final class HomeViewModelTests: XCTestCase {
}
func testRolePermissionBuildsCommonMenusAndStoreCardRole() async throws {
appStore.roleCode = AppRoleCode.storeAdmin.rawValue
appStore.session.roleCode = AppRoleCode.storeAdmin.rawValue
let permissionResponse = RolePermissionResponse(
role: RoleInfo(
id: 46,
@ -170,7 +170,7 @@ final class HomeViewModelTests: XCTestCase {
),
scenic: []
)
appStore.savePermissionItems([HomePermissionItem(id: "1", name: "钱包", uri: "wallet")])
appStore.permissions.savePermissionItems([HomePermissionItem(id: "1", name: "钱包", uri: "wallet")])
let permissionJSON = try TestJSON.envelope(data: [permissionResponse])
let session = MockURLSession(responses: [permissionJSON])
@ -185,26 +185,26 @@ final class HomeViewModelTests: XCTestCase {
}
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
appStore.currentScenicId = 10
appStore.currentScenicName = ""
appStore.session.currentScenicId = 10
appStore.session.currentScenicName = ""
let viewModel = HomeViewModel(appStore: appStore)
viewModel.refreshLocalDisplayState()
XCTAssertEqual(viewModel.currentScenicName, "")
XCTAssertEqual(appStore.currentScenicId, 0)
XCTAssertEqual(appStore.currentScenicName, "")
XCTAssertEqual(appStore.session.currentScenicId, 0)
XCTAssertEqual(appStore.session.currentScenicName, "")
}
func testRefreshLocalDisplayStateClearsWhenIdMissing() {
appStore.currentScenicId = 0
appStore.currentScenicName = "残留名称"
appStore.session.currentScenicId = 0
appStore.session.currentScenicName = "残留名称"
let viewModel = HomeViewModel(appStore: appStore)
viewModel.refreshLocalDisplayState()
XCTAssertEqual(viewModel.currentScenicName, "")
XCTAssertEqual(appStore.currentScenicName, "")
XCTAssertEqual(appStore.session.currentScenicName, "")
}
private func statusActionContainer(in view: HomeQuickActionsView) throws -> UIView {

View File

@ -23,7 +23,7 @@ final class LocationProviderTests: XCTestCase {
func testScenicSelectionUsesInjectedProviderSnapshot() async {
let mock = MockLocationProvider()
mock.snapshot = HomeLocationSnapshot(latitude: 30.2, longitude: 120.1, address: "杭州")
appStore.saveRoleScenicList([
appStore.permissions.saveRoleScenicList([
ScenicInfo(id: 1, name: "西湖", status: 1, location: ScenicLocationInfo(lng: 120.1, lat: 30.2, address: "杭州"), coverImg: nil),
])

View File

@ -17,7 +17,7 @@ final class LocationReportHistoryViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "LocationReportHistoryViewModelTests")!
defaults.removePersistentDomain(forName: "LocationReportHistoryViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "staff-99"
appStore.session.userId = "staff-99"
}
override func tearDown() {

View File

@ -17,8 +17,8 @@ final class LocationReportServiceTests: XCTestCase {
defaults = UserDefaults(suiteName: "LocationReportServiceTests")!
defaults.removePersistentDomain(forName: "LocationReportServiceTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "staff-1"
appStore.currentScenicId = 100
appStore.session.userId = "staff-1"
appStore.session.currentScenicId = 100
}
override func tearDown() {
@ -122,7 +122,7 @@ final class LocationReportServiceTests: XCTestCase {
}
func testMissingScenicThrows() async {
appStore.currentScenicId = 0
appStore.session.currentScenicId = 0
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [])))
let stateStore = HomeLocationStateStore(store: appStore)
let service = LocationReportService(appStore: appStore, locationStateStore: stateStore)

View File

@ -10,17 +10,27 @@ import XCTest
/// ViewModel
@MainActor
final class MaterialManagementViewModelTests: XCTestCase {
func testDetailCarouselLoopsAcrossCopiedBoundaryPages() {
XCTAssertEqual(MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: 3), 1)
XCTAssertEqual(MaterialCarouselIndexMapper.normalizedRenderedPage(0, mediaCount: 3), 3)
XCTAssertEqual(MaterialCarouselIndexMapper.logicalPage(renderedPage: 3, mediaCount: 3), 2)
XCTAssertEqual(MaterialCarouselIndexMapper.normalizedRenderedPage(4, mediaCount: 3), 1)
XCTAssertEqual(MaterialCarouselIndexMapper.logicalPage(renderedPage: 1, mediaCount: 3), 0)
XCTAssertEqual(MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: 1), 0)
}
func testListLoadsMoreSearchesAndMapsFilterStatus() async {
let api = MockMaterialAPI()
api.listResponses = [
MaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }, order: MaterialOrderInfo(totalNum: 1386, avgOrderAmount: "12", refundTotal: "3")),
MaterialListResponse(total: 12, list: [makeItem(id: 10), makeItem(id: 11)]),
MaterialListResponse(total: 12, list: [makeItem(id: 9), makeItem(id: 10), makeItem(id: 11)]),
MaterialListResponse(total: 1, list: [makeItem(id: 99)]),
]
let viewModel = MaterialListViewModel()
await viewModel.loadInitial(api: api)
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
XCTAssertEqual(viewModel.items.map(\.id), Array(0 ... 11))
await viewModel.selectFilter(.pending, api: api)
XCTAssertEqual(api.listCalls.map(\.status), [4, 4, 0])
@ -118,43 +128,69 @@ final class MaterialManagementViewModelTests: XCTestCase {
XCTAssertNil(api.lastUploadRequest?.id)
}
func testCloudMediaImportBuildsMaterialRequestWithoutUploadingAgain() async {
let api = MockMaterialAPI()
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
func testChangingMediaTypeClearsSelectedMedia() async {
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
var didSucceed = false
viewModel.onSubmitSuccess = { didSucceed = true }
await viewModel.addLocalMedia(
[makeUploadItem(fileName: "image.jpg")],
uploader: MockMaterialUploader(urls: ["https://cdn/image.jpg"])
)
await viewModel.loadInitial(api: api)
viewModel.updateMaterialName("云盘素材")
viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
viewModel.addCloudMediaFiles([
CloudFile(id: 31, fileUrl: "https://cdn/cloud.jpg", coverUrl: "https://cdn/cover-thumb.jpg", name: "cloud.jpg", type: 2, fileSize: 88),
])
await viewModel.setCoverImage(makeUploadItem(fileName: "cover.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/cover.jpg"]))
await viewModel.submit(api: api)
viewModel.selectMediaType(.video)
XCTAssertTrue(didSucceed)
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([31]))
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.originalName, "cloud.jpg")
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/cloud.jpg")
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.size, 88)
XCTAssertEqual(viewModel.mediaType, .video)
XCTAssertTrue(viewModel.uploadedMediaList.isEmpty)
}
func testCloudMediaImportFiltersByCurrentMaterialTypeAndDeduplicates() {
func testMediaLimitsAndSequentialUploadMatchAndroid() async {
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
let imageItems = (0 ..< 11).map { makeUploadItem(fileName: "\($0).jpg") }
let uploader = MockMaterialUploader(urls: (0 ..< 11).map { "https://cdn/\($0).jpg" })
viewModel.addCloudMediaFiles([
CloudFile(id: 1, fileUrl: "https://cdn/video.mp4", name: "video.mp4", type: 1),
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
])
await viewModel.addLocalMedia(imageItems, uploader: uploader)
XCTAssertEqual(viewModel.uploadedMediaList.map(\.ossUrl), ["https://cdn/image.jpg"])
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([2]))
XCTAssertTrue(messages.isEmpty)
XCTAssertEqual(viewModel.uploadedMediaList.count, 9)
XCTAssertEqual(uploader.uploadedFileNames, (0 ..< 9).map { "\($0).jpg" })
viewModel.selectMediaType(.video)
let videoUploader = MockMaterialUploader(urls: ["https://cdn/a.mp4", "https://cdn/b.mp4"])
await viewModel.addLocalMedia(
[makeUploadItem(fileName: "a.mp4"), makeUploadItem(fileName: "b.mp4")],
uploader: videoUploader
)
XCTAssertEqual(viewModel.uploadedMediaList.map(\.fileName), ["a.mp4"])
XCTAssertEqual(videoUploader.uploadedFileNames, ["a.mp4"])
}
func testTagDeletionRequiresConfirmation() async {
let api = MockMaterialAPI()
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
viewModel.updateTagInput("snow-tag")
await viewModel.addTag(api: api)
let tag = try! XCTUnwrap(viewModel.tagList.first)
viewModel.requestDeleteTag(tag)
XCTAssertEqual(viewModel.pendingDeleteTag, tag)
XCTAssertEqual(viewModel.tagList, [tag])
viewModel.cancelDeleteTag()
XCTAssertNil(viewModel.pendingDeleteTag)
XCTAssertEqual(viewModel.tagList, [tag])
viewModel.requestDeleteTag(tag)
viewModel.confirmDeleteTag()
XCTAssertNil(viewModel.pendingDeleteTag)
XCTAssertTrue(viewModel.tagList.isEmpty)
}
func testInputLengthsAreCappedLikeAndroid() {
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
viewModel.updateMaterialName(String(repeating: "", count: 35))
viewModel.updateTagInput(String(repeating: "", count: 20))
XCTAssertEqual(viewModel.materialName.count, 30)
XCTAssertEqual(viewModel.tagInput.count, 15)
}
func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
@ -261,6 +297,7 @@ private final class MockMaterialAPI: MaterialManagementServing {
@MainActor
private final class MockMaterialUploader: MaterialOSSUploading {
private var urls: [String]
private(set) var uploadedFileNames: [String] = []
init(urls: [String]) {
self.urls = urls
@ -273,6 +310,7 @@ private final class MockMaterialUploader: MaterialOSSUploading {
scenicId: Int,
onProgress: @escaping (Int) -> Void
) async throws -> String {
uploadedFileNames.append(fileName)
onProgress(100)
return urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst()
}

View File

@ -10,9 +10,9 @@ import XCTest
final class OrderListViewModelTests: XCTestCase {
override func tearDown() {
AppStore.shared.roleCode = ""
AppStore.shared.roleName = ""
AppStore.shared.currentScenicId = 0
AppStore.shared.session.roleCode = ""
AppStore.shared.session.roleName = ""
AppStore.shared.session.currentScenicId = 0
super.tearDown()
}
@ -37,12 +37,12 @@ final class OrderListViewModelTests: XCTestCase {
}
func testRoleRoutingPath() {
AppStore.shared.roleCode = AppRoleCode.scenicAdmin.rawValue
AppStore.shared.session.roleCode = AppRoleCode.scenicAdmin.rawValue
let scenicVM = OrderListViewModel()
XCTAssertEqual(scenicVM.listAPIPath, "/api/app/scenic-admin/order/list")
XCTAssertTrue(scenicVM.isScenicAdminReadOnly)
AppStore.shared.roleCode = AppRoleCode.photographer.rawValue
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
let photoVM = OrderListViewModel()
XCTAssertEqual(photoVM.listAPIPath, "/api/yf-handset-app/photog/order/listv2")
XCTAssertFalse(photoVM.isScenicAdminReadOnly)

View File

@ -16,9 +16,9 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests")!
defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "10001"
appStore.currentScenicId = 10
appStore.currentScenicName = "测试景区"
appStore.session.userId = "10001"
appStore.session.currentScenicId = 10
appStore.session.currentScenicName = "测试景区"
}
override func tearDown() {
@ -76,12 +76,12 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.isVoiceBroadcastOpen)
viewModel.toggleReceiveVoice()
XCTAssertTrue(appStore.isOpenReceiveVoice)
XCTAssertTrue(appStore.payment.isOpenReceiveVoice)
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
}
func testLoadPayCodeClearsQRWhenScenicMissing() async {
appStore.currentScenicId = 0
appStore.session.currentScenicId = 0
let session = MockURLSession(responses: [])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)

View File

@ -54,7 +54,7 @@ final class PaymentCollectionRecordViewModelTests: XCTestCase {
let defaults = UserDefaults(suiteName: "PaymentCollectionRecordViewModelTests")!
defaults.removePersistentDomain(forName: "PaymentCollectionRecordViewModelTests")
let appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 10
appStore.session.currentScenicId = 10
let recordJSON = """
{"code":100000,"msg":"success","data":{"analyse":[{"date":"2024-01-15","order_count":1,"order_amount_sum":"50.00"}],"list":[{"order_number":"NO001","user_phone":"13800138000","order_amount":"50.00","create_date":"2024-01-15","create_time":"10:00:00"}]}}

View File

@ -17,7 +17,7 @@ final class PermissionApplyViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "PermissionApplyViewModelTests")!
defaults.removePersistentDomain(forName: "PermissionApplyViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.saveRolePermissionList([
appStore.permissions.saveRolePermissionList([
RolePermissionResponse(
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
scenic: [ScenicInfo(id: 100, name: "已有景区", status: 1)]

View File

@ -0,0 +1,47 @@
//
// ProfileAvatarCropTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
///
final class ProfileAvatarCropTests: XCTestCase {
func testCropOutputIs480PixelSquare() throws {
let source = UIGraphicsImageRenderer(size: CGSize(width: 800, height: 600)).image { context in
UIColor.red.setFill()
context.fill(CGRect(x: 0, y: 0, width: 800, height: 600))
}
let controller = ProfileAvatarCropViewController(image: source) { _ in }
controller.loadViewIfNeeded()
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
controller.view.setNeedsLayout()
controller.view.layoutIfNeeded()
let data = try XCTUnwrap(controller.renderedCropData())
let image = try XCTUnwrap(UIImage(data: data))
XCTAssertEqual(image.size.width * image.scale, 480)
XCTAssertEqual(image.size.height * image.scale, 480)
}
func testCropNormalizesImageOrientation() throws {
let base = UIGraphicsImageRenderer(size: CGSize(width: 600, height: 800)).image { context in
UIColor.blue.setFill()
context.fill(CGRect(x: 0, y: 0, width: 600, height: 800))
}
let cgImage = try XCTUnwrap(base.cgImage)
let rotated = UIImage(cgImage: cgImage, scale: 1, orientation: .left)
let controller = ProfileAvatarCropViewController(image: rotated) { _ in }
controller.loadViewIfNeeded()
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
controller.view.layoutIfNeeded()
let data = try XCTUnwrap(controller.renderedCropData())
let output = try XCTUnwrap(UIImage(data: data))
XCTAssertEqual(output.imageOrientation, .up)
XCTAssertEqual(output.size.width * output.scale, 480)
XCTAssertEqual(output.size.height * output.scale, 480)
}
}

View File

@ -57,7 +57,7 @@ final class ProfileEditViewModelTests: XCTestCase {
)
await fulfillment(of: [expectation], timeout: 1)
XCTAssertEqual(AppStore.shared.userName, "新昵称")
XCTAssertEqual(AppStore.shared.session.userName, "新昵称")
}
}

View File

@ -20,14 +20,14 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "ProfileSpaceSettingsViewModelTests")!
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 11
appStore.session.currentScenicId = 11
calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
fixedNow = calendar.date(from: DateComponents(year: 2026, month: 7, day: 8))!
}
override func tearDown() {
AppStore.shared.currentScenicId = 0
AppStore.shared.session.currentScenicId = 0
appStore.logout()
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
super.tearDown()
@ -170,7 +170,8 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
viewModel.updateHolidayEndTime("20:00")
viewModel.updateBusinessEndTime("08:00")
XCTAssertThrowsError(try viewModel.makeSaveRequest())
XCTAssertEqual(viewModel.businessEndTime, "18:00")
XCTAssertNoThrow(try viewModel.makeSaveRequest())
}
func testHasChangesAndSaveRequestFieldsMatchAndroid() async throws {
@ -250,7 +251,7 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
}
func testSpaceSettingsRoutePushesProfileSpaceSettingsPage() {
AppStore.shared.currentScenicId = 11
AppStore.shared.session.currentScenicId = 11
let root = UIViewController()
let navigationController = UINavigationController(rootViewController: root)
@ -259,8 +260,26 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
XCTAssertTrue(navigationController.topViewController is ProfileSpaceSettingsViewController)
}
func testCertificationExpansionAndAvatarUploadRefresh() async {
let response = PhotographerProfileInfoResponse.loaded(certifications: ["1", "2", "3", "4"])
let api = FakeProfileSpaceSettingsAPI(response: response)
let viewModel = makeViewModel()
await viewModel.load(api: api)
XCTAssertFalse(viewModel.isCertificationExpanded)
viewModel.toggleCertificationExpanded()
XCTAssertTrue(viewModel.isCertificationExpanded)
let avatarData = Data([1, 2, 3])
await viewModel.uploadAvatar(data: avatarData, fileName: "avatar.jpg", api: api)
XCTAssertEqual(api.uploadedAvatarData, avatarData)
XCTAssertEqual(api.uploadedAvatarFileName, "avatar.jpg")
XCTAssertEqual(viewModel.avatarURL, "https://example.com/new-avatar.jpg")
}
func testSystemSettingsRoutePushesSettingPage() {
AppStore.shared.currentScenicId = 11
AppStore.shared.session.currentScenicId = 11
let tabBarController = MainTabBarController()
tabBarController.loadViewIfNeeded()
guard
@ -292,6 +311,8 @@ private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
var lastUpdateRequest: UpdatePhotographerProfileInfoRequest?
var lastAddScheduleRequest: AddPhotographerScheduleRequest?
var lastDeletedScheduleId: Int?
var uploadedAvatarData: Data?
var uploadedAvatarFileName: String?
init(response: PhotographerProfileInfoResponse) {
self.response = response
@ -335,10 +356,40 @@ private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
func deleteSchedule(id: Int) async throws {
lastDeletedScheduleId = id
}
func uploadAvatar(data: Data, fileName: String) async throws {
uploadedAvatarData = data
uploadedAvatarFileName = fileName
response = PhotographerProfileInfoResponse(
id: response.id,
scenicId: response.scenicId,
photogUid: response.photogUid,
realName: response.realName,
nickname: response.nickname,
avatar: "https://example.com/new-avatar.jpg",
scenicCertification: response.scenicCertification,
description: response.description,
attrLabel: response.attrLabel,
shootLabel: response.shootLabel,
cameraDevice: response.cameraDevice,
businessStartTime: response.businessStartTime,
businessEndTime: response.businessEndTime,
holidayStartTime: response.holidayStartTime,
holidayEndTime: response.holidayEndTime,
businessRange: response.businessRange,
acceptOrderStatus: response.acceptOrderStatus,
schedule: response.schedule,
virtualPhone: response.virtualPhone
)
}
}
private extension PhotographerProfileInfoResponse {
static func loaded() -> PhotographerProfileInfoResponse {
loaded(certifications: ["西湖"])
}
static func loaded(certifications: [String]) -> PhotographerProfileInfoResponse {
PhotographerProfileInfoResponse(
id: 1,
scenicId: 11,
@ -346,7 +397,7 @@ private extension PhotographerProfileInfoResponse {
realName: "林摄影",
nickname: "小林",
avatar: "https://example.com/a.jpg",
scenicCertification: ["西湖"],
scenicCertification: certifications,
description: "简介",
attrLabel: ["航拍"],
shootLabel: ["亲子"],

View File

@ -15,10 +15,10 @@ final class ProfileViewModelTests: XCTestCase {
}
func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
AppStore.shared.roleCode = AppRoleCode.photographer.rawValue
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
let viewModel = ProfileViewModel()
XCTAssertTrue(viewModel.showPhotographerFields)
AppStore.shared.roleCode = ""
AppStore.shared.session.roleCode = ""
}
func testMaskCardNumberShowsLastFourDigits() {
@ -45,24 +45,24 @@ final class ProfileViewModelTests: XCTestCase {
}
func testAccountTypeLabelForStoreAccount() {
AppStore.shared.accountType = V9StoreUser.accountTypeValue
AppStore.shared.session.accountType = .storeUser
let viewModel = ProfileViewModel()
XCTAssertEqual(viewModel.accountTypeLabel, "门店账号")
XCTAssertTrue(viewModel.isStoreAccount)
AppStore.shared.accountType = ""
AppStore.shared.session.accountType = .unknown("")
}
func testAccountTypeLabelForScenicAccount() {
AppStore.shared.accountType = V9ScenicUser.accountTypeValue
AppStore.shared.session.accountType = .scenicUser
let viewModel = ProfileViewModel()
XCTAssertEqual(viewModel.accountTypeLabel, "景区账号")
XCTAssertFalse(viewModel.isStoreAccount)
AppStore.shared.accountType = ""
AppStore.shared.session.accountType = .unknown("")
}
func testApplyLocalProfileUpdateSyncsNicknameAndAvatar() {
AppStore.shared.userName = "全局昵称"
AppStore.shared.avatar = "https://cdn.example.com/new.jpg"
AppStore.shared.session.userName = "全局昵称"
AppStore.shared.session.avatar = "https://cdn.example.com/new.jpg"
let viewModel = ProfileViewModel()
viewModel.applyLocalProfileUpdate()
@ -70,7 +70,7 @@ final class ProfileViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.displayNickname, "全局昵称")
XCTAssertEqual(viewModel.displayAvatarURL, "https://cdn.example.com/new.jpg")
AppStore.shared.userName = ""
AppStore.shared.avatar = ""
AppStore.shared.session.userName = ""
AppStore.shared.session.avatar = ""
}
}

View File

@ -118,9 +118,9 @@ final class ScenicQueueFeatureTests: XCTestCase {
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.settings")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.settings")
let store = AppStore(defaults: defaults)
store.userId = "u1"
store.accountType = "photog"
store.currentScenicId = 11
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
let api = MockScenicQueueAPI()
let tts = MockScenicQueueTTSManager()
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
@ -165,20 +165,20 @@ final class ScenicQueueFeatureTests: XCTestCase {
XCTAssertEqual(request?.missCallRequeueOffset, 2)
XCTAssertEqual(request?.autoCallNextCount, 3)
XCTAssertEqual(request?.voiceBroadcasts.map(\.content), ["请准备拍摄", "第二条"])
XCTAssertEqual(store.scenicQueuePunchSpotName, "东门打卡点")
XCTAssertEqual(store.scenicQueueSettingsSnapshot()?.shootMinute, 9)
XCTAssertEqual(store.scenicQueue.punchSpotName, "东门打卡点")
XCTAssertEqual(store.scenicQueue.settingsSnapshot()?.shootMinute, 9)
}
func testMainViewModelGatePagingActionsTTSAndSocketFiltering() async {
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.main")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.main")
let store = AppStore(defaults: defaults)
store.userId = "9"
store.accountType = "photog"
store.currentScenicId = 11
store.scenicQueuePunchSpotId = "22"
store.scenicQueuePunchSpotName = "东门打卡点"
store.saveScenicQueueSettingsSnapshot(
store.session.userId = "9"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
store.scenicQueue.punchSpotId = "22"
store.scenicQueue.punchSpotName = "东门打卡点"
store.scenicQueue.saveSettingsSnapshot(
ScenicQueueSettingsSnapshot(showStartShootingButton: true, quickCallButtonEnabled: true, prepareCallButtonEnabled: true)
)
let api = MockScenicQueueAPI()
@ -248,28 +248,28 @@ final class ScenicQueueFeatureTests: XCTestCase {
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.store")
let store = AppStore(defaults: defaults)
store.userId = "1"
store.accountType = "photog"
store.scenicQueuePunchSpotId = "22"
store.scenicQueuePunchSpotName = "东门打卡点"
store.scenicQueueRemark = "播报"
store.saveScenicQueueSettingsSnapshot(ScenicQueueSettingsSnapshot(shootMinute: 3))
store.saveScenicQueuePresetVoices(["a", "b", "c", "d", "e", "f"])
store.session.userId = "1"
store.session.accountType = .unknown("photog")
store.scenicQueue.punchSpotId = "22"
store.scenicQueue.punchSpotName = "东门打卡点"
store.scenicQueue.remark = "播报"
store.scenicQueue.saveSettingsSnapshot(ScenicQueueSettingsSnapshot(shootMinute: 3))
store.scenicQueue.savePresetVoices(["a", "b", "c", "d", "e", "f"])
store.userId = "2"
store.accountType = "photog"
XCTAssertEqual(store.scenicQueuePunchSpotId, "")
XCTAssertNil(store.scenicQueueSettingsSnapshot())
store.session.userId = "2"
store.session.accountType = .unknown("photog")
XCTAssertEqual(store.scenicQueue.punchSpotId, "")
XCTAssertNil(store.scenicQueue.settingsSnapshot())
store.userId = "1"
store.accountType = "photog"
XCTAssertEqual(store.scenicQueuePunchSpotId, "22")
XCTAssertEqual(store.scenicQueuePresetVoices(), ["a", "b", "c", "d", "e"])
store.session.userId = "1"
store.session.accountType = .unknown("photog")
XCTAssertEqual(store.scenicQueue.punchSpotId, "22")
XCTAssertEqual(store.scenicQueue.presetVoices(), ["a", "b", "c", "d", "e"])
store.logout()
store.userId = "1"
store.accountType = "photog"
XCTAssertEqual(store.scenicQueuePunchSpotId, "")
XCTAssertNil(store.scenicQueueSettingsSnapshot())
store.session.userId = "1"
store.session.accountType = .unknown("photog")
XCTAssertEqual(store.scenicQueue.punchSpotId, "")
XCTAssertNil(store.scenicQueue.settingsSnapshot())
}
func testAliyunNLSTokenProviderBuildsSignedCreateTokenRequest() async throws {
@ -321,8 +321,8 @@ final class ScenicQueueFeatureTests: XCTestCase {
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.offlineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.offlineTTS")
let store = AppStore(defaults: defaults)
store.userId = "u1"
store.accountType = "photog"
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
let tts = MockScenicQueueTTSManager()
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
var messages: [String] = []
@ -330,7 +330,7 @@ final class ScenicQueueFeatureTests: XCTestCase {
viewModel.setUseOfflineTTS(true)
XCTAssertFalse(store.scenicQueueUseOfflineTTS)
XCTAssertFalse(store.scenicQueue.useOfflineTTS)
XCTAssertEqual(messages.last, "离线语音资源未安装")
}
}

View File

@ -24,7 +24,7 @@ final class ScenicSelectionViewModelTests: XCTestCase {
}
func testSearchFiltersByNameAndAddress() {
appStore.saveRoleScenicList([
appStore.permissions.saveRoleScenicList([
ScenicInfo(id: 1, name: "西湖景区", status: 1, location: ScenicLocationInfo(lng: 120.1, lat: 30.2, address: "杭州西湖"), coverImg: nil),
ScenicInfo(id: 2, name: "灵隐寺", status: 1, location: ScenicLocationInfo(lng: 120.2, lat: 30.3, address: "杭州灵隐"), coverImg: nil),
])
@ -43,12 +43,12 @@ final class ScenicSelectionViewModelTests: XCTestCase {
}
func testNearestTagWhenLocationProvided() {
appStore.saveRoleScenicList([
appStore.permissions.saveRoleScenicList([
ScenicInfo(id: 1, name: "", status: 1, location: ScenicLocationInfo(lng: 116.40, lat: 39.91, address: "A"), coverImg: nil),
ScenicInfo(id: 2, name: "", status: 1, location: ScenicLocationInfo(lng: 116.50, lat: 39.95, address: "B"), coverImg: nil),
])
let items = ScenicSpotDisplayMapper.buildItems(
scenicList: appStore.roleScenicList(),
scenicList: appStore.permissions.roleScenicList(),
searchQuery: "",
currentLat: 39.9087,
currentLng: 116.3975,
@ -58,7 +58,7 @@ final class ScenicSelectionViewModelTests: XCTestCase {
}
func testClosedStatusTag() {
appStore.saveRoleScenicList([
appStore.permissions.saveRoleScenicList([
ScenicInfo(id: 1, name: "停业", status: 2, location: nil, coverImg: nil),
])
let viewModel = ScenicSelectionViewModel(appStore: appStore)
@ -68,7 +68,7 @@ final class ScenicSelectionViewModelTests: XCTestCase {
}
func testSelectSpotWritesAppStore() {
appStore.saveRoleScenicList([
appStore.permissions.saveRoleScenicList([
ScenicInfo(id: 5, name: "测试景区", status: 1),
])
let viewModel = ScenicSelectionViewModel(appStore: appStore)
@ -78,8 +78,8 @@ final class ScenicSelectionViewModelTests: XCTestCase {
viewModel.loadFromLocal()
viewModel.selectSpot(viewModel.spots[0])
XCTAssertEqual(appStore.currentScenicId, 5)
XCTAssertEqual(appStore.currentScenicName, "测试景区")
XCTAssertEqual(appStore.session.currentScenicId, 5)
XCTAssertEqual(appStore.session.currentScenicName, "测试景区")
XCTAssertTrue(completed)
XCTAssertTrue(viewModel.spots.first?.isSelected == true)
}

View File

@ -11,10 +11,10 @@ import XCTest
final class StatisticsViewModelTests: XCTestCase {
override func tearDown() {
AppStore.shared.roleCode = ""
AppStore.shared.roleName = ""
AppStore.shared.currentScenicId = 0
AppStore.shared.currentStoreId = 0
AppStore.shared.session.roleCode = ""
AppStore.shared.session.roleName = ""
AppStore.shared.session.currentScenicId = 0
AppStore.shared.session.currentStoreId = 0
super.tearDown()
}
@ -68,7 +68,7 @@ final class StatisticsViewModelTests: XCTestCase {
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
""".data(using: .utf8)!
AppStore.shared.currentScenicId = 10
AppStore.shared.session.currentScenicId = 10
let session = MockURLSession(responses: [listJSON])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()
@ -93,7 +93,7 @@ final class StatisticsViewModelTests: XCTestCase {
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":2,"order_price":"50","refund":"10","received":"90"}]}}
""".data(using: .utf8)!
AppStore.shared.currentScenicId = 10
AppStore.shared.session.currentScenicId = 10
let session = MockURLSession(responses: [summaryJSON, listJSON])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()
@ -120,8 +120,8 @@ final class StatisticsViewModelTests: XCTestCase {
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
""".data(using: .utf8)!
AppStore.shared.roleCode = AppRoleCode.scenicAdmin.rawValue
AppStore.shared.currentScenicId = 20
AppStore.shared.session.roleCode = AppRoleCode.scenicAdmin.rawValue
AppStore.shared.session.currentScenicId = 20
let session = MockURLSession(responses: [summaryJSON, listJSON])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()
@ -141,8 +141,8 @@ final class StatisticsViewModelTests: XCTestCase {
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
""".data(using: .utf8)!
AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue
AppStore.shared.currentStoreId = 30
AppStore.shared.session.roleCode = AppRoleCode.storeAdmin.rawValue
AppStore.shared.session.currentStoreId = 30
let session = MockURLSession(responses: [summaryJSON, listJSON])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()
@ -156,8 +156,8 @@ final class StatisticsViewModelTests: XCTestCase {
}
func testStoreAdminWithoutStoreIdDoesNotRequest() async {
AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue
AppStore.shared.currentStoreId = 0
AppStore.shared.session.roleCode = AppRoleCode.storeAdmin.rawValue
AppStore.shared.session.currentStoreId = 0
let session = MockURLSession(responses: [])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()
@ -175,7 +175,7 @@ final class StatisticsViewModelTests: XCTestCase {
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":1,"order_price":"10","refund":"0","received":"10"}]}}
""".data(using: .utf8)!
AppStore.shared.currentScenicId = 10
AppStore.shared.session.currentScenicId = 10
let session = MockURLSession(responses: [page1JSON])
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
let viewModel = StatisticsViewModel()

View File

@ -17,7 +17,7 @@ final class TaskAddViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "TaskAddViewModelTests")!
defaults.removePersistentDomain(forName: "TaskAddViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 10
appStore.session.currentScenicId = 10
}
func testValidateRequiresTaskName() async {

View File

@ -17,7 +17,7 @@ final class TaskOrderSelectViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "TaskOrderSelectViewModelTests")!
defaults.removePersistentDomain(forName: "TaskOrderSelectViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 10
appStore.session.currentScenicId = 10
}
func testToggleSelectionSelectsAndDeselects() {

View File

@ -86,9 +86,9 @@ final class TravelAlbumOTGCoreTests: XCTestCase {
}
func testOSSPunchPointObjectKeyUsesAndroidUserPath() {
let previousUserId = AppStore.shared.userId
AppStore.shared.userId = "u900"
defer { AppStore.shared.userId = previousUserId }
let previousUserId = AppStore.shared.session.userId
AppStore.shared.session.userId = "u900"
defer { AppStore.shared.session.userId = previousUserId }
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!

View File

@ -782,10 +782,10 @@ private func makeOTGTestDefaults() -> UserDefaults {
@MainActor
private func prepareAppStoreForOTGTests() {
AppStore.shared.userId = "u1"
AppStore.shared.accountType = "photog"
AppStore.shared.currentScenicId = 11
AppStore.shared.currentStoreId = 22
AppStore.shared.session.userId = "u1"
AppStore.shared.session.accountType = .unknown("photog")
AppStore.shared.session.currentScenicId = 11
AppStore.shared.session.currentStoreId = 22
}
private func waitUntil(

View File

@ -30,7 +30,7 @@ final class UmengBootstrapTests: XCTestCase {
UmengBootstrap.setConfigureHandlerForTesting { _, _ in
callCount += 1
}
appStore.privacyAgreementAccepted = false
appStore.session.privacyAgreementAccepted = false
XCTAssertFalse(UmengBootstrap.configureIfNeeded(appStore: appStore))
XCTAssertFalse(UmengBootstrap.isConfigured)
@ -42,7 +42,7 @@ final class UmengBootstrapTests: XCTestCase {
UmengBootstrap.setConfigureHandlerForTesting { appKey, channel in
calls.append((appKey, channel))
}
appStore.privacyAgreementAccepted = true
appStore.session.privacyAgreementAccepted = true
XCTAssertTrue(UmengBootstrap.configureIfNeeded(appStore: appStore))
XCTAssertTrue(UmengBootstrap.isConfigured)