// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package preferences import ( "math" "reflect" "testing" "git.stormux.org/storm/cthulhu/internal/config" ) func TestSpeechValuesMergeRoundsNumbersAndPreservesInheritance(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) rate, pitch, gain := 61.29999999999999, 4.499999999999998, 4.999999999999999 values.Rate, values.Pitch, values.Volume = &rate, &pitch, &gain values.UppercaseVoice.AveragePitch = &pitch values.HyperlinkVoice.Rate = &rate values.SystemVoice.Gain = &gain values.EchoVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &gain} merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if *merged.DefaultVoice.Rate != 61.3 || *merged.DefaultVoice.AveragePitch != 4.5 || *merged.DefaultVoice.Gain != 5 || *merged.UppercaseVoice.AveragePitch != 4.5 || *merged.HyperlinkVoice.Rate != 61.3 || *merged.SystemVoice.Gain != 5 || *merged.EchoVoice.Rate != 61.3 || *merged.EchoVoice.AveragePitch != 4.5 || *merged.EchoVoice.Gain != 5 { t.Fatal("speech values retained floating-point noise") } if merged.UppercaseVoice.Rate != nil || merged.HyperlinkVoice.Gain != nil || merged.SystemVoice.AveragePitch != nil { t.Fatal("rounding turned inheritance into explicit settings") } if pitch != 4.499999999999998 { t.Fatal("merging mutated caller's values") } for _, invalid := range []float64{9.01, -0.01, math.NaN(), math.Inf(1)} { values.Pitch = &invalid if _, err := values.Merge(settings); err == nil { t.Fatalf("rounding hid invalid pitch %g", invalid) } } } func TestSoundValuesDefaultAndMerge(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) if !values.EnableSound || !values.PlaySoundForRole || !values.PlaySoundForState || !values.PlaySoundForPositionInSet || values.ProgressBar.OutputMode != "beep" || values.SpatializeObjectSounds { t.Fatalf("default sound checkboxes = %+v", values) } values.EnableSound = false values.EnableBraille = true values.EnableContractedBraille = true values.BrailleContractionTable = "en-us-g2.ctb" values.SoundVolume = 0.75 values.SoundSink = "pulse" values.SoundTheme = "none" values.PlaySoundForRole = false values.PlaySoundForState = false values.PlaySoundForPositionInSet = false values.SpatializeObjectSounds = true values.ProgressBar.OutputMode = "off" values.ProgressBar.ReportBackground = true values.RoleSoundPresentation = "speech_only" merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if got := FromSettings(merged); !reflect.DeepEqual(got, values) { t.Fatalf("sound values did not round trip:\n got %+v\nwant %+v", got, values) } } func TestReadingValuesRoundTrip(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) if values.Reading.OnlySpeakDisplayedText || !values.Reading.SpeakBlankLines || !values.SpeakDescription || values.Reading.ReadFullRowInSpreadSheet || !values.EnableSpeech || !values.Reading.EnablePauseBreaks || values.Reading.SpeakNumbersAsDigits || values.Reading.SpeakMultiCaseStringsAsWords || values.Reading.EnableIndentation || values.Reading.PhoneticCharacterHints { t.Fatalf("default reading values = %+v", values) } values.EnableSpeech = false values.SayAllStyle = "sentence" values.SpeechVerbosityLevel = 0 values.Reading.OnlySpeakDisplayedText = true values.Reading.EnablePauseBreaks = false values.Reading.SpeakNumbersAsDigits = true values.Reading.SpeakMultiCaseStringsAsWords = true values.Reading.EnableIndentation = true values.Reading.PhoneticCharacterHints = true values.Reading.SpeakBlankLines = false values.Reading.EnableMnemonicSpeaking = true values.Reading.EnablePositionSpeaking = true values.Reading.EnableTutorialMessages = true values.Reading.MessagesAreDetailed = false values.Reading.UseColorNames = false values.Reading.PresentTooltips = true values.Reading.EnableMouseReview = true values.Reading.SpeakMisspelledIndicator = false values.Reading.SpeakContextBlockquote = false values.Reading.SpeakContextList = false values.Reading.SpeakContextTable = false values.Reading.SpeakContextPanel = false values.Reading.SpeakContextLandmark = false values.Reading.SpeakContextNonLandmarkForm = false values.Reading.SayAllContextBlockquote = false values.Reading.SayAllContextList = false values.Reading.SayAllContextTable = false values.Reading.SayAllContextPanel = false values.Reading.SayAllContextLandmark = false values.Reading.SayAllContextNonLandmarkForm = false values.Reading.ReadFullRowInGUITable = false values.Reading.ReadFullRowInDocumentTable = false values.Reading.ReadFullRowInSpreadSheet = true values.SpeakDescription = false merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if got := FromSettings(merged); !reflect.DeepEqual(got, values) { t.Fatalf("reading values did not round trip:\n got %+v\nwant %+v", got, values) } } func TestSpeechValuesRoundTrip(t *testing.T) { settings := config.DefaultSettings() settings.SpeechHistoryLimit = 1000 family := &config.VoiceFamily{Name: "Generic Voice", Lang: "en", Dialect: "US", Variant: "male"} settings.SpeechOutputModule = "espeak-ng" settings.DefaultVoice.Family = family values := FromSettings(settings) if values.SpeechOutputModule != "espeak-ng" || values.SpeechVoiceFamily != family || values.SpeechHistoryLimit != 1000 { t.Fatalf("speech values = %+v", values) } merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if merged.SpeechOutputModule != "espeak-ng" || merged.DefaultVoice.Family != family || merged.SpeechHistoryLimit != 1000 { t.Fatalf("merged speech values = %+v", merged) } } func TestKeyboardValuesMergeAndOwnership(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) values.KeyboardLayout = 2 values.CthulhuModifierKeys = []string{"Caps_Lock", "Insert"} merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if merged.KeyboardLayout != 2 || !reflect.DeepEqual(merged.CthulhuModifierKeys, values.CthulhuModifierKeys) { t.Fatalf("keyboard settings were not merged: %+v", merged) } values.CthulhuModifierKeys[0] = "KP_Insert" if merged.CthulhuModifierKeys[0] != "Caps_Lock" { t.Fatal("merged modifiers alias the UI values") } values.CthulhuModifierKeys = []string{} merged, err = values.Merge(settings) if err != nil || len(merged.CthulhuModifierKeys) != 0 { t.Fatalf("cannot disable all modifiers: %v", err) } } func TestCapitalCheckboxesRoundTrip(t *testing.T) { for mask := 0; mask < 8; mask++ { settings := config.DefaultSettings() values := FromSettings(settings) values.RaisePitchForCapitals = mask&1 != 0 values.BeepForCapitals = mask&2 != 0 values.SayCapForCapitals = mask&4 != 0 merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if got := FromSettings(merged); !reflect.DeepEqual(got, values) { t.Fatalf("mask %d did not round trip", mask) } } } func TestStructuralFocusValuesRoundTrip(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) if values.StructuralNavigationTriggersFocusMode { t.Fatal("automatic structural focus defaults on") } values.StructuralNavigationTriggersFocusMode = true merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if !FromSettings(merged).StructuralNavigationTriggersFocusMode { t.Fatal("preference lost during Apply") } }