// // QueueManagementViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/25. // import XCTest @testable import suixinkan_ios @MainActor /// 排队管理列表测试,覆盖点位门禁、列表分页和动作。 final class QueueManagementViewModelTests: XCTestCase { override func setUp() { super.setUp() clearQueueDefaults() } func testReloadWithoutScenicClearsState() async { let api = ScenicQueueMock() let viewModel = QueueManagementViewModel() await viewModel.reload(api: api, scenicId: nil, userId: "77", spots: sampleSpots) XCTAssertTrue(viewModel.items.isEmpty) XCTAssertNil(viewModel.selectedSpotId) XCTAssertFalse(viewModel.queueGatePassed) XCTAssertEqual(viewModel.queueCount, 0) } func testReloadWithoutSavedSpotShowsGateAndDoesNotRequestQueue() async { let api = ScenicQueueMock() let viewModel = QueueManagementViewModel() await viewModel.reload(api: api, scenicId: 88, userId: "77", spots: sampleSpots) XCTAssertEqual(viewModel.scenicSpots.map(\.id), [901, 902]) XCTAssertNil(viewModel.selectedSpotId) XCTAssertFalse(viewModel.queueGatePassed) XCTAssertTrue(api.homeRequests.isEmpty) XCTAssertTrue(api.statsRequests.isEmpty) } func testReloadWithSavedSpotLoadsHomeAndStats() async { saveSelectedSpot() let api = ScenicQueueMock() api.homeResponses = [ScenicQueueHomeData(list: ScenicQueueHomeListBlock(list: [ticket(id: 1001), ticket(id: 1002)], total: 2), time: "2026-06-25 10:01")] api.statsResponse = ScenicQueueStatsData(queueCount: 5, avgWaitMin: 12.5, time: "2026-06-25 10:02") let viewModel = QueueManagementViewModel() await viewModel.reload(api: api, scenicId: 88, userId: "77", spots: sampleSpots) XCTAssertEqual(viewModel.selectedSpotId, 901) XCTAssertTrue(viewModel.queueGatePassed) XCTAssertEqual(viewModel.items.map(\.id), [1001, 1002]) XCTAssertEqual(viewModel.queueCount, 5) XCTAssertEqual(viewModel.avgWaitMin, 12.5) XCTAssertEqual(viewModel.lastSyncTimeText, "2026-06-25 10:02") XCTAssertEqual(api.homeRequests.first?.type, QueueListType.queueing.rawValue) } func testStatsFailureKeepsQueueList() async { saveSelectedSpot() let api = ScenicQueueMock() api.homeResponses = [ScenicQueueHomeData(list: ScenicQueueHomeListBlock(list: [ticket(id: 1001)], total: 1), time: "home-time")] api.statsError = QueueTestError.sample let viewModel = QueueManagementViewModel() await viewModel.reload(api: api, scenicId: 88, userId: "77", spots: sampleSpots) XCTAssertEqual(viewModel.items.map(\.id), [1001]) XCTAssertEqual(viewModel.queueCount, 0) XCTAssertEqual(viewModel.lastSyncTimeText, "home-time") } func testCallPassFinishRequeueAndUserMark() async throws { saveSelectedSpot() let api = ScenicQueueMock() api.homeResponses = Array(repeating: ScenicQueueHomeData(list: ScenicQueueHomeListBlock(list: [ticket(id: 1001)], total: 1)), count: 8) let viewModel = QueueManagementViewModel() await viewModel.reload(api: api, scenicId: 88, userId: "77", spots: sampleSpots) try await viewModel.callQueue(api: api, id: 1001) XCTAssertEqual(api.callIds, [1001]) XCTAssertEqual(viewModel.items.first?.isCalled, 1) try await viewModel.passQueue(api: api, scenicId: 88, userId: "77", id: 1001) XCTAssertEqual(api.passIds, [1001]) try await viewModel.finishQueue(api: api, scenicId: 88, userId: "77", id: 1001) XCTAssertEqual(api.finishIds, [1001]) try await viewModel.requeue(api: api, scenicId: 88, userId: "77", id: 1001, operatorId: 77) XCTAssertEqual(api.requeueRequests.map(\.recordId), [1001]) XCTAssertEqual(api.requeueRequests.map(\.operatorId), [77]) XCTAssertEqual(viewModel.selectedListType, .queueing) try await viewModel.userMark( api: api, scenicId: 88, userId: "77", request: ScenicQueueUserMarkRequest(uid: 7001, scenicId: 88, markAsFreelancePhotog: 1, queueBanDays: 14, operatorId: 77) ) XCTAssertEqual(api.userMarkRequests.first?.uid, 7001) XCTAssertEqual(api.userMarkRequests.first?.queueBanDays, 14) } func testSocketSubscriptionAndParse() { XCTAssertEqual(ScenicQueueSocketClient.subscriptionPayloads(scenicSpotId: 901), [ #"{"type":306,"params":{"scenic_spot_id":901}}"#, #"{"type":307,"params":{"scenic_spot_id":901}}"# ]) let message = ScenicQueueSocketClient.parseMessage(""" {"code":100000,"data":{"action":"scenic_queue_ticket_called","params":{"scenic_spot_id":"901","record_id":"1002","operator_uid":"77","event_id":"evt-1"}}} """) XCTAssertEqual(message?.code, 100000) XCTAssertTrue(message?.isScenicQueueEvent == true) XCTAssertEqual(message?.data?.params?.scenicSpotId, 901) XCTAssertEqual(message?.data?.params?.recordId, 1002) XCTAssertEqual(message?.data?.params?.operatorUid, 77) XCTAssertEqual(message?.data?.params?.eventId, "evt-1") } } private let sampleSpots = [ ScenicSpotItem(id: 901, name: "东门打卡点"), ScenicSpotItem(id: 902, name: "西门打卡点") ] private func saveSelectedSpot() { ScenicQueueSettingsStore.saveSelectedSpot(id: 901, name: "东门打卡点", userId: "77", scenicId: 88) } private func clearQueueDefaults() { let defaults = UserDefaults.standard [ ScenicQueueLocalSettings.selectedSpotIdKey, ScenicQueueLocalSettings.selectedSpotNameKey, ScenicQueueSettingsStore.scopedKey(base: ScenicQueueLocalSettings.selectedSpotIdKey, userId: "77", scenicId: 88), ScenicQueueSettingsStore.scopedKey(base: ScenicQueueLocalSettings.selectedSpotNameKey, userId: "77", scenicId: 88), ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.settingsSnapshotKey, userId: "77", scenicId: 88, spotId: 901) ].forEach(defaults.removeObject) } private func ticket(id: Int64, isCalled: Int = 0) -> ScenicQueueTicket { ScenicQueueTicket( id: id, queueCode: "A\(id)", mobile: "13800000000", status: 0, statusText: "", waitMin: 5, aheadCount: 1, isCalled: isCalled, createdAt: "2026-06-25 10:00:00", calledAt: "", expiredAt: "", finishedAt: "", queueCountToday: 2, uid: 7001 ) } @MainActor final class ScenicQueueMock: ScenicQueueServing { var homeResponses: [ScenicQueueHomeData] = [] var statsResponse = ScenicQueueStatsData() var statsError: Error? var settingResponse = ScenicQueueSettingData() var callIds: [Int64] = [] var passIds: [Int64] = [] var finishIds: [Int64] = [] var requeueRequests: [(recordId: Int64, operatorId: Int)] = [] var userMarkRequests: [ScenicQueueUserMarkRequest] = [] var saveSettingRequests: [ScenicQueueSaveSettingRequest] = [] var homeRequests: [(scenicId: Int, scenicSpotId: Int, type: Int, page: Int, pageSize: Int)] = [] var statsRequests: [(scenicId: Int, scenicSpotId: Int)] = [] var qrcodeURL = "https://example.com/qrcode.png" var logs = ScenicQueueSettingChangeLogData() func scenicQueueStats(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueStatsData { statsRequests.append((scenicId, scenicSpotId)) if let statsError { throw statsError } return statsResponse } func scenicQueueHome(scenicId: Int, scenicSpotId: Int, type: Int, page: Int, pageSize: Int) async throws -> ScenicQueueHomeData { homeRequests.append((scenicId, scenicSpotId, type, page, pageSize)) return homeResponses.isEmpty ? ScenicQueueHomeData() : homeResponses.removeFirst() } func socketToken() async throws -> SocketTokenResponse { SocketTokenResponse(socketToken: "token") } func scenicQueueCall(id: Int64) async throws -> ScenicQueueCallData { callIds.append(id) return ScenicQueueCallData(id: id, status: 1, statusText: "已叫号") } func scenicQueuePass(id: Int64) async throws -> ScenicQueuePassData { passIds.append(id) return ScenicQueuePassData(id: id) } func scenicQueueFinish(id: Int64) async throws -> ScenicQueueFinishData { finishIds.append(id) return ScenicQueueFinishData(id: id) } func scenicQueueRequeueInsertBefore(recordId: Int64, operatorId: Int) async throws { requeueRequests.append((recordId, operatorId)) } func scenicQueueUserMark(_ request: ScenicQueueUserMarkRequest) async throws { userMarkRequests.append(request) } func scenicQueueSetting(scenicId: Int, scenicSpotId: Int?) async throws -> ScenicQueueSettingData { settingResponse } func scenicQueueSaveSetting(_ request: ScenicQueueSaveSettingRequest) async throws { saveSettingRequests.append(request) } func scenicQueueShootQueueQRCode(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueShootQueueQRCodeData { ScenicQueueShootQueueQRCodeData(qrcodeUrl: qrcodeURL) } func scenicQueueSettingChangeLog(scenicId: Int, scenicSpotId: Int?, page: Int, pageSize: Int) async throws -> ScenicQueueSettingChangeLogData { logs } } private enum QueueTestError: Error { case sample }