import Foundation import Notifying import Recording import Testing @testable import Attendi @Suite("Content view model") struct ContentViewModelTests { typealias Model = ContentView.Model // MARK: Load @MainActor @Suite("Load") struct Load { @Test func `load fills the supported locales and aligns the picked locale`() async { let preinstaller = PreinstallingMock() preinstaller.localesSupported = [.dutch, .english] preinstaller.equivalents = [.current: .english] let model = Model(preinstaller: preinstaller) await model.load(identifier: "") #expect(model.locale == .english) #expect(model.locales.count == 2) #expect(model.locales.contains(.dutch)) #expect(model.locales.contains(.english)) } @Test func `load restores the persisted locale`() async { let preinstaller = PreinstallingMock() preinstaller.localesSupported = [.dutch, .english] preinstaller.equivalents = [.dutch: .dutch] let model = Model(preinstaller: preinstaller) await model.load(identifier: Locale.dutch.identifier) #expect(model.locale == .dutch) } @Test func `load falls back to the default locale when the persisted locale is unsupported`() async { let preinstaller = PreinstallingMock() preinstaller.localesSupported = [.dutch, .english] preinstaller.equivalents = [.byDefault: .english] let model = Model(preinstaller: preinstaller) await model.load(identifier: "fr-FR") #expect(model.locale == .english) } @Test func `load preinstalls the aligned locale`() async throws { let preinstaller = PreinstallingMock() preinstaller.localesSupported = [.dutch, .english] preinstaller.equivalents = [.current: .english] let model = Model(preinstaller: preinstaller) await model.load(identifier: "") try await Task.sleep(for: .seconds(0.1)) #expect(preinstaller.localesPreinstalled == [.english]) } } // MARK: Preinstall @MainActor @Suite("Preinstall") struct Preinstall { @Test func `preinstalling the same locale twice runs once`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() model.preinstall() try await Task.sleep(for: .seconds(0.1)) #expect(preinstaller.localesPreinstalled == [.dutch]) } @Test func `preinstalling a newly picked locale runs again`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() model.locale = .english model.preinstall() try await Task.sleep(for: .seconds(0.1)) #expect(preinstaller.localesPreinstalled == [.dutch, .english]) } } // MARK: Events @MainActor @Suite("Events") struct Events { @Test func `a started event posts an info notification`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() try await Task.sleep(for: .seconds(0.1)) preinstaller.emit(.started(.dutch)) try await Task.sleep(for: .seconds(0.1)) #expect(model.notifier.notifications.count == 1) #expect(model.notifier.notifications.first?.kind == .info) #expect(model.notifier.notifications.first?.symbol == "arrow.down.circle") #expect(model.notifier.notifications.first?.message == String(localized: .viewRecordingNotificationDownloadStarted(model.name(for: .dutch)))) } @Test func `a cancelled event posts a warning notification`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() try await Task.sleep(for: .seconds(0.1)) preinstaller.emit(.cancelled(.dutch)) try await Task.sleep(for: .seconds(0.1)) #expect(model.notifier.notifications.first?.kind == .warning) #expect(model.notifier.notifications.first?.symbol == "xmark.circle") #expect(model.notifier.notifications.first?.message == String(localized: .viewRecordingNotificationDownloadCancelled(model.name(for: .dutch)))) } @Test func `a failed event posts an error notification`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() try await Task.sleep(for: .seconds(0.1)) preinstaller.emit(.failed(.dutch)) try await Task.sleep(for: .seconds(0.1)) #expect(model.notifier.notifications.first?.kind == .error) #expect(model.notifier.notifications.first?.symbol == "exclamationmark.triangle") #expect(model.notifier.notifications.first?.message == String(localized: .viewRecordingNotificationDownloadFailed(model.name(for: .dutch)))) } @Test func `a cancellation of the current locale lets a re-pick retry`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .dutch model.preinstall() try await Task.sleep(for: .seconds(0.1)) preinstaller.emit(.cancelled(.dutch)) try await Task.sleep(for: .seconds(0.1)) model.preinstall() try await Task.sleep(for: .seconds(0.1)) #expect(preinstaller.localesPreinstalled == [.dutch, .dutch]) } @Test func `a cancellation of another locale keeps the current preinstallation`() async throws { let preinstaller = PreinstallingMock() let model = Model(preinstaller: preinstaller) model.locale = .english model.preinstall() try await Task.sleep(for: .seconds(0.1)) preinstaller.emit(.cancelled(.dutch)) try await Task.sleep(for: .seconds(0.1)) model.preinstall() try await Task.sleep(for: .seconds(0.1)) #expect(preinstaller.localesPreinstalled == [.english]) } } // MARK: Name @MainActor @Suite("Name") struct Name { @Test func `name pairs the localized language name with the region flag`() { let model = Model(preinstaller: PreinstallingMock()) #expect(model.name(for: .english) == "\(languageName("en")) πŸ‡ΊπŸ‡Έ") #expect(model.name(for: .dutch) == "\(languageName("nl")) πŸ‡³πŸ‡±") } @Test func `name omits the flag for a locale without a region`() { let model = Model(preinstaller: PreinstallingMock()) #expect(!model.name(for: Locale(identifier: "en")).containsFlag) } @Test func `name omits the flag for a numeric region`() { let model = Model(preinstaller: PreinstallingMock()) #expect(!model.name(for: Locale(identifier: "es-419")).containsFlag) } } // MARK: Navigation Title @MainActor @Suite("Navigation title") struct NavigationTitle { @Test func `navigation title names the selected locale`() { let model = Model(preinstaller: PreinstallingMock()) model.locale = .english #expect(String(localized: model.navigationTitle) == "Transcribe to \(model.name(for: .english))") } @Test func `navigation title updates when the locale changes`() { let model = Model(preinstaller: PreinstallingMock()) model.locale = .english let english = String(localized: model.navigationTitle) model.locale = .dutch let dutch = String(localized: model.navigationTitle) #expect(english == "Transcribe to \(model.name(for: .english))") #expect(dutch == "Transcribe to \(model.name(for: .dutch))") #expect(english != dutch) } } } // MARK: - Constants private extension Locale { /// The locale the model falls back to when no equivalent of the current locale is supported. static let byDefault = Locale(identifier: "en_US") /// A supported locale to preinstall in the tests. static let dutch = Locale(identifier: "nl-NL") /// Another supported locale to preinstall in the tests. static let english = Locale(identifier: "en-US") } // MARK: - Helpers /// The capitalized, localized language name for a language code, mirroring how the model builds the language part of a locale's name. /// /// - Parameter code: The language code to name. /// - Returns: The localized language name in the current locale, capitalized. private func languageName(_ code: String) -> String { (Locale.current.localizedString(forLanguageCode: code) ?? code).localizedCapitalized } private extension String { /// Whether the string contains a Regional Indicator Symbol scalar β€” part of a flag emoji. var containsFlag: Bool { unicodeScalars.contains { $0.value >= 0x1F1E6 && $0.value <= 0x1F1FF } } }