Files
suixinkan_uikit/suixinkanTests/ScenicQueueFeatureTests.swift
汉秋 efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00

636 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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.session.currentScenicId = 11
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")
store.session.currentScenicId = 11
XCTAssertEqual(store.scenicQueue.punchSpotId, "")
XCTAssertNil(store.scenicQueue.settingsSnapshot())
store.session.userId = "1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
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")
store.session.currentScenicId = 11
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 defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.onlineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.onlineTTS")
let store = AppStore(defaults: defaults)
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
store.scenicQueue.useOfflineTTS = false
let manager = AliyunNLSTTSManager(
appStore: store,
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 testAliyunTTSManagerInitializesOfflineEngineWithBundledVoice() async throws {
let api = MockScenicQueueAPI()
let tokenProvider = MockAliyunNLSTokenProvider()
let bridge = MockAliyunNuiTTSBridge()
bridge.paramValues["model_sample_rate"] = "24000"
let player = MockAliyunPCMStreamingPlayer()
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.nativeOfflineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.nativeOfflineTTS")
let store = AppStore(defaults: defaults)
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
store.scenicQueue.useOfflineTTS = true
let workspace = FileManager.default.temporaryDirectory
.appendingPathComponent("ScenicQueueFeatureTests-\(UUID().uuidString)", isDirectory: true)
let voiceDirectory = workspace.appendingPathComponent("tts/voices", isDirectory: true)
try FileManager.default.createDirectory(at: voiceDirectory, withIntermediateDirectories: true)
try Data([0]).write(to: workspace.appendingPathComponent("tts/languagedata_embedded.bin"))
try Data([0]).write(to: workspace.appendingPathComponent("tts/parameter.cfg"))
try Data([0]).write(to: voiceDirectory.appendingPathComponent("aijia"))
defer { try? FileManager.default.removeItem(at: workspace) }
let manager = AliyunNLSTTSManager(
appStore: store,
tokenProvider: tokenProvider,
bridge: bridge,
streamingPlayer: player,
workspacePathProvider: { workspace.path }
)
XCTAssertTrue(manager.supportsOfflineTTS)
await manager.prepareQueueTTSEngine(api: api)
let ok = await manager.speakPlainTextOnce("请到拍摄区域")
XCTAssertTrue(ok)
let ticketData = try XCTUnwrap(bridge.initializedTickets.last?.data(using: .utf8))
let ticket = try XCTUnwrap(JSONSerialization.jsonObject(with: ticketData) as? [String: String])
XCTAssertEqual(ticket["app_key"], "gfWimmOuTku1AbIV")
XCTAssertEqual(ticket["sdk_code"], "software_nls_tts_offline_standard")
XCTAssertEqual(ticket["mode_type"], "0")
XCTAssertEqual(ticket["workspace"], workspace.path)
XCTAssertEqual(ticket["ak_id"], "ak")
XCTAssertEqual(ticket["sts_token"], "security-token")
XCTAssertTrue(bridge.events.contains { $0.hasPrefix("set:extend_font_name=") && $0.hasSuffix("/tts/voices/aijia") })
XCTAssertTrue(bridge.events.contains("set:encode_type=pcm"))
XCTAssertTrue(bridge.events.contains("set:sample_rate=24000"))
XCTAssertTrue(bridge.events.contains("set:speed_level=1.0"))
XCTAssertFalse(bridge.events.contains("set:mode_type=2"))
XCTAssertEqual(Array(player.events.suffix(3)), ["start:24000", "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")
store.session.currentScenicId = 11
XCTAssertTrue(store.scenicQueue.useOfflineTTS)
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
var paramValues: [String: String] = [:]
private(set) var events: [String] = []
private(set) var initializedTickets: [String] = []
private let callbackBridge = AliyunNuiTTSBridge()
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
initializedTickets.append(ticket)
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? {
paramValues[key]
}
}
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
)
}
}