Add global Wine preferences tab
Phase 3 of the Wine/Proton stage (decisions 6, 9, 13, 19 in docs/testing/wine-proton.md): a global-only Wine tab modeled on the Pronunciation page, with the controller-only checkbox and a typed editable program list. Wine values flow through the preferences protocol so a global Apply reaches the live Wine settings from Phase 2 with no reader restart. Entries are validated as edited and named in-tab, blocking Apply until fixed; case-insensitive duplicates collapse. Per-application preferences expose no Wine editing and an app-scoped Apply cannot change the governing list.
This commit is contained in:
1 parent
dc6a961032
commit
75f34ea767
15 files changed
+543
-5
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/preferences"
|
||||
"git.stormux.org/storm/cthulhu/internal/speech"
|
||||
"git.stormux.org/storm/cthulhu/internal/wine"
|
||||
)
|
||||
|
||||
// The Wine tab is global-only: a global preferences Apply carries the edited
|
||||
// Wine values through the preferences protocol into the live global Wine
|
||||
// source the manager reads, persists them through TOML, and needs no reader
|
||||
// restart. An application-scoped Apply never changes the governing values.
|
||||
func TestWinePreferencesGlobalApplyGovernsAndPersists(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "settings.toml")
|
||||
global := config.DefaultSettings()
|
||||
global.Path = path
|
||||
global.Wine = config.WineSettings{ExcludedPrograms: []string{"Game.exe"}}
|
||||
store, err := config.NewStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Save(global); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), global,
|
||||
settingsUpdateFunc(func(context.Context, speech.Settings) error { return nil }))
|
||||
worker.live = &liveSettings{}
|
||||
worker.live.Store(global)
|
||||
worker.live.StoreGlobalWine(global.Wine)
|
||||
manager := newWineManager(slog.New(slog.DiscardHandler), worker.live)
|
||||
governing := func() wine.Settings { return manager.SettingsSource() }
|
||||
ctx := context.Background()
|
||||
|
||||
// An invalid entry is named and blocks the global Apply.
|
||||
if !worker.beginEdit(ctx, "") {
|
||||
t.Fatal("global preferences did not open")
|
||||
}
|
||||
invalid := preferences.FromSettings(worker.editing)
|
||||
invalid.Wine.ExcludedPrograms = []string{"Game.exe", `C:\Games\Sneaky.exe`}
|
||||
response := worker.apply(ctx, preferences.Request{Values: invalid, Save: true})
|
||||
if response.Accepted || !strings.Contains(response.Message, `C:\Games\Sneaky.exe`) {
|
||||
t.Fatalf("invalid Wine entry was not named and blocked: %+v", response)
|
||||
}
|
||||
if want := (wine.Settings{ExcludedPrograms: []string{"Game.exe"}}); !reflect.DeepEqual(governing(), want) {
|
||||
t.Fatalf("rejected Apply changed governing Wine settings: %+v", governing())
|
||||
}
|
||||
|
||||
// A valid global Apply reaches the same manager with no restart and
|
||||
// persists through TOML.
|
||||
values := preferences.FromSettings(worker.editing)
|
||||
values.Wine = config.WineSettings{ControllerOnly: true,
|
||||
ExcludedPrograms: []string{"Game.exe", "New Game.exe", "new game.EXE"}}
|
||||
response = worker.apply(ctx, preferences.Request{Values: values, Save: true})
|
||||
if !response.Accepted {
|
||||
t.Fatalf("global Wine Apply rejected: %+v", response)
|
||||
}
|
||||
want := wine.Settings{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe", "New Game.exe"}}
|
||||
if !reflect.DeepEqual(governing(), want) {
|
||||
t.Fatalf("governing Wine settings after Apply = %+v, want %+v", governing(), want)
|
||||
}
|
||||
loaded, err := store.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(loaded.Wine, config.WineSettings(want)) {
|
||||
t.Fatalf("persisted Wine settings = %+v, want %+v", loaded.Wine, want)
|
||||
}
|
||||
worker.editingActive = false
|
||||
|
||||
// An application-scoped Apply has no Wine editing surface. Even a
|
||||
// request carrying different Wine values cannot change the governing
|
||||
// list, the global profile, or the application profile's Wine values.
|
||||
worker.switchApplication(ctx, "Example Editor")
|
||||
if !worker.beginEdit(ctx, "\x00") || worker.editApp != "Example Editor" {
|
||||
t.Fatal("application preferences did not open")
|
||||
}
|
||||
appValues := preferences.FromSettings(worker.editing)
|
||||
appValues.TimeFormat = "24-hour"
|
||||
appValues.Wine = config.WineSettings{ExcludedPrograms: []string{"Sneaky.exe"}}
|
||||
response = worker.apply(ctx, preferences.Request{Values: appValues, Save: true})
|
||||
if !response.Accepted || worker.settings.TimeFormat != "24-hour" {
|
||||
t.Fatalf("application Apply rejected: %+v", response)
|
||||
}
|
||||
if !reflect.DeepEqual(governing(), want) {
|
||||
t.Fatalf("application Apply changed governing Wine settings: %+v", governing())
|
||||
}
|
||||
if !reflect.DeepEqual(worker.globalSettings.Wine, config.WineSettings(want)) {
|
||||
t.Fatalf("application Apply changed global Wine settings: %+v", worker.globalSettings.Wine)
|
||||
}
|
||||
if !reflect.DeepEqual(worker.appSettings["Example Editor"].Wine, config.WineSettings(want)) {
|
||||
t.Fatalf("application Apply rewrote Wine values: %+v", worker.appSettings["Example Editor"].Wine)
|
||||
}
|
||||
reloaded, err := store.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(reloaded.Wine, config.WineSettings(want)) {
|
||||
t.Fatalf("application Apply persisted into the global Wine profile: %+v", reloaded.Wine)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user