diff --git a/Packages/MoraCore/Sources/MoraCore/EnglishL1Profile.swift b/Packages/MoraCore/Sources/MoraCore/EnglishL1Profile.swift index 2fcc16ad..1952dc1e 100644 --- a/Packages/MoraCore/Sources/MoraCore/EnglishL1Profile.swift +++ b/Packages/MoraCore/Sources/MoraCore/EnglishL1Profile.swift @@ -162,7 +162,10 @@ public struct EnglishL1Profile: L1Profile { completionComeBack: "See you tomorrow!", a11yCloseSession: "End the quest", a11yMicButton: "Mic", - a11yStreakChip: { days in "\(days)-day streak" } - // PR 3 will append the four language-switch fields. + a11yStreakChip: { days in "\(days)-day streak" }, + homeChangeLanguageButton: "Change language", + languageSwitchSheetTitle: "Pick a language", + languageSwitchSheetCancel: "Cancel", + languageSwitchSheetConfirm: "Done" ) } diff --git a/Packages/MoraCore/Sources/MoraCore/JapaneseL1Profile.swift b/Packages/MoraCore/Sources/MoraCore/JapaneseL1Profile.swift index 47f4f693..66380c28 100644 --- a/Packages/MoraCore/Sources/MoraCore/JapaneseL1Profile.swift +++ b/Packages/MoraCore/Sources/MoraCore/JapaneseL1Profile.swift @@ -297,7 +297,11 @@ public struct JapaneseL1Profile: L1Profile { a11yCloseSession: "クエストを おわる", a11yMicButton: "マイク", // `日` G1 kept. - a11yStreakChip: { days in "\(days)日 れんぞく" } + a11yStreakChip: { days in "\(days)日 れんぞく" }, + homeChangeLanguageButton: "ことばを かえる", + languageSwitchSheetTitle: "ことばを えらぶ", + languageSwitchSheetCancel: "キャンセル", + languageSwitchSheetConfirm: "OK" ) /// Ages ≤6 (entry tier). Kanji budget: empty — every kanji collapses to @@ -434,7 +438,11 @@ public struct JapaneseL1Profile: L1Profile { a11yCloseSession: "クエストを おわる", a11yMicButton: "マイク", // `日` G1 → にち - a11yStreakChip: { days in "\(days)にち れんぞく" } + a11yStreakChip: { days in "\(days)にち れんぞく" }, + homeChangeLanguageButton: "ことばを かえる", + languageSwitchSheetTitle: "ことばを えらぶ", + languageSwitchSheetCancel: "キャンセル", + languageSwitchSheetConfirm: "OK" ) /// Ages 8+ (advanced tier). Kanji budget: only JPKanjiLevel.grade1And2 @@ -554,6 +562,10 @@ public struct JapaneseL1Profile: L1Profile { completionComeBack: "明日も またね", a11yCloseSession: "クエストを おわる", a11yMicButton: "マイク", - a11yStreakChip: { days in "\(days)日 れんぞく" } + a11yStreakChip: { days in "\(days)日 れんぞく" }, + homeChangeLanguageButton: "ことばを かえる", + languageSwitchSheetTitle: "ことばを えらぶ", + languageSwitchSheetCancel: "キャンセル", + languageSwitchSheetConfirm: "OK" ) } diff --git a/Packages/MoraCore/Sources/MoraCore/KoreanL1Profile.swift b/Packages/MoraCore/Sources/MoraCore/KoreanL1Profile.swift index f0fa1a97..24737e2c 100644 --- a/Packages/MoraCore/Sources/MoraCore/KoreanL1Profile.swift +++ b/Packages/MoraCore/Sources/MoraCore/KoreanL1Profile.swift @@ -203,6 +203,10 @@ public struct KoreanL1Profile: L1Profile { completionComeBack: "내일 또 만나요", a11yCloseSession: "퀘스트를 끝내기", a11yMicButton: "마이크", - a11yStreakChip: { days in "\(days)일 연속" } + a11yStreakChip: { days in "\(days)일 연속" }, + homeChangeLanguageButton: "언어 바꾸기", + languageSwitchSheetTitle: "언어 선택", + languageSwitchSheetCancel: "취소", + languageSwitchSheetConfirm: "확인" ) } diff --git a/Packages/MoraCore/Sources/MoraCore/MoraStrings.swift b/Packages/MoraCore/Sources/MoraCore/MoraStrings.swift index feab455a..abe10b66 100644 --- a/Packages/MoraCore/Sources/MoraCore/MoraStrings.swift +++ b/Packages/MoraCore/Sources/MoraCore/MoraStrings.swift @@ -128,6 +128,12 @@ public struct MoraStrings: Sendable { public let a11yMicButton: String public let a11yStreakChip: @Sendable (Int) -> String + // In-app language switch (Home globe button + sheet) + public let homeChangeLanguageButton: String + public let languageSwitchSheetTitle: String + public let languageSwitchSheetCancel: String + public let languageSwitchSheetConfirm: String + public init( ageOnboardingPrompt: String, ageOnboardingCTA: String, @@ -199,7 +205,11 @@ public struct MoraStrings: Sendable { completionScore: @escaping @Sendable (Int, Int) -> String, completionComeBack: String, a11yCloseSession: String, a11yMicButton: String, - a11yStreakChip: @escaping @Sendable (Int) -> String + a11yStreakChip: @escaping @Sendable (Int) -> String, + homeChangeLanguageButton: String, + languageSwitchSheetTitle: String, + languageSwitchSheetCancel: String, + languageSwitchSheetConfirm: String ) { self.ageOnboardingPrompt = ageOnboardingPrompt self.ageOnboardingCTA = ageOnboardingCTA @@ -290,6 +300,10 @@ public struct MoraStrings: Sendable { self.a11yCloseSession = a11yCloseSession self.a11yMicButton = a11yMicButton self.a11yStreakChip = a11yStreakChip + self.homeChangeLanguageButton = homeChangeLanguageButton + self.languageSwitchSheetTitle = languageSwitchSheetTitle + self.languageSwitchSheetCancel = languageSwitchSheetCancel + self.languageSwitchSheetConfirm = languageSwitchSheetConfirm } } diff --git a/Packages/MoraCore/Tests/MoraCoreTests/LocaleScriptBudgetTests.swift b/Packages/MoraCore/Tests/MoraCoreTests/LocaleScriptBudgetTests.swift index 7794846c..203d6f53 100644 --- a/Packages/MoraCore/Tests/MoraCoreTests/LocaleScriptBudgetTests.swift +++ b/Packages/MoraCore/Tests/MoraCoreTests/LocaleScriptBudgetTests.swift @@ -147,5 +147,9 @@ func everyStringField(_ s: MoraStrings) -> [(name: String, value: String)] { ("a11yCloseSession", s.a11yCloseSession), ("a11yMicButton", s.a11yMicButton), ("a11yStreakChip(5)", s.a11yStreakChip(5)), + ("homeChangeLanguageButton", s.homeChangeLanguageButton), + ("languageSwitchSheetTitle", s.languageSwitchSheetTitle), + ("languageSwitchSheetCancel", s.languageSwitchSheetCancel), + ("languageSwitchSheetConfirm", s.languageSwitchSheetConfirm), ] } diff --git a/Packages/MoraUI/Sources/MoraUI/Home/HomeView.swift b/Packages/MoraUI/Sources/MoraUI/Home/HomeView.swift index 5eccd7c8..1bdd8a44 100644 --- a/Packages/MoraUI/Sources/MoraUI/Home/HomeView.swift +++ b/Packages/MoraUI/Sources/MoraUI/Home/HomeView.swift @@ -8,6 +8,8 @@ import SwiftUI import UIKit #endif +private let homeViewLog = Logger(subsystem: "tech.reenable.Mora", category: "HomeView") + #if DEBUG private let debugBarLog = Logger(subsystem: "tech.reenable.Mora", category: "DebugBar") #endif @@ -60,11 +62,10 @@ public struct HomeView: View { // after downloading a premium voice flips the gate off immediately. @State private var needsBetterVoice: Bool = AppleTTSEngine.needsEnhancedVoice @State private var installedVoices: [String] = AppleTTSEngine.installedEnglishVoiceSummaries() + @State private var languageSheet: LanguageSwitchSheet? @Environment(\.scenePhase) private var scenePhase @Environment(\.moraStrings) private var strings - #if DEBUG - @Environment(\.modelContext) private var debugContext - #endif + @Environment(\.modelContext) private var modelContext public init() {} @@ -98,6 +99,10 @@ public struct HomeView: View { YokaiIntroFlow(mode: .replay) { showYokaiIntroReplay = false } .environment(\.moraStrings, strings) } + .sheet(item: $languageSheet) { model in + LanguageSwitchSheetView(model: model) + .environment(\.moraStrings, strings) + } #if os(iOS) .navigationBarHidden(true) #endif @@ -106,6 +111,38 @@ public struct HomeView: View { private var header: some View { HStack { wordmark + Button { + guard let profile = profiles.first else { return } + let previousID = profile.l1Identifier + languageSheet = LanguageSwitchSheet( + currentIdentifier: previousID, + onCommit: { newID in + profile.l1Identifier = newID + do { + try modelContext.save() + languageSheet = nil + } catch { + // Revert the in-memory change so it doesn't drift + // from disk; leave the sheet open so the user sees + // their tap didn't take rather than silently losing + // it on next launch. + profile.l1Identifier = previousID + homeViewLog.error( + "Failed to persist language switch to \(newID, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + } + }, + onCancel: { + languageSheet = nil + } + ) + } label: { + Image(systemName: "globe") + .foregroundStyle(MoraTheme.Ink.muted) + } + .buttonStyle(.plain) + .accessibilityLabel(strings.homeChangeLanguageButton) + Spacer() StreakChip(count: streaks.first?.currentCount ?? 0) } @@ -390,7 +427,7 @@ public struct HomeView: View { streak = existing } else { streak = DailyStreak() - debugContext.insert(streak) + modelContext.insert(streak) } // Route through the real DailyStreak rules instead of mutating fields // directly: simulate "the next day" so each tap counts as one more @@ -430,7 +467,7 @@ public struct HomeView: View { var befriendedIDs = Set(bestiary.map(\.yokaiID)) if let id = currentYokaiID, !befriendedIDs.contains(id) { - debugContext.insert(BestiaryEntryEntity(yokaiID: id, befriendedAt: now)) + modelContext.insert(BestiaryEntryEntity(yokaiID: id, befriendedAt: now)) befriendedIDs.insert(id) } @@ -438,7 +475,7 @@ public struct HomeView: View { !befriendedIDs.contains($0) }) if let nextID { - debugContext.insert( + modelContext.insert( YokaiEncounterEntity( yokaiID: nextID, weekStart: now, @@ -460,9 +497,9 @@ public struct HomeView: View { private func resetCurriculum() { debugBarLog.info("resetCurriculum: wiping encounters, bestiary, cameos, streak, profile.createdAt") do { - try debugContext.delete(model: YokaiEncounterEntity.self) - try debugContext.delete(model: BestiaryEntryEntity.self) - try debugContext.delete(model: YokaiCameoEntity.self) + try modelContext.delete(model: YokaiEncounterEntity.self) + try modelContext.delete(model: BestiaryEntryEntity.self) + try modelContext.delete(model: YokaiCameoEntity.self) } catch { assertionFailure("Failed to delete debug models: \(error)") } @@ -479,7 +516,7 @@ public struct HomeView: View { private func persistDebugChanges() { do { - try debugContext.save() + try modelContext.save() } catch { assertionFailure("Failed to persist debug change: \(error)") } diff --git a/Packages/MoraUI/Sources/MoraUI/LanguageAge/AgePickerView.swift b/Packages/MoraUI/Sources/MoraUI/LanguageAge/AgePickerView.swift index 17eadfb8..21fecab6 100644 --- a/Packages/MoraUI/Sources/MoraUI/LanguageAge/AgePickerView.swift +++ b/Packages/MoraUI/Sources/MoraUI/LanguageAge/AgePickerView.swift @@ -7,15 +7,6 @@ struct AgePickerView: View { @Binding var selectedAge: Int? let onContinue: () -> Void - /// 4..12 plus a sentinel `13` that renders as "13+" and maps to the - /// 13-and-over bucket internally. Under-4 is out of scope in alpha. - private let ages: [Int] = Array(4...12) + [13] - private let columns = [ - GridItem(.flexible(), spacing: MoraTheme.Space.md), - GridItem(.flexible(), spacing: MoraTheme.Space.md), - GridItem(.flexible(), spacing: MoraTheme.Space.md), - ] - var body: some View { ZStack { MoraTheme.Background.page.ignoresSafeArea() @@ -25,8 +16,8 @@ struct AgePickerView: View { .foregroundStyle(MoraTheme.Ink.primary) .padding(.top, MoraTheme.Space.xxl) - LazyVGrid(columns: columns, spacing: MoraTheme.Space.md) { - ForEach(ages, id: \.self) { age in + HStack(spacing: MoraTheme.Space.md) { + ForEach(LanguageAgeFlow.ageOptions, id: \.self) { age in tile(age) } } @@ -62,12 +53,11 @@ struct AgePickerView: View { private func tile(_ age: Int) -> some View { let selected = selectedAge == age - let label = age == 13 ? "13+" : "\(age)" return Button { selectedAge = age } label: { - Text(label) - .font(MoraType.hero(72)) + Text("\(age)") + .font(MoraType.hero(120)) .foregroundStyle(MoraTheme.Ink.primary) .frame(maxWidth: .infinity, minHeight: 120) .background( diff --git a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageAgeFlow.swift b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageAgeFlow.swift index d5c528bd..91ca4106 100644 --- a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageAgeFlow.swift +++ b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageAgeFlow.swift @@ -9,7 +9,7 @@ import SwiftUI final class LanguageAgeState { var step: Step = .language var selectedLanguageID: String - var selectedAge: Int? = 8 // pre-selected per spec §6.2 + var selectedAge: Int? = LanguageAgeFlow.defaultAge // pre-selected per spec §7.2 static let onboardedKey = "tech.reenable.Mora.languageAgeOnboarded" @@ -99,6 +99,13 @@ public struct LanguageAgeFlow: View { self.onFinished = onFinished } + /// Age tiles shown in Step 2. Narrowed in PR 3 to the dyslexia + /// intervention window (6–8 = JP 小学校低学年). See spec §7.2. + public static let ageOptions: [Int] = [6, 7, 8] + + /// Default selected age in Step 2 — middle of the target range. + public static let defaultAge: Int = 7 + /// Identifiers of language rows that are tap-enabled. See spec §7.4. public static let activeLanguageIdentifiers: [String] = ["ja", "ko", "en"] diff --git a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePicker.swift b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePicker.swift new file mode 100644 index 00000000..6ca11c0b --- /dev/null +++ b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePicker.swift @@ -0,0 +1,99 @@ +// Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePicker.swift +import MoraCore +import SwiftUI + +/// Reusable language-selection row list. +/// +/// Renders the locale-neutral header and one tappable row per language. +/// Active vs. coming-soon status is driven by `LanguageAgeFlow` so the +/// activation list stays in one place. +public struct LanguagePicker: View { + @Binding public var selection: String + + public init(selection: Binding) { + _selection = selection + } + + private struct Option: Identifiable { + let id: String + let label: String + let enabled: Bool + } + + private static let allOptions: [(id: String, label: String)] = [ + (id: "ja", label: "にほんご"), + (id: "ko", label: "한국어"), + (id: "zh", label: "中文"), + (id: "en", label: "English"), + ] + + private var options: [Option] { + let active = Set(LanguageAgeFlow.activeLanguageIdentifiers) + return Self.allOptions.map { entry in + Option(id: entry.id, label: entry.label, enabled: active.contains(entry.id)) + } + } + + public var body: some View { + VStack(spacing: MoraTheme.Space.xl) { + Text("Language / 言語 / 语言 / 언어") + .font(MoraType.label()) + .foregroundStyle(MoraTheme.Ink.muted) + .padding(.top, MoraTheme.Space.xxl) + + VStack(spacing: MoraTheme.Space.sm) { + ForEach(options) { option in + row(option) + } + } + .padding(.horizontal, MoraTheme.Space.xxl) + } + } + + private func row(_ option: Option) -> some View { + let selected = option.id == selection + return Button { + guard option.enabled else { return } + selection = option.id + } label: { + HStack { + Text(option.label) + .font(MoraType.heading()) + .foregroundStyle( + option.enabled + ? MoraTheme.Ink.primary + : MoraTheme.Ink.muted + ) + if !option.enabled { + Spacer() + Text("Coming soon") + .font(MoraType.pill()) + .foregroundStyle(MoraTheme.Ink.muted) + } else if selected { + Spacer() + Image(systemName: "checkmark") + .foregroundStyle(MoraTheme.Accent.orange) + } else { + Spacer() + } + } + .padding(MoraTheme.Space.md) + .background( + selected + ? MoraTheme.Background.peach + : MoraTheme.Background.cream, + in: RoundedRectangle(cornerRadius: MoraTheme.Radius.tile) + ) + .overlay( + RoundedRectangle(cornerRadius: MoraTheme.Radius.tile) + .stroke( + selected ? MoraTheme.Accent.orange : .clear, + lineWidth: 3 + ) + ) + } + .buttonStyle(.plain) + .disabled(!option.enabled) + .accessibilityAddTraits(selected ? .isSelected : []) + } +} diff --git a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePickerView.swift b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePickerView.swift index a916a820..edaed7e4 100644 --- a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePickerView.swift +++ b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePickerView.swift @@ -6,41 +6,11 @@ struct LanguagePickerView: View { @Binding var selectedLanguageID: String let onContinue: () -> Void - private struct Option: Identifiable { - let id: String - let label: String - let enabled: Bool - } - - private static let allOptions: [(id: String, label: String)] = [ - (id: "ja", label: "にほんご"), - (id: "ko", label: "한국어"), - (id: "zh", label: "中文"), - (id: "en", label: "English"), - ] - - private var options: [Option] { - let active = Set(LanguageAgeFlow.activeLanguageIdentifiers) - return Self.allOptions.map { entry in - Option(id: entry.id, label: entry.label, enabled: active.contains(entry.id)) - } - } - var body: some View { ZStack { MoraTheme.Background.page.ignoresSafeArea() VStack(spacing: MoraTheme.Space.xl) { - Text("Language / 言語 / 语言 / 언어") - .font(MoraType.label()) - .foregroundStyle(MoraTheme.Ink.muted) - .padding(.top, MoraTheme.Space.xxl) - - VStack(spacing: MoraTheme.Space.sm) { - ForEach(options) { option in - row(option) - } - } - .padding(.horizontal, MoraTheme.Space.xxl) + LanguagePicker(selection: $selectedLanguageID) Spacer() @@ -71,51 +41,4 @@ struct LanguagePickerView: View { } } } - - private func row(_ option: Option) -> some View { - let selected = option.id == selectedLanguageID - return Button { - guard option.enabled else { return } - selectedLanguageID = option.id - } label: { - HStack { - Text(option.label) - .font(MoraType.heading()) - .foregroundStyle( - option.enabled - ? MoraTheme.Ink.primary - : MoraTheme.Ink.muted - ) - if !option.enabled { - Spacer() - Text("Coming soon") - .font(MoraType.pill()) - .foregroundStyle(MoraTheme.Ink.muted) - } else if selected { - Spacer() - Image(systemName: "checkmark") - .foregroundStyle(MoraTheme.Accent.orange) - } else { - Spacer() - } - } - .padding(MoraTheme.Space.md) - .background( - selected - ? MoraTheme.Background.peach - : MoraTheme.Background.cream, - in: RoundedRectangle(cornerRadius: MoraTheme.Radius.tile) - ) - .overlay( - RoundedRectangle(cornerRadius: MoraTheme.Radius.tile) - .stroke( - selected ? MoraTheme.Accent.orange : .clear, - lineWidth: 3 - ) - ) - } - .buttonStyle(.plain) - .disabled(!option.enabled) - .accessibilityAddTraits(selected ? .isSelected : []) - } } diff --git a/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageSwitchSheet.swift b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageSwitchSheet.swift new file mode 100644 index 00000000..7e922ea8 --- /dev/null +++ b/Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageSwitchSheet.swift @@ -0,0 +1,81 @@ +// Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageSwitchSheet.swift +import MoraCore +import SwiftUI + +/// Sheet that lets the user re-pick the L1 from Home, without re-running +/// onboarding. Reuses `LanguagePicker`. Writes only `LearnerProfile.l1Identifier`; +/// age / level / interests / font are not touched. See spec §7.3. +@MainActor +public final class LanguageSwitchSheet: ObservableObject { + public let currentIdentifier: String + private let onCommit: (String) -> Void + private let onCancel: () -> Void + + @Published public var pickedID: String + + public init( + currentIdentifier: String, + onCommit: @escaping (String) -> Void, + onCancel: @escaping () -> Void + ) { + self.currentIdentifier = currentIdentifier + self.onCommit = onCommit + self.onCancel = onCancel + self.pickedID = currentIdentifier + } + + public var isConfirmDisabled: Bool { + pickedID == currentIdentifier + } + + public func select(identifier: String) { + pickedID = identifier + } + + public func confirm() { + guard !isConfirmDisabled else { return } + onCommit(pickedID) + } + + public func cancel() { + onCancel() + } +} + +extension LanguageSwitchSheet: Identifiable { + public var id: String { currentIdentifier } +} + +/// SwiftUI rendering of the sheet model. Hosted as a `.sheet` from `HomeView`. +public struct LanguageSwitchSheetView: View { + @ObservedObject var model: LanguageSwitchSheet + @Environment(\.moraStrings) private var strings + + public init(model: LanguageSwitchSheet) { + self.model = model + } + + public var body: some View { + NavigationStack { + LanguagePicker(selection: $model.pickedID) + .padding() + .navigationTitle(strings.languageSwitchSheetTitle) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(strings.languageSwitchSheetCancel) { + model.cancel() + } + } + ToolbarItem(placement: .confirmationAction) { + Button(strings.languageSwitchSheetConfirm) { + model.confirm() + } + .disabled(model.isConfirmDisabled) + } + } + } + } +} diff --git a/Packages/MoraUI/Tests/MoraUITests/HomeViewLanguageSwitchTests.swift b/Packages/MoraUI/Tests/MoraUITests/HomeViewLanguageSwitchTests.swift new file mode 100644 index 00000000..ec361de5 --- /dev/null +++ b/Packages/MoraUI/Tests/MoraUITests/HomeViewLanguageSwitchTests.swift @@ -0,0 +1,14 @@ +import XCTest +import MoraCore +@testable import MoraUI + +@MainActor +final class HomeViewLanguageSwitchTests: XCTestCase { + func test_globe_a11yLabel_matchesMoraStrings() { + let strings = JapaneseL1Profile().uiStrings(at: .advanced) + XCTAssertEqual(strings.homeChangeLanguageButton, "ことばを かえる") + } + + // Full SwiftUI render harness for sheet presentation is out of scope; + // the sheet's behavior is covered by LanguageSwitchSheetTests. +} diff --git a/Packages/MoraUI/Tests/MoraUITests/LanguageAgeFlowTests.swift b/Packages/MoraUI/Tests/MoraUITests/LanguageAgeFlowTests.swift index e3d1b670..d080eb2e 100644 --- a/Packages/MoraUI/Tests/MoraUITests/LanguageAgeFlowTests.swift +++ b/Packages/MoraUI/Tests/MoraUITests/LanguageAgeFlowTests.swift @@ -130,4 +130,12 @@ final class LanguageAgeFlowTests: XCTestCase { ) XCTAssertTrue(profiles.isEmpty) } + + func test_agePicker_showsThreeTiles_for6_7_8() { + XCTAssertEqual(LanguageAgeFlow.ageOptions, [6, 7, 8]) + } + + func test_agePicker_defaultSelection_is7() { + XCTAssertEqual(LanguageAgeFlow.defaultAge, 7) + } } diff --git a/Packages/MoraUI/Tests/MoraUITests/LanguageSwitchSheetTests.swift b/Packages/MoraUI/Tests/MoraUITests/LanguageSwitchSheetTests.swift new file mode 100644 index 00000000..157eb129 --- /dev/null +++ b/Packages/MoraUI/Tests/MoraUITests/LanguageSwitchSheetTests.swift @@ -0,0 +1,50 @@ +import XCTest +import MoraCore +@testable import MoraUI + +@MainActor +final class LanguageSwitchSheetTests: XCTestCase { + func test_onCommit_calledWithPickedID_whenConfirmTapped() { + var committed: String? + let sheet = LanguageSwitchSheet( + currentIdentifier: "ja", + onCommit: { committed = $0 }, + onCancel: {} + ) + sheet.select(identifier: "ko") + sheet.confirm() + XCTAssertEqual(committed, "ko") + } + + func test_onCancel_called_whenCancelTapped() { + var cancelled = false + let sheet = LanguageSwitchSheet( + currentIdentifier: "ja", + onCommit: { _ in }, + onCancel: { cancelled = true } + ) + sheet.cancel() + XCTAssertTrue(cancelled) + } + + func test_confirmDisabled_whenSelectionEqualsCurrent() { + let sheet = LanguageSwitchSheet( + currentIdentifier: "ja", + onCommit: { _ in XCTFail("should not commit") }, + onCancel: {} + ) + XCTAssertEqual(sheet.pickedID, "ja") + XCTAssertTrue(sheet.isConfirmDisabled) + } + + func test_confirmEnabled_whenSelectionDiffersFromCurrent() { + let sheet = LanguageSwitchSheet( + currentIdentifier: "ja", + onCommit: { _ in }, + onCancel: {} + ) + sheet.select(identifier: "en") + XCTAssertEqual(sheet.pickedID, "en") + XCTAssertFalse(sheet.isConfirmDisabled) + } +}