Premium/enhanced voices ship with the tier in their name (e.g. "Ava (Premium)"), so appending "— Premium" produced "Ava (Premium) — Premium" in the voice picker. Move the label logic to SpeechSettings.displayLabel and skip the suffix when the name already contains the tier (still name-only for default voices). Deterministic string helper, unit-tested. Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
68 lines
2.9 KiB
Swift
68 lines
2.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
import AVFoundation
|
|
@testable import Workouts
|
|
|
|
/// Locks the voice-resolution contract: the picker offers English voices, and the
|
|
/// announcer speaks with the user's stored choice when set, falling back to a valid
|
|
/// English voice otherwise.
|
|
struct SpeechSettingsTests {
|
|
|
|
@Test func availableVoicesAreEnglish() {
|
|
let voices = SpeechSettings.availableVoices()
|
|
#expect(!voices.isEmpty)
|
|
#expect(voices.allSatisfy { $0.language.hasPrefix("en") })
|
|
}
|
|
|
|
@Test func resolvedVoiceHonorsStoredIdentifierThenFallsBack() {
|
|
let key = SpeechSettings.voiceIdentifierKey
|
|
let original = UserDefaults.standard.string(forKey: key)
|
|
defer {
|
|
if let original { UserDefaults.standard.set(original, forKey: key) }
|
|
else { UserDefaults.standard.removeObject(forKey: key) }
|
|
}
|
|
|
|
// Automatic (empty) resolves to some installed English voice.
|
|
UserDefaults.standard.set("", forKey: key)
|
|
let auto = SpeechSettings.resolvedVoice()
|
|
#expect(auto != nil)
|
|
#expect(auto?.language.hasPrefix("en") == true)
|
|
|
|
// A specific installed voice resolves to exactly that voice.
|
|
let target = try? #require(SpeechSettings.availableVoices().first)
|
|
if let target {
|
|
UserDefaults.standard.set(target.identifier, forKey: key)
|
|
#expect(SpeechSettings.resolvedVoice()?.identifier == target.identifier)
|
|
}
|
|
}
|
|
|
|
@Test func displayLabelAvoidsRedundantQualitySuffix() {
|
|
// Default voices: name only, no "— Default" noise.
|
|
#expect(SpeechSettings.displayLabel(name: "Samantha", quality: .default) == "Samantha")
|
|
|
|
// Enhanced/premium voices get the tier appended when the name lacks it.
|
|
#expect(SpeechSettings.displayLabel(name: "Samantha", quality: .enhanced) == "Samantha — Enhanced")
|
|
|
|
// …but not when the name already carries it (e.g. "Ava (Premium)").
|
|
#expect(SpeechSettings.displayLabel(name: "Ava (Premium)", quality: .premium) == "Ava (Premium)")
|
|
#expect(SpeechSettings.displayLabel(name: "Daniel (Enhanced)", quality: .enhanced) == "Daniel (Enhanced)")
|
|
}
|
|
|
|
@Test func prosodyValuesClampToValidRanges() {
|
|
let rateKey = SpeechSettings.rateKey
|
|
let originalRate = UserDefaults.standard.object(forKey: rateKey) as? Double
|
|
defer {
|
|
if let originalRate { UserDefaults.standard.set(originalRate, forKey: rateKey) }
|
|
else { UserDefaults.standard.removeObject(forKey: rateKey) }
|
|
}
|
|
|
|
// An out-of-range stored value is clamped, never passed raw to the utterance.
|
|
UserDefaults.standard.set(99.0, forKey: rateKey)
|
|
#expect(Double(SpeechSettings.rate) <= SpeechSettings.rateRange.upperBound)
|
|
|
|
// A missing value resolves to the shared default.
|
|
UserDefaults.standard.removeObject(forKey: rateKey)
|
|
#expect(SpeechSettings.rate == Float(SpeechSettings.rateDefault))
|
|
}
|
|
}
|