完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -78,10 +78,12 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
XCTAssertEqual(item?.iconName, "shield.fill")
|
||||
}
|
||||
|
||||
func testHomeMenuCatalogUsesSyncedSymbols() {
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "space_settings")?.iconName, "person.crop.square.fill")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "wallet")?.iconName, "creditcard.fill")
|
||||
func testHomeMenuCatalogUsesIOS16CompatibleIcons() {
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "space_settings")?.iconName, "home_menu_space")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "wallet")?.iconName, "home_menu_wallet")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "sample_management")?.iconName, "sample_ic_home_menu_route")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "travel_album")?.iconName, "rectangle.stack.badge.plus")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "store")?.iconName, "building.2.fill")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "/scenic-queue")?.iconName, "person.3.fill")
|
||||
XCTAssertEqual(HomeMenuItem.moreFunctions.iconName, "ellipsis")
|
||||
}
|
||||
|
||||
@ -123,6 +123,53 @@ final class HomeViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(viewModel.commonMenus.isEmpty)
|
||||
}
|
||||
|
||||
func testPermissionApplyDestinationRoutesReviewingAndRejectedToStatus() async throws {
|
||||
let reviewingJSON = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP001","role_id":1,"role_name":"摄影师","scenic_list":[],"status":1,"status_label":"审核中","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let rejectedJSON = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP002","role_id":1,"role_name":"摄影师","scenic_list":[],"status":3,"status_label":"已驳回","created_at":"","audit_note":"资料不全","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let session = MockURLSession(responses: [reviewingJSON, rejectedJSON])
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
let reviewingDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
||||
let rejectedDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
||||
XCTAssertEqual(reviewingDestination, .status(applyCode: "AP001"))
|
||||
XCTAssertEqual(rejectedDestination, .status(applyCode: "AP002"))
|
||||
}
|
||||
|
||||
func testPermissionApplyDestinationFallsBackToApply() async throws {
|
||||
let completedJSON = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP003","role_id":1,"role_name":"摄影师","scenic_list":[],"status":2,"status_label":"已通过","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let emptyJSON = #"{"code":100000,"msg":"success","data":[]}"#.data(using: .utf8)!
|
||||
let session = MockURLSession(responses: [completedJSON, emptyJSON])
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
let completedDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
||||
let emptyDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
||||
XCTAssertEqual(completedDestination, .apply)
|
||||
XCTAssertEqual(emptyDestination, .apply)
|
||||
}
|
||||
|
||||
func testPermissionApplyDestinationFallsBackToApplyOnError() async {
|
||||
let response = HTTPURLResponse(
|
||||
url: URL(string: "https://api-test.zhifly.cn/mock")!,
|
||||
statusCode: 500,
|
||||
httpVersion: nil,
|
||||
headerFields: nil
|
||||
)!
|
||||
let session = MockURLSession(results: [.success((Data(), response))])
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
let destination = await viewModel.resolvePermissionApplyDestination(api: api)
|
||||
XCTAssertEqual(destination, .apply)
|
||||
}
|
||||
|
||||
func testRolePermissionBuildsCommonMenusAndStoreCardRole() async throws {
|
||||
appStore.session.roleCode = AppRoleCode.storeAdmin.rawValue
|
||||
let permissionResponse = RolePermissionResponse(
|
||||
|
||||
@ -53,4 +53,30 @@ final class PermissionApplyStatusViewModelTests: XCTestCase {
|
||||
|
||||
XCTAssertNil(edited)
|
||||
}
|
||||
|
||||
func testCompletedApplicationIsNotDisplayedAsPending() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP004","role_id":1,"role_name":"摄影师","scenic_list":[],"status":2,"status_label":"已通过","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [json])))
|
||||
let viewModel = PermissionApplyStatusViewModel(applyCode: "AP004")
|
||||
|
||||
await viewModel.loadPendingData(api: api)
|
||||
|
||||
XCTAssertNil(viewModel.pendingData)
|
||||
XCTAssertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
func testScenicListDialogState() {
|
||||
let viewModel = PermissionApplyStatusViewModel(applyCode: "AP005")
|
||||
let scenic = ScenicInfo(id: 12, name: "西湖景区", status: 1)
|
||||
viewModel.showScenicList(roleName: "摄影师", scenics: [scenic])
|
||||
|
||||
XCTAssertTrue(viewModel.showScenicListDialog)
|
||||
XCTAssertEqual(viewModel.dialogRoleName, "摄影师")
|
||||
XCTAssertEqual(viewModel.dialogScenicList, [scenic])
|
||||
|
||||
viewModel.hideScenicList()
|
||||
XCTAssertFalse(viewModel.showScenicListDialog)
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ final class PermissionApplyViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(existing?.isDisabled, true)
|
||||
XCTAssertEqual(existing?.isSelected, true)
|
||||
XCTAssertEqual(fresh?.isDisabled, false)
|
||||
if let existing { viewModel.toggleScenic(existing) }
|
||||
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
|
||||
}
|
||||
|
||||
func testSubmitValidationRequiresRoleAndScenic() async {
|
||||
@ -55,21 +57,24 @@ final class PermissionApplyViewModelTests: XCTestCase {
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.submit(api: api)
|
||||
XCTAssertEqual(message, "请选择角色")
|
||||
XCTAssertEqual(message, "请选择景区和角色")
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.submit(api: api)
|
||||
XCTAssertEqual(message, "请至少选择一个景区")
|
||||
XCTAssertEqual(message, "请选择景区和角色")
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
}
|
||||
|
||||
func testSubmitSuccessReturnsApplyCode() async throws {
|
||||
func testSubmitSuccessUsesAndroidPayloadAndCompletion() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let submitJSON = """
|
||||
{"code":100000,"msg":"success","data":{"code":"AP999"}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON, submitJSON])))
|
||||
let session = MockURLSession(responses: [scenicJSON, submitJSON])
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
@ -77,10 +82,138 @@ final class PermissionApplyViewModelTests: XCTestCase {
|
||||
viewModel.toggleScenic(scenic)
|
||||
}
|
||||
|
||||
var applyCode: String?
|
||||
viewModel.onSubmitSuccess = { applyCode = $0 }
|
||||
var didSubmit = false
|
||||
viewModel.onSubmitSuccess = { didSubmit = true }
|
||||
XCTAssertTrue(viewModel.canSubmit)
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertEqual(applyCode, "AP999")
|
||||
XCTAssertTrue(didSubmit)
|
||||
XCTAssertFalse(viewModel.isSubmitting)
|
||||
XCTAssertEqual(session.requests.last?.url?.path, "/api/yf-handset-app/photog/role-apply/submit")
|
||||
let body = try XCTUnwrap(session.requests.last?.httpBody)
|
||||
let object = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
|
||||
XCTAssertEqual(object["role_id"] as? Int, 10)
|
||||
XCTAssertEqual(object["scenic_id"] as? [Int], [101])
|
||||
}
|
||||
|
||||
func testSubmitFailureRestoresEnabledButtonAndKeepsSelection() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let failureJSON = #"{"code":100001,"msg":"failed","data":null}"#.data(using: .utf8)!
|
||||
let api = HomeAPI(
|
||||
client: APIClient(
|
||||
environment: .testing,
|
||||
session: MockURLSession(responses: [scenicJSON, failureJSON])
|
||||
)
|
||||
)
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertEqual(message, "提交失败,请稍后重试")
|
||||
XCTAssertFalse(viewModel.isSubmitting)
|
||||
XCTAssertTrue(viewModel.canSubmit)
|
||||
XCTAssertEqual(viewModel.selectedScenicIds, [101])
|
||||
}
|
||||
|
||||
func testSwitchingRoleClearsScenicSelectionAndLoadedOptions() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
|
||||
|
||||
viewModel.selectRole(RoleOption(id: 11, name: "门店管理员", description: "", isSelected: true))
|
||||
|
||||
XCTAssertEqual(viewModel.selectedRoleId, 11)
|
||||
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
|
||||
XCTAssertTrue(viewModel.availableScenics.isEmpty)
|
||||
}
|
||||
|
||||
func testAvailableRolesAreDeduplicatedByRoleID() {
|
||||
appStore.permissions.saveRolePermissionList([
|
||||
RolePermissionResponse(
|
||||
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
|
||||
scenic: [ScenicInfo(id: 100, name: "景区A", status: 1)]
|
||||
),
|
||||
RolePermissionResponse(
|
||||
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
|
||||
scenic: [ScenicInfo(id: 101, name: "景区B", status: 1)]
|
||||
),
|
||||
])
|
||||
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
|
||||
XCTAssertEqual(viewModel.availableRoles.map(\.id), [10])
|
||||
XCTAssertEqual(viewModel.rolePermissionList.count, 2)
|
||||
}
|
||||
|
||||
func testScenicSearchAndDeletionConfirmation() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":3,"list":[{"id":100,"name":"已有景区"},{"id":101,"name":"西湖景区"},{"id":102,"name":"东湖景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
|
||||
viewModel.updateScenicSearchQuery("西湖")
|
||||
XCTAssertEqual(viewModel.filteredScenics.map(\.id), [101])
|
||||
let scenic = try XCTUnwrap(viewModel.filteredScenics.first)
|
||||
viewModel.toggleScenic(scenic)
|
||||
XCTAssertTrue(viewModel.canSubmit)
|
||||
|
||||
viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name)
|
||||
XCTAssertEqual(viewModel.pendingDeletion, .scenic(id: 101, name: "西湖景区"))
|
||||
viewModel.cancelDeletion()
|
||||
XCTAssertEqual(viewModel.selectedScenicIds, [101])
|
||||
|
||||
viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name)
|
||||
viewModel.confirmDeletion()
|
||||
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
}
|
||||
|
||||
func testRoleDeletionRequiresConfirmationAndClearsScenics() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
|
||||
|
||||
viewModel.clearRoleSelection()
|
||||
XCTAssertEqual(viewModel.pendingDeletion, .role(name: "摄影师"))
|
||||
XCTAssertEqual(viewModel.selectedRoleId, 10)
|
||||
viewModel.confirmDeletion()
|
||||
|
||||
XCTAssertNil(viewModel.selectedRoleId)
|
||||
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
|
||||
XCTAssertTrue(viewModel.availableScenics.isEmpty)
|
||||
}
|
||||
|
||||
func testExistingRoleListAndScenicDialogState() {
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
XCTAssertEqual(viewModel.rolePermissionList.count, 1)
|
||||
XCTAssertEqual(viewModel.availableRoles.first?.description, "拍摄")
|
||||
|
||||
let scenics = viewModel.rolePermissionList[0].scenic
|
||||
viewModel.showScenicList(roleName: "摄影师", scenics: scenics)
|
||||
XCTAssertTrue(viewModel.showScenicListDialog)
|
||||
XCTAssertEqual(viewModel.dialogRoleName, "摄影师")
|
||||
XCTAssertEqual(viewModel.dialogScenicList.map(\.id), [100])
|
||||
viewModel.hideScenicList()
|
||||
XCTAssertFalse(viewModel.showScenicListDialog)
|
||||
}
|
||||
}
|
||||
|
||||
25
suixinkanTests/PermissionApplyViewsTests.swift
Normal file
25
suixinkanTests/PermissionApplyViewsTests.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// PermissionApplyViewsTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 权限申请页标签流式布局测试。
|
||||
@MainActor
|
||||
final class PermissionApplyViewsTests: XCTestCase {
|
||||
|
||||
func testSelectedChipsAreLeftAlignedAndWrapWithAndroidSpacing() {
|
||||
let frames = PermissionChipLayoutMetrics.makeFrames(
|
||||
itemWidths: [80, 120, 90],
|
||||
containerWidth: 260
|
||||
)
|
||||
|
||||
XCTAssertEqual(frames[0].minX, 0, accuracy: 0.5)
|
||||
XCTAssertEqual(frames[1].minX, frames[0].maxX + 12, accuracy: 0.5)
|
||||
XCTAssertEqual(frames[2].minX, 0, accuracy: 0.5)
|
||||
XCTAssertEqual(frames[2].minY, frames[0].maxY + 2, accuracy: 0.5)
|
||||
XCTAssertTrue(frames.allSatisfy { $0.maxX <= 260.5 })
|
||||
}
|
||||
}
|
||||
@ -304,7 +304,15 @@ final class ScenicQueueFeatureTests: XCTestCase {
|
||||
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
|
||||
@ -321,6 +329,58 @@ final class ScenicQueueFeatureTests: XCTestCase {
|
||||
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")
|
||||
@ -471,10 +531,13 @@ private final class MockAliyunNLSTokenProvider: AliyunNLSTokenProviding {
|
||||
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
|
||||
}
|
||||
@ -507,7 +570,7 @@ private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
|
||||
}
|
||||
|
||||
func paramValue(for key: String) -> String? {
|
||||
nil
|
||||
paramValues[key]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,25 @@ final class StatisticsViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.rangeValue(for: "本月"), "4")
|
||||
}
|
||||
|
||||
func testPeriodButtonCapsuleCornerRadiusUsesHalfHeight() {
|
||||
let button = StatisticsPeriodButton(frame: CGRect(x: 0, y: 0, width: 80, height: 32))
|
||||
|
||||
button.setNeedsLayout()
|
||||
button.layoutIfNeeded()
|
||||
|
||||
XCTAssertEqual(button.layer.cornerRadius, 16)
|
||||
}
|
||||
|
||||
func testPeriodButtonFixedCornerRadiusDoesNotFollowHeight() {
|
||||
let button = StatisticsPeriodButton(frame: CGRect(x: 0, y: 0, width: 80, height: 32))
|
||||
button.cornerStyle = .fixed(10)
|
||||
|
||||
button.setNeedsLayout()
|
||||
button.layoutIfNeeded()
|
||||
|
||||
XCTAssertEqual(button.layer.cornerRadius, 10)
|
||||
}
|
||||
|
||||
func testFormattedSelectedPeriodTimeForToday() {
|
||||
let viewModel = StatisticsViewModel()
|
||||
let text = viewModel.formattedSelectedPeriodTime(for: "今日")
|
||||
|
||||
71
suixinkanTests/UIButtonConfigurationTests.swift
Normal file
71
suixinkanTests/UIButtonConfigurationTests.swift
Normal file
@ -0,0 +1,71 @@
|
||||
//
|
||||
// UIButtonConfigurationTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// `UIButton` configuration 内容边距辅助方法测试。
|
||||
@MainActor
|
||||
final class UIButtonConfigurationTests: XCTestCase {
|
||||
|
||||
func testContentInsetsCreatePlainConfiguration() {
|
||||
let button = UIButton(type: .system)
|
||||
|
||||
button.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 2, leading: 4, bottom: 6, trailing: 8)
|
||||
)
|
||||
|
||||
let contentInsets = button.configuration?.contentInsets
|
||||
XCTAssertEqual(contentInsets?.top, 2)
|
||||
XCTAssertEqual(contentInsets?.leading, 4)
|
||||
XCTAssertEqual(contentInsets?.bottom, 6)
|
||||
XCTAssertEqual(contentInsets?.trailing, 8)
|
||||
}
|
||||
|
||||
func testContentInsetsPreserveExistingConfiguration() {
|
||||
let image = UIImage(systemName: "star.fill")
|
||||
var configuration = UIButton.Configuration.filled()
|
||||
configuration.title = "保留标题"
|
||||
configuration.image = image
|
||||
configuration.imagePlacement = .trailing
|
||||
configuration.imagePadding = 7
|
||||
configuration.baseForegroundColor = .red
|
||||
let button = UIButton(configuration: configuration)
|
||||
|
||||
button.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 1, leading: 3, bottom: 5, trailing: 9)
|
||||
)
|
||||
|
||||
XCTAssertEqual(button.configuration?.title, "保留标题")
|
||||
XCTAssertEqual(button.configuration?.image, image)
|
||||
XCTAssertEqual(button.configuration?.imagePlacement, .trailing)
|
||||
XCTAssertEqual(button.configuration?.imagePadding, 7)
|
||||
XCTAssertEqual(button.configuration?.baseForegroundColor, .red)
|
||||
XCTAssertEqual(button.configuration?.contentInsets.leading, 3)
|
||||
XCTAssertEqual(button.configuration?.contentInsets.trailing, 9)
|
||||
}
|
||||
|
||||
func testContentInsetsPreserveLegacyButtonContentAndAllowUpdates() {
|
||||
let image = UIImage(systemName: "star")
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("原始标题", for: .normal)
|
||||
button.setTitleColor(.blue, for: .normal)
|
||||
button.setImage(image, for: .normal)
|
||||
|
||||
button.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)
|
||||
)
|
||||
|
||||
XCTAssertEqual(button.currentTitle, "原始标题")
|
||||
XCTAssertEqual(button.currentTitleColor, .blue)
|
||||
XCTAssertEqual(button.currentImage, image)
|
||||
|
||||
button.setTitle("更新标题", for: .normal)
|
||||
button.setTitleColor(.red, for: .normal)
|
||||
XCTAssertEqual(button.currentTitle, "更新标题")
|
||||
XCTAssertEqual(button.currentTitleColor, .red)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user