Files
fenrir/internal/app/menu_save_test.go

169 lines
6.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"reflect"
"testing"
"time"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/input"
"git.stormux.org/storm/fenrir/internal/interaction"
)
func admit_menu_save(t *testing.T, a *application) {
t.Helper()
a.open_settings()
receive_ui(t, a.ui)
a.ui.handle(interaction.Key{Name: "tab"})
a.ui.handle(interaction.Key{Name: "tab"})
a.ui.handle(interaction.Key{Name: "right"}) // Rate 24 -> 25.
a.ui.action("save", a.ui.session)
if !a.saver.busy || a.ui.session != nil {
t.Fatal("menu save was not admitted")
}
}
func committed_menu_result(t *testing.T, a *application) error {
t.Helper()
select {
case err := <-a.saver.results:
return err
case <-time.After(time.Second):
t.Fatal("menu save worker stalled")
return nil
}
}
func TestMenuCommitSurvivesCloseAndHandoff(t *testing.T) {
for _, terminalMode := range []bool{false, true} {
for _, closeAction := range []string{"escape", "terminal", "focus", "other menu"} {
t.Run(closeAction+map[bool]string{false: "/tty", true: "/x11"}[terminalMode], func(t *testing.T) {
a, voice := persistence_application(t, terminalMode)
admit_menu_save(t, a)
err := committed_menu_result(t, a) // Gate completion after disk commit.
if err != nil {
t.Fatal(err)
}
switch closeAction {
case "terminal", "focus":
source := a.source.(*fakeSource)
if closeAction == "terminal" {
source.snapshot.Terminal++
} else {
source.snapshot.Active = false
}
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
default:
a.ui.handle(interaction.Key{Name: "escape"})
}
if a.ui.active || a.keyboards.(*fakeKeys).captures[1] {
t.Fatal("close did not release capture immediately")
}
if closeAction == "other menu" {
a.ui.begin("Other menu")
}
generation, speechCount := a.ui.generation, len(voice.messages)
a.settings_save_result(err)
saved, _, err := config.Load(a.options.ConfigPath, nil)
if err != nil || saved.Presentation.Speech.Rate != 25 || a.settings.live.Presentation.Speech.Rate != 25 || !reflect.DeepEqual(a.settings.saveDraft, saved) || !reflect.DeepEqual(a.settings.resetBase, saved) {
t.Fatal("committed menu settings diverged", saved, a.settings, err)
}
if a.ui.generation != generation || len(voice.messages) != speechCount || a.ui.active != (closeAction == "other menu") {
t.Fatal("stale save changed modal presentation")
}
configured := len(voice.parameters)
a.settings_save_result(nil)
if len(voice.parameters) != configured {
t.Fatal("duplicate result applied twice")
}
if closeAction == "other menu" {
a.ui.close()
}
a.open_settings()
receive_ui(t, a.ui)
if a.ui.session.Values()["presentation.speech.rate"].Number != 25 {
t.Fatal("reopened form lost the commit")
}
})
}
}
}
func TestMenuCommitPreservesNewerOverridesAndCoordinatesSaves(t *testing.T) {
for _, terminalMode := range []bool{false, true} {
a, _ := persistence_application(t, terminalMode)
admit_menu_save(t, a)
err := committed_menu_result(t, a)
if err != nil {
t.Fatal(err)
}
a.ui.close()
a.open_settings()
if a.ui.active {
t.Fatal("settings menu reopened during an admitted save")
}
persistence_reply(t, persistence_request(a, "SAVE"), true)
persistence_reply(t, persistence_request(a, "SAVEAS ignored.toml"), true)
persistence_reply(t, persistence_request(a, "RESET"), true)
a.save_settings() // Cannot replace the pending menu snapshot.
persistence_reply(t, persistence_request(a, "SET speech#pitch=0.8;keyboard#word_echo=True"), false)
for _, id := range []command.ID{command.NextQuickValue, command.PreviousQuickValue, command.ProgressMonitor, command.Mute, command.AutoRead} {
a.handle_input(input.Decision{Command: id})
}
// Returning rate to its old value still counts as a later adjustment.
a.settings_save_result(err)
if a.settings.live.Presentation.Speech.Rate != 24 || a.settings.live.Presentation.Speech.Pitch != 60 || !a.settings.live.Keyboard.WordEcho || a.settings.live.Presentation.Progress.Enabled == a.settings.resetBase.Presentation.Progress.Enabled || !a.muted || a.engine.Automatic_reading() || !a.quick.changed {
t.Fatal("menu completion overwrote a later/session change", a.settings.live)
}
if a.settings.saveDraft.Presentation.Speech.Rate != 24 || a.settings.saveDraft.Presentation.Speech.Pitch != 60 || !a.settings.saveDraft.Keyboard.WordEcho || a.settings.resetBase.Presentation.Speech.Rate != 25 {
t.Fatal("save draft or reset baseline diverged", a.settings)
}
persistence_reply(t, persistence_request(a, "RESET"), false)
if a.settings.live.Presentation.Speech.Rate != 25 || a.settings.live.Presentation.Speech.Pitch != a.settings.resetBase.Presentation.Speech.Pitch || a.settings.live.Keyboard.WordEcho || !a.muted || a.engine.Automatic_reading() {
t.Fatal("RESET did not restore committed values and preserve session toggles")
}
}
}
func TestMenuFailureAfterCloseIsSilentAndDoesNotCommit(t *testing.T) {
a, voice := persistence_application(t, true)
base := a.settings.resetBase
a.open_settings()
receive_ui(t, a.ui)
external := base
external.Presentation.Speech.Rate = 70
if err := config.Save(a.options.ConfigPath, external); err != nil {
t.Fatal(err)
}
a.ui.action("save", a.ui.session)
err := committed_menu_result(t, a)
if err == nil {
t.Fatal("external edit was overwritten")
}
a.ui.close()
count := len(voice.messages)
a.settings_save_result(err)
if a.ui.active || a.saver.busy || len(voice.messages) != count || !reflect.DeepEqual(a.settings.resetBase, base) || !reflect.DeepEqual(a.settings.saveDraft, base) {
t.Fatal("closed failure changed state or presented into another session")
}
}
func TestQuitWaitsForMenuCommitAfterClose(t *testing.T) {
a, _ := persistence_application(t, true)
admit_menu_save(t, a)
a.ui.close()
a.handle_input(input.Decision{Command: command.Quit})
if a.quitting || !a.quitAfterSave {
t.Fatal("quit did not wait for menu save")
}
if err := a.loop(nil); err != nil || a.ctx.Err() != nil || a.settings.resetBase.Presentation.Speech.Rate != 25 || a.saver.busy {
t.Fatal("quit lost admitted commit", err, a.ctx.Err())
}
}