将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
567 lines
22 KiB
Swift
567 lines
22 KiB
Swift
//
|
||
// ScenicQueueFeatureTests.swift
|
||
// suixinkanTests
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan
|
||
|
||
@MainActor
|
||
/// 排队管理同步功能测试。
|
||
final class ScenicQueueFeatureTests: XCTestCase {
|
||
|
||
func testModelDecodingAndMapping() throws {
|
||
let homeJSON = """
|
||
{
|
||
"stats":{"type":1},
|
||
"list":{
|
||
"list":[{
|
||
"id":101,
|
||
"queue_code":"A12",
|
||
"mobile":"13800138000",
|
||
"status":1,
|
||
"status_text":"待叫号",
|
||
"wait_min":8,
|
||
"ahead_count":2,
|
||
"is_called":0,
|
||
"created_at":"2026-07-07 10:20:00",
|
||
"identity_tag":"VIP",
|
||
"queue_ban_label":"限制1天",
|
||
"queue_time":"2026-07-07 10:20:00",
|
||
"queue_count_today":3,
|
||
"uid":88,
|
||
"mark_as_photog":0,
|
||
"mark_as_freelance_photog":1,
|
||
"is_miss_requeue":1,
|
||
"is_miss_requeue_text":"过号重排"
|
||
}],
|
||
"total":1,
|
||
"page":1,
|
||
"page_size":10
|
||
},
|
||
"time":"2026-07-07 10:21:00"
|
||
}
|
||
""".data(using: .utf8)!
|
||
|
||
let home = try JSONDecoder().decode(ScenicQueueHomeData.self, from: homeJSON)
|
||
let ticket = try XCTUnwrap(home.list?.list.first?.toCurrentTicket())
|
||
|
||
XCTAssertEqual(ticket.recordId, 101)
|
||
XCTAssertEqual(ticket.queueNo, "A12")
|
||
XCTAssertEqual(ticket.phoneMasked, "138****8000")
|
||
XCTAssertEqual(ticket.dialPhoneDigits, "13800138000")
|
||
XCTAssertEqual(ticket.status, .waiting)
|
||
XCTAssertTrue(ticket.shouldShowIdentityTag)
|
||
XCTAssertEqual(ticket.missRequeueText, "过号重排")
|
||
}
|
||
|
||
func testSettingDecodingClampsSnapshotAndSocketMessage() throws {
|
||
let settingJSON = """
|
||
{
|
||
"exists":true,
|
||
"setting":{
|
||
"id":7,
|
||
"scenic_id":11,
|
||
"scenic_spot_id":22,
|
||
"scenic_spot_name":"东门打卡点",
|
||
"photo_estimate_min":40,
|
||
"photo_estimate_sec":70,
|
||
"first_notice_threshold_pos":60,
|
||
"first_notice_sms_enabled":1,
|
||
"first_notice_call_enabled":0,
|
||
"second_notice_threshold_pos":2,
|
||
"second_notice_sms_enabled":0,
|
||
"second_notice_call_enabled":1,
|
||
"countdown_broadcast_interval_sec":99,
|
||
"countdown_readable_threshold_sec":2,
|
||
"queue_distance_meter":10000000,
|
||
"queue_take_limit":1000000,
|
||
"miss_call_requeue_offset":0,
|
||
"show_start_shoot_button":0,
|
||
"auto_call_next_count":9,
|
||
"business_start_time":"09:30",
|
||
"business_end_time":"18:00",
|
||
"voice_broadcasts":[{"content":"请提前准备","sort_order":1}],
|
||
"status":0,
|
||
"remark":"自定义播报"
|
||
}
|
||
}
|
||
""".data(using: .utf8)!
|
||
let data = try JSONDecoder().decode(ScenicQueueSettingData.self, from: settingJSON)
|
||
let snapshot = try XCTUnwrap(data.setting).toSnapshot(fallback: ScenicQueueSettingsSnapshot())
|
||
|
||
XCTAssertEqual(snapshot.shootMinute, 30)
|
||
XCTAssertEqual(snapshot.shootSecond, 59)
|
||
XCTAssertEqual(snapshot.firstAheadCount, 50)
|
||
XCTAssertEqual(snapshot.broadcastIntervalSec, 50)
|
||
XCTAssertEqual(snapshot.countdownThresholdSec, 15)
|
||
XCTAssertEqual(snapshot.queueDistanceMeter, 9_999_990)
|
||
XCTAssertEqual(snapshot.queueTakeLimit, 999_999)
|
||
XCTAssertEqual(snapshot.missCallRequeueOffset, 1)
|
||
XCTAssertFalse(snapshot.showStartShootingButton)
|
||
XCTAssertEqual(snapshot.autoCallAheadCount, 5)
|
||
XCTAssertFalse(snapshot.businessOpen)
|
||
|
||
let socketJSON = """
|
||
{"code":100000,"data":{"action":"scenic_queue_ticket_called","params":{"scenic_spot_id":22,"record_id":101,"operator_uid":99,"event_id":"evt-1"}}}
|
||
""".data(using: .utf8)!
|
||
let message = try JSONDecoder().decode(ScenicQueueSocketMessage.self, from: socketJSON)
|
||
XCTAssertTrue(message.isQueueEvent)
|
||
XCTAssertEqual(message.data?.params?.eventId, "evt-1")
|
||
|
||
let sts = try MockScenicQueueAPI.makeSTS()
|
||
XCTAssertEqual(sts.bucket, "vipsky")
|
||
XCTAssertEqual(sts.credentials.securityToken, "security-token")
|
||
}
|
||
|
||
func testSettingsValidationSaveRequestAndPresetLimit() async {
|
||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.settings")!
|
||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.settings")
|
||
let store = AppStore(defaults: defaults)
|
||
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)
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
await viewModel.save(api: api)
|
||
XCTAssertEqual(viewModel.settingsTab, .basic)
|
||
XCTAssertTrue(viewModel.highlightedInvalidFieldKeys.contains("punchSpot"))
|
||
XCTAssertEqual(messages.last, "请选择打卡点")
|
||
|
||
await viewModel.setPunchSpot(ScenicQueuePunchSpotOption(id: "22", label: "东门打卡点"), api: api)
|
||
viewModel.setShootMinute(9)
|
||
viewModel.setShootSecond(30)
|
||
viewModel.setFirstAheadCount(4)
|
||
viewModel.toggleFirstSms()
|
||
viewModel.toggleSecondPhone()
|
||
viewModel.setBroadcastIntervalSec(40)
|
||
viewModel.setCountdownThresholdSec(30)
|
||
viewModel.setQueueDistanceMeter(500)
|
||
viewModel.setQueueTakeLimit(6)
|
||
viewModel.setMissCallRequeueOffset(2)
|
||
viewModel.setAutoCallAheadCount(3)
|
||
viewModel.setCustomTTSText("请准备拍摄")
|
||
viewModel.saveCurrentTextAsPresetVoiceLocal()
|
||
viewModel.setCustomTTSText("第二条")
|
||
viewModel.saveCurrentTextAsPresetVoiceLocal()
|
||
await viewModel.save(api: api)
|
||
|
||
let request = api.savedSetting
|
||
XCTAssertEqual(request?.scenicId, 11)
|
||
XCTAssertEqual(request?.scenicSpotId, 22)
|
||
XCTAssertEqual(request?.photoEstimateMin, 9)
|
||
XCTAssertEqual(request?.photoEstimateSec, 30)
|
||
XCTAssertEqual(request?.firstNoticeThresholdPos, 4)
|
||
XCTAssertEqual(request?.firstNoticeSmsEnabled, 1)
|
||
XCTAssertEqual(request?.secondNoticeCallEnabled, 1)
|
||
XCTAssertEqual(request?.countdownBroadcastIntervalSec, 40)
|
||
XCTAssertEqual(request?.countdownReadableThresholdSec, 30)
|
||
XCTAssertEqual(request?.queueDistanceMeter, 500)
|
||
XCTAssertEqual(request?.queueTakeLimit, 6)
|
||
XCTAssertEqual(request?.missCallRequeueOffset, 2)
|
||
XCTAssertEqual(request?.autoCallNextCount, 3)
|
||
XCTAssertEqual(request?.voiceBroadcasts.map(\.content), ["请准备拍摄", "第二条"])
|
||
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.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()
|
||
api.homePages[1] = .page(
|
||
items: [.item(id: 101, queueCode: "A12", isCalled: 0), .item(id: 102, queueCode: "A13", isCalled: 0)],
|
||
total: 3,
|
||
page: 1
|
||
)
|
||
api.homePages[2] = .page(
|
||
items: [.item(id: 102, queueCode: "A13", isCalled: 0), .item(id: 103, queueCode: "A14", isCalled: 0)],
|
||
total: 3,
|
||
page: 2
|
||
)
|
||
let tts = MockScenicQueueTTSManager()
|
||
let socket = ScenicQueueSocketClient()
|
||
let viewModel = ScenicQueueViewModel(appStore: store, ttsManager: tts, socketClient: socket)
|
||
|
||
await viewModel.refreshQueueGate(api: api)
|
||
XCTAssertTrue(viewModel.queueGatePassed)
|
||
XCTAssertEqual(viewModel.selectedPunchSpotLine, "东门打卡点")
|
||
XCTAssertEqual(viewModel.summary, ScenicQueueSummary(peopleInQueue: 3, avgWaitMinutes: 6))
|
||
XCTAssertEqual(viewModel.currentQueue.map(\.recordId), [101, 102])
|
||
|
||
await viewModel.loadMoreCurrentQueue(api: api)
|
||
XCTAssertEqual(viewModel.currentQueue.map(\.recordId), [101, 102, 103])
|
||
|
||
viewModel.requestCallConfirm(recordId: 101, queueNo: "A12")
|
||
await viewModel.confirmCallTicket(api: api)
|
||
XCTAssertEqual(viewModel.currentQueue.first?.status, .called)
|
||
XCTAssertEqual(tts.events.last, "start:A12")
|
||
|
||
viewModel.requestSkipTicket(recordId: 101, queueNo: "A12")
|
||
await viewModel.confirmSkipTicket(api: api)
|
||
XCTAssertFalse(viewModel.currentQueue.contains { $0.recordId == 101 })
|
||
XCTAssertTrue(tts.events.contains("stopQueue"))
|
||
|
||
let calledMessage = ScenicQueueSocketMessage(
|
||
code: 100000,
|
||
data: ScenicQueueSocketData(
|
||
action: ScenicQueueSocketAction.ticketCalled.rawValue,
|
||
params: ScenicQueueSocketParams(scenicSpotId: 22, recordId: 103, operatorUid: 88, eventId: "evt-remote")
|
||
)
|
||
)
|
||
socket.onMessage?(calledMessage)
|
||
api.homePages[1] = .page(items: [.item(id: 103, queueCode: "A14", isCalled: 1)], total: 1, page: 1)
|
||
await viewModel.refreshCurrentQueue(api: api)
|
||
XCTAssertEqual(tts.events.last, "start:A14")
|
||
|
||
socket.onMessage?(calledMessage)
|
||
await viewModel.refreshCurrentQueue(api: api)
|
||
XCTAssertEqual(tts.events.filter { $0 == "start:A14" }.count, 1)
|
||
|
||
let ownMessage = ScenicQueueSocketMessage(
|
||
code: 100000,
|
||
data: ScenicQueueSocketData(
|
||
action: ScenicQueueSocketAction.ticketCalled.rawValue,
|
||
params: ScenicQueueSocketParams(scenicSpotId: 22, recordId: 103, operatorUid: 9, eventId: "evt-own")
|
||
)
|
||
)
|
||
socket.onMessage?(ownMessage)
|
||
await viewModel.refreshCurrentQueue(api: api)
|
||
XCTAssertEqual(tts.events.filter { $0 == "start:A14" }.count, 1)
|
||
}
|
||
|
||
func testAppStoreQueueCacheIsAccountScopedAndClearedOnLogout() {
|
||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.store")!
|
||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.store")
|
||
let store = AppStore(defaults: defaults)
|
||
|
||
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.session.userId = "2"
|
||
store.session.accountType = .unknown("photog")
|
||
XCTAssertEqual(store.scenicQueue.punchSpotId, "")
|
||
XCTAssertNil(store.scenicQueue.settingsSnapshot())
|
||
|
||
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.session.userId = "1"
|
||
store.session.accountType = .unknown("photog")
|
||
XCTAssertEqual(store.scenicQueue.punchSpotId, "")
|
||
XCTAssertNil(store.scenicQueue.settingsSnapshot())
|
||
}
|
||
|
||
func testAliyunNLSTokenProviderBuildsSignedCreateTokenRequest() async throws {
|
||
let credentials = try MockScenicQueueAPI.makeSTS().credentials
|
||
let provider = AliyunNLSTokenProvider(
|
||
dateProvider: { Date(timeIntervalSince1970: 1_704_067_200) },
|
||
nonceProvider: { "nonce-1" },
|
||
dataLoader: { request in
|
||
let url = try XCTUnwrap(request.url)
|
||
let query = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems)
|
||
XCTAssertEqual(query.first(where: { $0.name == "Action" })?.value, "CreateToken")
|
||
XCTAssertEqual(query.first(where: { $0.name == "AccessKeyId" })?.value, "ak")
|
||
XCTAssertEqual(query.first(where: { $0.name == "SecurityToken" })?.value, "security-token")
|
||
XCTAssertEqual(query.first(where: { $0.name == "SignatureMethod" })?.value, "HMAC-SHA1")
|
||
XCTAssertNotNil(query.first(where: { $0.name == "Signature" })?.value)
|
||
return #"{"Token":{"Id":"nls-token","ExpireTime":1704068200}}"#.data(using: .utf8)!
|
||
}
|
||
)
|
||
|
||
let token = try await provider.createToken(credentials: credentials)
|
||
|
||
XCTAssertEqual(token.id, "nls-token")
|
||
XCTAssertEqual(token.expireTime, 1_704_068_200)
|
||
}
|
||
|
||
func testAliyunTTSManagerUsesNativeBridgeAndPlayerWhenPrepared() async {
|
||
let api = MockScenicQueueAPI()
|
||
let tokenProvider = MockAliyunNLSTokenProvider()
|
||
let bridge = MockAliyunNuiTTSBridge()
|
||
let player = MockAliyunPCMStreamingPlayer()
|
||
let manager = AliyunNLSTTSManager(
|
||
tokenProvider: tokenProvider,
|
||
bridge: bridge,
|
||
streamingPlayer: player
|
||
)
|
||
|
||
await manager.prepareQueueTTSEngine(api: api)
|
||
let ok = await manager.speakPlainTextOnce("A12号,请到拍摄区域,准备拍摄。")
|
||
|
||
XCTAssertTrue(ok)
|
||
XCTAssertTrue(bridge.events.contains { $0.hasPrefix("initialize:") })
|
||
XCTAssertTrue(bridge.events.contains("set:speech_rate=-120"))
|
||
XCTAssertTrue(bridge.events.contains("set:volume=2.0"))
|
||
XCTAssertTrue(bridge.events.contains("play:A12号,请到拍摄区域,准备拍摄。"))
|
||
XCTAssertEqual(Array(player.events.suffix(3)), ["start:16000", "write:4", "drain"])
|
||
}
|
||
|
||
func testSettingsOfflineTTSSwitchReportsUnavailable() {
|
||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.offlineTTS")!
|
||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.offlineTTS")
|
||
let store = AppStore(defaults: defaults)
|
||
store.session.userId = "u1"
|
||
store.session.accountType = .unknown("photog")
|
||
let tts = MockScenicQueueTTSManager()
|
||
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
viewModel.setUseOfflineTTS(true)
|
||
|
||
XCTAssertFalse(store.scenicQueue.useOfflineTTS)
|
||
XCTAssertEqual(messages.last, "离线语音资源未安装")
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
private final class MockScenicQueueAPI: ScenicQueueAPIProtocol {
|
||
var savedSetting: ScenicQueueSaveSettingRequest?
|
||
var homePages: [Int: ScenicQueueHomeData] = [:]
|
||
|
||
func saveSetting(_ request: ScenicQueueSaveSettingRequest) async throws {
|
||
savedSetting = request
|
||
}
|
||
|
||
func call(recordId: Int64) async throws -> ScenicQueueCallData {
|
||
ScenicQueueCallData(id: recordId, status: 2, statusText: "已叫号", calledAt: "2026-07-07 10:30:00")
|
||
}
|
||
|
||
func pass(recordId: Int64) async throws -> ScenicQueuePassData {
|
||
ScenicQueuePassData(id: recordId, status: 3, statusText: "已过号")
|
||
}
|
||
|
||
func finish(recordId: Int64) async throws -> ScenicQueueFinishData {
|
||
ScenicQueueFinishData(id: recordId, status: 4, statusText: "已完成")
|
||
}
|
||
|
||
func userMark(_ request: ScenicQueueUserMarkRequest) async throws {}
|
||
|
||
func requeueInsertBefore(recordId: Int64, operatorId: Int) async throws {}
|
||
|
||
func shootQueueQrcode(scenicId: Int64, scenicSpotId: Int64) async throws -> ScenicQueueShootQueueQrcodeData {
|
||
ScenicQueueShootQueueQrcodeData(qrcodeUrl: nil, scene: nil, page: nil, time: nil)
|
||
}
|
||
|
||
func setting(scenicId: Int64?, scenicSpotId: Int64?) async throws -> ScenicQueueSettingData {
|
||
ScenicQueueSettingData(exists: false, setting: nil, time: nil)
|
||
}
|
||
|
||
func stats(scenicId: Int64, scenicSpotId: Int64) async throws -> ScenicQueueStatsData {
|
||
ScenicQueueStatsData(queueCount: 3, avgWaitMin: 5.6, time: "2026-07-07 10:20:00")
|
||
}
|
||
|
||
func home(scenicId: Int64, scenicSpotId: Int64, type: Int, page: Int, pageSize: Int) async throws -> ScenicQueueHomeData {
|
||
if type == 2 {
|
||
return .page(items: [], total: 0, page: page)
|
||
}
|
||
return homePages[page] ?? .page(items: [], total: 0, page: page)
|
||
}
|
||
|
||
func settingChangeLog(scenicId: Int64, scenicSpotId: Int64?, page: Int, pageSize: Int) async throws -> ScenicQueueSettingChangeLogData {
|
||
ScenicQueueSettingChangeLogData(total: 0, page: page, pageSize: pageSize, list: [])
|
||
}
|
||
|
||
func scenicSpotListAll(scenicId: String) async throws -> ScenicSpotListResponse {
|
||
ScenicSpotListResponse(list: [ScenicSpotItem(id: 22, name: "东门打卡点")], total: 1)
|
||
}
|
||
|
||
func socketToken() async throws -> SocketTokenResponse {
|
||
SocketTokenResponse(socketToken: "socket-token")
|
||
}
|
||
|
||
func aliyunSTS(bucket: String) async throws -> AliyunOSSResponse {
|
||
try Self.makeSTS(bucket: bucket)
|
||
}
|
||
|
||
static func makeSTS(bucket: String = "vipsky") throws -> AliyunOSSResponse {
|
||
let json = """
|
||
{
|
||
"base_url":"https://vipsky.oss-cn-hangzhou.aliyuncs.com",
|
||
"endpoint":"oss-cn-hangzhou.aliyuncs.com",
|
||
"region":"cn-hangzhou",
|
||
"bucket":"\(bucket)",
|
||
"expire_seconds":3600,
|
||
"credentials":{
|
||
"access_key_id":"ak",
|
||
"access_key_secret":"secret",
|
||
"security_token":"security-token"
|
||
}
|
||
}
|
||
""".data(using: .utf8)!
|
||
return try JSONDecoder().decode(AliyunOSSResponse.self, from: json)
|
||
}
|
||
}
|
||
|
||
private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging {
|
||
private(set) var events: [String] = []
|
||
private(set) var customTextLooping = false
|
||
let supportsOfflineTTS = false
|
||
|
||
func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
|
||
events.append("prepare")
|
||
}
|
||
|
||
func startQueueCallLoop(queueNo: String) {
|
||
events.append("start:\(queueNo)")
|
||
}
|
||
|
||
func stopQueueCallLoop() {
|
||
events.append("stopQueue")
|
||
}
|
||
|
||
func speakPlainTextOnce(_ text: String) async -> Bool {
|
||
events.append("speak:\(text)")
|
||
return true
|
||
}
|
||
|
||
func startCustomTextLoop(_ text: String) async -> Bool {
|
||
customTextLooping = true
|
||
events.append("custom:\(text)")
|
||
return true
|
||
}
|
||
|
||
func stopCustomTextLoop() {
|
||
customTextLooping = false
|
||
events.append("stopCustom")
|
||
}
|
||
|
||
func interruptShootingAnnouncement() {
|
||
events.append("interrupt")
|
||
}
|
||
|
||
func estimateAnnouncementDurationMillis(_ text: String) -> Int64 {
|
||
1_000
|
||
}
|
||
}
|
||
|
||
private final class MockAliyunNLSTokenProvider: AliyunNLSTokenProviding {
|
||
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken {
|
||
AliyunNLSToken(id: "mock-nls-token", expireTime: 1_704_068_200)
|
||
}
|
||
}
|
||
|
||
private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
|
||
weak var delegate: AliyunNuiTTSBridgeDelegate?
|
||
var sdkAvailable = true
|
||
private(set) var events: [String] = []
|
||
private let callbackBridge = AliyunNuiTTSBridge()
|
||
|
||
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
|
||
events.append("initialize:\(ticket.contains("mock-nls-token"))")
|
||
return 0
|
||
}
|
||
|
||
func setParam(_ param: String, value: String) -> Int {
|
||
events.append("set:\(param)=\(value)")
|
||
return 0
|
||
}
|
||
|
||
func play(priority: String, taskId: String, text: String) -> Int {
|
||
events.append("play:\(text)")
|
||
delegate?.aliyunNuiTTSBridge(
|
||
callbackBridge,
|
||
didReceiveAudioData: Data([0, 0, 1, 0]),
|
||
taskId: taskId,
|
||
info: nil
|
||
)
|
||
delegate?.aliyunNuiTTSBridge(callbackBridge, didReceiveEvent: 1, taskId: taskId, code: 0)
|
||
return 0
|
||
}
|
||
|
||
func cancel(taskId: String?) -> Int {
|
||
events.append("cancel:\(taskId ?? "")")
|
||
return 0
|
||
}
|
||
|
||
func releaseEngine() -> Int {
|
||
events.append("release")
|
||
return 0
|
||
}
|
||
|
||
func paramValue(for key: String) -> String? {
|
||
nil
|
||
}
|
||
}
|
||
|
||
private final class MockAliyunPCMStreamingPlayer: AliyunPCMStreamingPlaying {
|
||
private(set) var events: [String] = []
|
||
|
||
func start(sampleRate: Double) {
|
||
events.append("start:\(Int(sampleRate))")
|
||
}
|
||
|
||
func write(_ data: Data) {
|
||
events.append("write:\(data.count)")
|
||
}
|
||
|
||
func drain(_ completion: @escaping () -> Void) {
|
||
events.append("drain")
|
||
completion()
|
||
}
|
||
|
||
func stop() {
|
||
events.append("stop")
|
||
}
|
||
}
|
||
|
||
private extension ScenicQueueHomeData {
|
||
static func page(items: [ScenicQueueHomeItem], total: Int, page: Int) -> ScenicQueueHomeData {
|
||
ScenicQueueHomeData(
|
||
stats: ScenicQueueHomeStats(type: 1),
|
||
list: ScenicQueueHomeListBlock(list: items, total: total, page: page, pageSize: 10),
|
||
time: "2026-07-07 10:20:00"
|
||
)
|
||
}
|
||
}
|
||
|
||
private extension ScenicQueueHomeItem {
|
||
static func item(id: Int64, queueCode: String, isCalled: Int) -> ScenicQueueHomeItem {
|
||
ScenicQueueHomeItem(
|
||
id: id,
|
||
queueCode: queueCode,
|
||
mobile: "13800138000",
|
||
status: isCalled == 1 ? 2 : 1,
|
||
statusText: isCalled == 1 ? "已叫号" : "待叫号",
|
||
waitMin: 5,
|
||
aheadCount: 1,
|
||
isCalled: isCalled,
|
||
createdAt: "2026-07-07 10:20:00",
|
||
calledAt: isCalled == 1 ? "2026-07-07 10:30:00" : nil,
|
||
expiredAt: nil,
|
||
finishedAt: nil,
|
||
identityTag: "",
|
||
queueBanLabel: "",
|
||
queueBanDays: nil,
|
||
queueTime: "2026-07-07 10:20:00",
|
||
queueCountToday: 1,
|
||
uid: 88,
|
||
markAsPhotog: 0,
|
||
markAsFreelancePhotog: 0,
|
||
isMissRequeue: 0,
|
||
isMissRequeueText: nil
|
||
)
|
||
}
|
||
}
|