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.
109 lines
4.4 KiB
Go
109 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|