package app import ( "os" "path/filepath" "strings" "testing" "time" "rickub.com/turbo-editors/turbo-core/settings" ) // fakeClock is a clock the test moves by hand, so an autosave deadline is // reached without anything waiting for it. type fakeClock struct{ at time.Time } func (c *fakeClock) now() time.Time { return c.at } func (c *fakeClock) pass(d time.Duration) { c.at = c.at.Add(d) } // newAutosaveApp returns an editor with autosave on, a clock the test drives, // and a file open in the front window. func newAutosaveApp(t *testing.T, delay time.Duration) (*App, *fakeClock, string) { t.Helper() a, _ := newTestApp(t) clock := &fakeClock{at: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)} a.now = clock.now path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") a.Open(path) a.SetAutosave(true, delay) return a, clock, path } // onDisk returns what a file holds right now. func onDisk(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } return string(data) } func TestAutosaveIsOffUnlessAProjectAsksForIt(t *testing.T) { a, _ := newTestApp(t) if a.AutosaveEnabled() { t.Error("autosave is on in a project that never asked for it") } } func TestAutosaveWritesTheFileOnceTypingStops(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") clock.pass(2 * time.Second) a.saveDueDocuments() if got := onDisk(t, path); !strings.Contains(got, "// hello") { t.Errorf("the file on disk is %q; autosave did not write it", got) } if activeBuffer(t, a).Modified() { t.Error("the buffer is still marked modified after being saved") } } func TestAutosaveWaitsWhileTypingContinues(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) // Each keystroke pushes the deadline out again, so a steady typist is // never interrupted by a write. for range 5 { typeText(a, "x") clock.pass(1900 * time.Millisecond) a.saveDueDocuments() } if got := onDisk(t, path); got != "package main\n" { t.Errorf("the file was written mid-typing: %q", got) } } func TestAutosaveDoesNothingBeforeTheDelayHasPassed(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") clock.pass(1999 * time.Millisecond) a.saveDueDocuments() if got := onDisk(t, path); got != "package main\n" { t.Errorf("the file was written a millisecond early: %q", got) } } func TestAutosaveDoesNothingWhenItIsOff(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) a.SetAutosave(false, 0) typeText(a, "// hello") clock.pass(time.Hour) a.saveDueDocuments() if got := onDisk(t, path); got != "package main\n" { t.Errorf("a file was written with autosave off: %q", got) } } func TestTurningAutosaveOffForgetsThePendingSave(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") a.SetAutosave(false, 0) // the deadline was already set when this ran clock.pass(time.Hour) a.saveDueDocuments() if got := onDisk(t, path); got != "package main\n" { t.Errorf("a save pending when autosave was turned off still happened: %q", got) } } func TestAutosaveLeavesAFileThatHasNoNameAlone(t *testing.T) { a, clock, _ := newAutosaveApp(t, 2*time.Second) a.NewFile() // untitled: naming it is a question, and autosave asks nothing typeText(a, "package main") clock.pass(2 * time.Second) a.saveDueDocuments() if a.Modals() != 0 { t.Errorf("autosave opened %d dialogs; it must never ask anything", a.Modals()) } if !activeBuffer(t, a).Modified() { t.Error("an unnamed buffer was somehow saved") } } func TestAutosaveSavesEveryOpenFile(t *testing.T) { a, clock, first := newAutosaveApp(t, 2*time.Second) second := filepath.Join(t.TempDir(), "other.go") writeTestFile(t, second, "package other\n") a.Open(second) typeText(a, "// two") a.NextWindow() typeText(a, "// one") clock.pass(2 * time.Second) a.saveDueDocuments() if got := onDisk(t, first); !strings.Contains(got, "// one") { t.Errorf("the first file was not saved: %q", got) } if got := onDisk(t, second); !strings.Contains(got, "// two") { t.Errorf("the second file was not saved: %q", got) } } func TestAFailedAutosaveIsReportedWithoutADialog(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") // A directory cannot be written to as a file, which is the simplest way to // make the write fail without depending on who is running the test. if err := os.Remove(path); err != nil { t.Fatalf("removing the file: %v", err) } if err := os.Mkdir(path, 0o755); err != nil { t.Fatalf("putting a directory in its place: %v", err) } clock.pass(2 * time.Second) a.saveDueDocuments() if a.Modals() != 0 { t.Errorf("a failed autosave opened %d dialogs", a.Modals()) } } func TestAFailedAutosaveIsNotRetriedUntilTheNextEdit(t *testing.T) { a, clock, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") if err := os.Remove(path); err != nil { t.Fatalf("removing the file: %v", err) } if err := os.Mkdir(path, 0o755); err != nil { t.Fatalf("putting a directory in its place: %v", err) } clock.pass(2 * time.Second) a.saveDueDocuments() // An attempt that failed must leave nothing pending. A deadline still set // would come due again on the very next turn of the event loop, and go on // doing so for as long as the editor is open. if !a.autosave.due.IsZero() { t.Error("a failed autosave left its deadline set, so it will retry forever") } // And a further turn of the loop really does nothing, rather than merely // not being due yet. clock.pass(time.Hour) a.saveDueDocuments() if a.Modals() != 0 { t.Error("a later turn of the event loop retried the failed save") } } func TestClosingAWindowWithAutosaveOnAsksNothing(t *testing.T) { a, _, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") a.CloseFile() if a.Modals() != 0 { t.Errorf("closing asked about work autosave was going to write (%d dialogs)", a.Modals()) } if a.Desktop().Count() != 0 { t.Error("the window did not close") } if got := onDisk(t, path); !strings.Contains(got, "// hello") { t.Errorf("closing discarded the changes instead of saving them: %q", got) } } func TestClosingAnUnnamedWindowStillAsksWithAutosaveOn(t *testing.T) { a, _, _ := newAutosaveApp(t, 2*time.Second) a.NewFile() typeText(a, "package main") a.CloseFile() if a.Modals() != 1 { t.Errorf("Modals() = %d; work autosave cannot write is still a real question", a.Modals()) } } func TestLeavingWithAutosaveOnSavesInsteadOfAsking(t *testing.T) { a, _, path := newAutosaveApp(t, 2*time.Second) typeText(a, "// hello") a.Quit() if a.Modals() != 0 { t.Errorf("Quit() asked about work autosave was going to write (%d dialogs)", a.Modals()) } if !a.Quitting() { t.Error("Quit() did not leave") } if got := onDisk(t, path); !strings.Contains(got, "// hello") { t.Errorf("leaving discarded the changes: %q", got) } } func TestLeavingStillAsksAboutAnUnnamedFile(t *testing.T) { a, _, _ := newAutosaveApp(t, 2*time.Second) a.NewFile() typeText(a, "package main") a.Quit() if a.Quitting() { t.Error("Quit() left without asking about a file it could not save") } if a.Modals() != 1 { t.Errorf("Modals() = %d, want the question", a.Modals()) } } func TestSetAutosaveKeepsTheDelayWhenGivenNone(t *testing.T) { a, _ := newTestApp(t) a.SetAutosave(true, 5*time.Second) a.SetAutosave(true, 0) // "on", with nothing said about how long if a.autosave.delay != 5*time.Second { t.Errorf("delay = %v, want the 5s already set rather than zero", a.autosave.delay) } } func TestANewEditorHasTheDefaultAutosaveDelay(t *testing.T) { a, _ := newTestApp(t) // Zero would mean "save on every keystroke" the moment a project turned // autosave on without naming a delay. if a.autosave.delay <= 0 { t.Errorf("the default delay is %v", a.autosave.delay) } } func TestUseSettingsAppliesAutosaveAndRemembersTheFile(t *testing.T) { a, _ := newTestApp(t) a.UseSettings(settings.Settings{Autosave: true, AutosaveDelay: 5 * time.Second}, "/p/.turbo-go/settings.toml") if !a.AutosaveEnabled() { t.Error("UseSettings() did not turn autosave on") } if a.autosave.delay != 5*time.Second { t.Errorf("delay = %v, want 5s", a.autosave.delay) } if a.SettingsPath() != "/p/.turbo-go/settings.toml" { t.Errorf("SettingsPath() = %q", a.SettingsPath()) } } func TestUseSettingsDoesNotApplyTheTheme(t *testing.T) { // main resolves the theme, because only it knows whether -theme was given // and an explicit flag outranks the project's choice. a, _ := newTestApp(t) before := a.ThemeName() a.UseSettings(settings.Settings{Theme: "turbo-dark"}, "/p/.turbo-go/settings.toml") if a.ThemeName() != before { t.Errorf("ThemeName() = %q; UseSettings must leave the theme to the caller", a.ThemeName()) } }