完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import UIKit
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
@ -55,6 +56,24 @@ final class ScenicQueueFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(ticket.missRequeueText, "过号重排")
|
||||
}
|
||||
|
||||
func testHomeDecodingAcceptsBackendStatsWithoutNestedType() throws {
|
||||
let json = """
|
||||
{
|
||||
"stats":{"avg_wait_min":0,"queue_count":0},
|
||||
"list":{"list":[],"page":1,"page_size":10,"total":0},
|
||||
"type":1
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let home = try JSONDecoder().decode(ScenicQueueHomeData.self, from: json)
|
||||
|
||||
XCTAssertEqual(home.stats?.type, 0)
|
||||
XCTAssertEqual(home.list?.list, [])
|
||||
XCTAssertEqual(home.list?.page, 1)
|
||||
XCTAssertEqual(home.list?.pageSize, 10)
|
||||
XCTAssertEqual(home.list?.total, 0)
|
||||
}
|
||||
|
||||
func testSettingDecodingClampsSnapshotAndSocketMessage() throws {
|
||||
let settingJSON = """
|
||||
{
|
||||
@ -399,6 +418,191 @@ final class ScenicQueueFeatureTests: XCTestCase {
|
||||
XCTAssertFalse(store.scenicQueue.useOfflineTTS)
|
||||
XCTAssertEqual(messages.last, "离线语音资源未安装")
|
||||
}
|
||||
|
||||
func testQueueDistanceKilometerConversionAndInvalidFormats() {
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.kilometersDisplay(fromMeters: 0), "0")
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.kilometersDisplay(fromMeters: 500), "0.5")
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.kilometersDisplay(fromMeters: 1_230), "1.23")
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.kilometersDisplay(fromMeters: 9_999_990), "9999.99")
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.meters(fromKilometersText: "0.50"), 500)
|
||||
XCTAssertEqual(ScenicQueueSettingsViewModel.meters(fromKilometersText: "9999.99"), 9_999_990)
|
||||
XCTAssertNil(ScenicQueueSettingsViewModel.meters(fromKilometersText: "1.234"))
|
||||
XCTAssertNil(ScenicQueueSettingsViewModel.meters(fromKilometersText: "10000"))
|
||||
XCTAssertNil(ScenicQueueSettingsViewModel.meters(fromKilometersText: "公里"))
|
||||
}
|
||||
|
||||
func testInvalidNumericPendingBlocksSaveAndSelectsMatchingTab() async {
|
||||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.invalidNumeric")!
|
||||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.invalidNumeric")
|
||||
let store = AppStore(defaults: defaults)
|
||||
store.session.userId = "u1"
|
||||
store.session.accountType = .unknown("photog")
|
||||
store.session.currentScenicId = 11
|
||||
let api = MockScenicQueueAPI()
|
||||
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: MockScenicQueueTTSManager())
|
||||
await viewModel.setPunchSpot(ScenicQueuePunchSpotOption(id: "22", label: "东门打卡点"), api: api)
|
||||
|
||||
viewModel.reportOutOfRangeNumericPending(fieldKey: ScenicQueueNumericFieldKey.firstNoticeAhead.rawValue, pending: true)
|
||||
await viewModel.save(api: api)
|
||||
|
||||
XCTAssertNil(api.savedSetting)
|
||||
XCTAssertEqual(viewModel.settingsTab, .notify)
|
||||
XCTAssertTrue(viewModel.highlightedInvalidFieldKeys.contains(ScenicQueueNumericFieldKey.firstNoticeAhead.rawValue))
|
||||
}
|
||||
|
||||
func testPresetVoicePlayStopAndDeleteState() async {
|
||||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.presetPlayback")!
|
||||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.presetPlayback")
|
||||
let store = AppStore(defaults: defaults)
|
||||
store.session.userId = "u1"
|
||||
store.session.accountType = .unknown("photog")
|
||||
store.session.currentScenicId = 11
|
||||
store.scenicQueue.punchSpotId = "22"
|
||||
store.scenicQueue.punchSpotName = "东门打卡点"
|
||||
store.scenicQueue.savePresetVoices(["请到拍摄区域"])
|
||||
let tts = MockScenicQueueTTSManager()
|
||||
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
|
||||
|
||||
viewModel.startPresetVoiceLoop("请到拍摄区域")
|
||||
for _ in 0..<20 where viewModel.ttsLoopAnchorText == nil {
|
||||
await Task.yield()
|
||||
}
|
||||
XCTAssertTrue(viewModel.isCustomTTSLooping)
|
||||
XCTAssertEqual(viewModel.ttsLoopAnchorText, "请到拍摄区域")
|
||||
XCTAssertTrue(tts.events.contains("custom:请到拍摄区域"))
|
||||
|
||||
viewModel.deletePresetVoice(at: 0)
|
||||
XCTAssertTrue(viewModel.presetVoices.isEmpty)
|
||||
XCTAssertFalse(viewModel.isCustomTTSLooping)
|
||||
XCTAssertNil(viewModel.ttsLoopAnchorText)
|
||||
XCTAssertEqual(tts.events.last, "stopCustom")
|
||||
}
|
||||
|
||||
func testTicketActionStateMatchesAndroidCardMatrix() {
|
||||
let waiting = makeTicket(status: .waiting, showCompleteAction: false)
|
||||
let waitingState = ScenicQueueTicketActionState.make(ticket: waiting, shootingQueueNo: nil, remainSeconds: 0, showStartShootingButton: true)
|
||||
XCTAssertTrue(waitingState.skipEnabled)
|
||||
XCTAssertEqual(waitingState.primaryTitle, "叫号")
|
||||
XCTAssertTrue(waitingState.primaryVisible)
|
||||
XCTAssertFalse(waitingState.recallVisible)
|
||||
XCTAssertFalse(waitingState.completeVisible)
|
||||
|
||||
let called = makeTicket(status: .called, showCompleteAction: true)
|
||||
let hiddenStartState = ScenicQueueTicketActionState.make(ticket: called, shootingQueueNo: nil, remainSeconds: 0, showStartShootingButton: false)
|
||||
XCTAssertFalse(hiddenStartState.primaryVisible)
|
||||
XCTAssertTrue(hiddenStartState.recallVisible)
|
||||
XCTAssertTrue(hiddenStartState.completeVisible)
|
||||
|
||||
let shootingState = ScenicQueueTicketActionState.make(ticket: called, shootingQueueNo: called.queueNo, remainSeconds: 17, showStartShootingButton: true)
|
||||
XCTAssertFalse(shootingState.skipEnabled)
|
||||
XCTAssertFalse(shootingState.primaryEnabled)
|
||||
XCTAssertEqual(shootingState.primaryTitle, "剩余 17 秒")
|
||||
XCTAssertFalse(shootingState.recallVisible)
|
||||
XCTAssertTrue(shootingState.completeVisible)
|
||||
}
|
||||
|
||||
func testQueueVisualTokensAndChangeLogPresentationContract() {
|
||||
XCTAssertEqual(ScenicQueueTokens.radiusCard, 12)
|
||||
XCTAssertEqual(ScenicQueueTokens.radiusDialog, 16)
|
||||
XCTAssertEqual(ScenicQueueTokens.radiusPrimaryRowButton, 10)
|
||||
XCTAssertEqual(ScenicQueueTokens.ticketIdFont.pointSize, 20)
|
||||
XCTAssertEqual(ScenicQueueTokens.dialogTicketFont.pointSize, 28)
|
||||
XCTAssertEqual(ScenicQueueTokens.listHorizontalInset, 12)
|
||||
XCTAssertTrue(ScenicQueueTokens.timelineDot.isEqualToRGB(0x1677FF))
|
||||
XCTAssertTrue(ScenicQueueTokens.tagWaitingForeground.isEqualToRGB(0x10B981))
|
||||
|
||||
let distanceField = ScenicQueueNumericFieldView(key: "queueDistanceMeter", unit: "千米", keyboard: .decimalPad)
|
||||
XCTAssertEqual(distanceField.subviews.compactMap { ($0 as? UILabel)?.text }, ["千米"])
|
||||
|
||||
let item = ScenicQueueSettingChangeLogItem(
|
||||
id: 1,
|
||||
scenicId: 11,
|
||||
scenicSpotId: 22,
|
||||
scenicSpotName: "东门打卡点",
|
||||
operatorId: nil,
|
||||
operatorName: " ",
|
||||
operatorPhoneTail: "8000",
|
||||
summary: " ",
|
||||
displayText: "拍摄时长由 5 分钟改为 8 分钟",
|
||||
createdAt: "2026-07-07T10:20:30+08:00"
|
||||
)
|
||||
XCTAssertEqual(item.metadataLine, "2026-07-07 10:20 未知 尾号8000")
|
||||
XCTAssertEqual(item.cardSummary, "拍摄时长由 5 分钟改为 8 分钟")
|
||||
}
|
||||
|
||||
func testPendingDialogDismissMethodsClearEveryOperationState() async {
|
||||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.pendingDialogs")!
|
||||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.pendingDialogs")
|
||||
let store = AppStore(defaults: defaults)
|
||||
store.session.userId = "9"
|
||||
store.session.accountType = .unknown("photog")
|
||||
store.session.currentScenicId = 11
|
||||
store.scenicQueue.punchSpotId = "22"
|
||||
store.scenicQueue.punchSpotName = "东门打卡点"
|
||||
let api = MockScenicQueueAPI()
|
||||
api.homePages[1] = .page(items: [.item(id: 1, queueCode: "A01", isCalled: 0)], total: 1, page: 1)
|
||||
let viewModel = ScenicQueueViewModel(appStore: store, ttsManager: MockScenicQueueTTSManager())
|
||||
await viewModel.refreshQueueGate(api: api)
|
||||
|
||||
viewModel.requestCallConfirm(recordId: 1, queueNo: "A01")
|
||||
viewModel.requestSkipTicket(recordId: 1, queueNo: "A01")
|
||||
viewModel.requestFinishConfirm(recordId: 1, queueNo: "A01")
|
||||
viewModel.requestRequeueConfirm(recordId: 1, queueNo: "A01")
|
||||
viewModel.onTicketMarkClick(recordId: 1, queueNo: "A01", uid: 1)
|
||||
XCTAssertNotNil(viewModel.pendingCall)
|
||||
XCTAssertNotNil(viewModel.pendingSkip)
|
||||
XCTAssertNotNil(viewModel.pendingFinish)
|
||||
XCTAssertNotNil(viewModel.pendingRequeue)
|
||||
XCTAssertNotNil(viewModel.pendingMark)
|
||||
|
||||
viewModel.dismissCallConfirmDialog()
|
||||
viewModel.dismissSkipDialog()
|
||||
viewModel.dismissFinishDialog()
|
||||
viewModel.dismissRequeueDialog()
|
||||
viewModel.dismissMarkDialog()
|
||||
XCTAssertNil(viewModel.pendingCall)
|
||||
XCTAssertNil(viewModel.pendingSkip)
|
||||
XCTAssertNil(viewModel.pendingFinish)
|
||||
XCTAssertNil(viewModel.pendingRequeue)
|
||||
XCTAssertNil(viewModel.pendingMark)
|
||||
}
|
||||
|
||||
private func makeTicket(status: ScenicQueueCallStatus, showCompleteAction: Bool) -> ScenicQueueTicket {
|
||||
ScenicQueueTicket(
|
||||
recordId: 1,
|
||||
queueNo: "A01",
|
||||
phoneMasked: "138****8000",
|
||||
dialPhoneDigits: "13800138000",
|
||||
status: status,
|
||||
statusText: status == .waiting ? "待叫号" : "已叫号",
|
||||
waitMinutes: 5,
|
||||
peopleAhead: 2,
|
||||
showCompleteAction: showCompleteAction,
|
||||
queueBanLabel: "",
|
||||
identityTag: "",
|
||||
queueTimeDisplay: "07-07 10:20",
|
||||
queueCountToday: 1,
|
||||
uid: 8,
|
||||
markAsPhotog: 0,
|
||||
markAsFreelancePhotog: 0,
|
||||
isMissRequeue: false,
|
||||
missRequeueText: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
func isEqualToRGB(_ rgb: UInt32) -> Bool {
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
guard getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return false }
|
||||
let expectedRed = CGFloat((rgb >> 16) & 0xFF) / 255
|
||||
let expectedGreen = CGFloat((rgb >> 8) & 0xFF) / 255
|
||||
let expectedBlue = CGFloat(rgb & 0xFF) / 255
|
||||
return abs(red - expectedRed) < 0.001 && abs(green - expectedGreen) < 0.001 && abs(blue - expectedBlue) < 0.001 && abs(alpha - 1) < 0.001
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user