183 lines
7.7 KiB
Swift
183 lines
7.7 KiB
Swift
//
|
||
// ScenicQueueSettingsViewModelTests.swift
|
||
// suixinkanTests
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan_ios
|
||
|
||
@MainActor
|
||
/// 排队设置测试,覆盖加载、保存和校验。
|
||
final class ScenicQueueSettingsViewModelTests: XCTestCase {
|
||
override func setUp() {
|
||
super.setUp()
|
||
clearQueueSettingsDefaults()
|
||
}
|
||
|
||
func testLoadSelectsFirstSpotAndAppliesLocalSnapshotWhenServerMissing() async {
|
||
let snapshot = ScenicQueueSettingsSnapshot(
|
||
shootMinute: 4,
|
||
shootSecond: 15,
|
||
firstAheadCount: 6,
|
||
firstSms: false,
|
||
firstPhone: true,
|
||
secondAheadCount: 2,
|
||
secondSms: true,
|
||
secondPhone: false,
|
||
broadcastIntervalSec: 55,
|
||
countdownThresholdSec: 25,
|
||
queueDistanceMeter: 8_880,
|
||
queueTakeLimit: 11,
|
||
missCallRequeueOffset: 5,
|
||
showStartShootingButton: false,
|
||
autoCallAheadCount: 3,
|
||
quickCallButtonEnabled: true,
|
||
prepareCallButtonEnabled: true,
|
||
businessOpen: false,
|
||
businessStartTime: "09:30",
|
||
businessEndTime: "18:45"
|
||
)
|
||
ScenicQueueSettingsStore.saveSettingsSnapshot(snapshot, userId: "77", scenicId: 88, spotId: 901)
|
||
let api = ScenicQueueMock()
|
||
api.settingResponse = ScenicQueueSettingData(exists: false)
|
||
let viewModel = ScenicQueueSettingsViewModel()
|
||
|
||
await viewModel.load(api: api, scenicId: 88, userId: "77", spots: settingsSpots)
|
||
|
||
XCTAssertEqual(viewModel.selectedSpotId, 901)
|
||
XCTAssertEqual(viewModel.photoEstimateMin, "4")
|
||
XCTAssertEqual(viewModel.photoEstimateSec, "15")
|
||
XCTAssertEqual(viewModel.firstAhead, "6")
|
||
XCTAssertTrue(viewModel.firstPhone)
|
||
XCTAssertEqual(viewModel.secondAhead, "2")
|
||
XCTAssertEqual(viewModel.broadcastIntervalSec, "55")
|
||
XCTAssertEqual(viewModel.countdownThresholdSec, "25")
|
||
XCTAssertEqual(viewModel.maxQueueRangeKm, "8.88")
|
||
XCTAssertEqual(viewModel.localQueueLimit, "11")
|
||
XCTAssertEqual(viewModel.localPassDelay, "5")
|
||
XCTAssertFalse(viewModel.showStartShootingButton)
|
||
XCTAssertEqual(viewModel.autoCallAheadCount, "3")
|
||
XCTAssertTrue(viewModel.quickCallButtonEnabled)
|
||
XCTAssertTrue(viewModel.prepareCallButtonEnabled)
|
||
XCTAssertFalse(viewModel.queueOpen)
|
||
}
|
||
|
||
func testSaveNormalizesBodyAndPersistsSnapshot() async throws {
|
||
let api = ScenicQueueMock()
|
||
let viewModel = ScenicQueueSettingsViewModel()
|
||
viewModel.scenicSpots = settingsSpots
|
||
viewModel.selectedSpotId = 901
|
||
viewModel.photoEstimateMin = "1"
|
||
viewModel.photoEstimateSec = "20"
|
||
viewModel.firstAhead = "5"
|
||
viewModel.secondAhead = "2"
|
||
viewModel.broadcastIntervalSec = "45"
|
||
viewModel.countdownThresholdSec = "20"
|
||
viewModel.localQueueLimit = "7"
|
||
viewModel.localPassDelay = "3"
|
||
viewModel.showStartShootingButton = false
|
||
viewModel.autoCallAheadCount = "4"
|
||
viewModel.quickCallButtonEnabled = true
|
||
viewModel.prepareCallButtonEnabled = true
|
||
viewModel.queueOpen = false
|
||
viewModel.customTtsText = " {number} 请到拍摄点 "
|
||
viewModel.presetVoices = ["预设播报", " ", "第二条"]
|
||
viewModel.businessStartTime = ScenicQueueSettingsViewModel.businessTime(hour: 11, minute: 0)
|
||
viewModel.businessEndTime = ScenicQueueSettingsViewModel.businessTime(hour: 21, minute: 30)
|
||
viewModel.updateMaxQueueRange("12.3")
|
||
|
||
try await viewModel.save(api: api, scenicId: 88, userId: "77")
|
||
|
||
let request = try XCTUnwrap(api.saveSettingRequests.first)
|
||
XCTAssertEqual(request.scenicId, 88)
|
||
XCTAssertEqual(request.scenicSpotId, 901)
|
||
XCTAssertEqual(request.photoEstimateMin, 1)
|
||
XCTAssertEqual(request.photoEstimateSec, 20)
|
||
XCTAssertEqual(request.queueDistanceMeter, 12_300)
|
||
XCTAssertEqual(request.queueTakeLimit, 7)
|
||
XCTAssertEqual(request.missCallRequeueOffset, 3)
|
||
XCTAssertEqual(request.showStartShootButton, 0)
|
||
XCTAssertEqual(request.autoCallNextCount, 4)
|
||
XCTAssertEqual(request.businessStartTime, "11:00")
|
||
XCTAssertEqual(request.businessEndTime, "21:30")
|
||
XCTAssertEqual(request.voiceBroadcasts.map(\.content), ["预设播报", "第二条"])
|
||
XCTAssertEqual(request.status, 0)
|
||
XCTAssertEqual(request.remark, "{number} 请到拍摄点")
|
||
|
||
XCTAssertEqual(ScenicQueueSettingsStore.selectedSpotId(userId: "77", scenicId: 88), 901)
|
||
let snapshot = try XCTUnwrap(ScenicQueueSettingsStore.settingsSnapshot(userId: "77", scenicId: 88, spotId: 901))
|
||
XCTAssertEqual(snapshot.queueDistanceMeter, 12_300)
|
||
XCTAssertEqual(snapshot.autoCallAheadCount, 4)
|
||
XCTAssertTrue(snapshot.quickCallButtonEnabled)
|
||
}
|
||
|
||
func testValidationRejectsInvalidValues() async {
|
||
let api = ScenicQueueMock()
|
||
let viewModel = ScenicQueueSettingsViewModel()
|
||
viewModel.selectedSpotId = 901
|
||
viewModel.photoEstimateMin = "0"
|
||
viewModel.photoEstimateSec = "0"
|
||
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))
|
||
|
||
viewModel.photoEstimateSec = "20"
|
||
viewModel.firstAhead = "1"
|
||
viewModel.secondAhead = "2"
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))
|
||
|
||
viewModel.firstAhead = "2"
|
||
viewModel.secondAhead = "1"
|
||
viewModel.broadcastIntervalSec = "30"
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))
|
||
}
|
||
|
||
func testFetchQRCodeRejectsEmptyURL() async {
|
||
let api = ScenicQueueMock()
|
||
api.qrcodeURL = ""
|
||
let viewModel = ScenicQueueSettingsViewModel()
|
||
viewModel.selectedSpotId = 901
|
||
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.fetchQRCode(api: api, scenicId: 88))
|
||
}
|
||
}
|
||
|
||
private let settingsSpots = [
|
||
ScenicSpotItem(id: 901, name: "东门打卡点"),
|
||
ScenicSpotItem(id: 902, name: "西门打卡点")
|
||
]
|
||
|
||
private func clearQueueSettingsDefaults() {
|
||
let defaults = UserDefaults.standard
|
||
[
|
||
ScenicQueueLocalSettings.selectedSpotIdKey,
|
||
ScenicQueueLocalSettings.selectedSpotNameKey,
|
||
ScenicQueueLocalSettings.customTtsTextKey,
|
||
ScenicQueueLocalSettings.photoEstimateSecondsKey,
|
||
ScenicQueueLocalSettings.broadcastIntervalSecondsKey,
|
||
ScenicQueueLocalSettings.countdownThresholdSecondsKey,
|
||
ScenicQueueLocalSettings.showStartShootingButtonKey,
|
||
ScenicQueueLocalSettings.autoCallAheadCountKey,
|
||
ScenicQueueLocalSettings.quickCallButtonEnabledKey,
|
||
ScenicQueueLocalSettings.prepareCallButtonEnabledKey,
|
||
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),
|
||
ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.customTtsTextKey, userId: "77", scenicId: 88, spotId: 901),
|
||
ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.presetVoicesKey, userId: "77", scenicId: 88, spotId: 901)
|
||
].forEach(defaults.removeObject)
|
||
}
|
||
|
||
private func XCTAssertThrowsErrorAsync<T>(
|
||
_ expression: @autoclosure () async throws -> T,
|
||
file: StaticString = #filePath,
|
||
line: UInt = #line
|
||
) async {
|
||
do {
|
||
try await expression()
|
||
XCTFail("Expected error to be thrown", file: file, line: line)
|
||
} catch {
|
||
}
|
||
}
|