package tui import ( "os" "path/filepath" "strings" "testing" "time" tea "charm.land/bubbletea/v2" ) // writeJobHistory writes a job-history.json into the sandboxed mpv config dir // and returns its path. func writeJobHistory(t *testing.T, content string) string { t.Helper() path, err := jobHistoryFilePath() if err != nil { t.Fatalf("failed to resolve job history path: %v", err) } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("failed to create mpv config dir: %v", err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("failed to write job history: %v", err) } return path } func TestLoadJobHistoryRecordsMissingFile(t *testing.T) { records, err := loadJobHistoryRecords(filepath.Join(t.TempDir(), "job-history.json")) if err != nil { t.Fatalf("expected nil error for missing file, got %v", err) } if len(records) != 0 { t.Errorf("expected empty history, got %d records", len(records)) } } func TestLoadJobHistoryRecordsNewestFirst(t *testing.T) { path := filepath.Join(t.TempDir(), "job-history.json") content := `[ {"id":"1","type":"install","appName":"mpv","status":"complete","startedAt":"2026-08-01T10:00:00Z"}, {"id":"2","type":"update","appName":"mpv","status":"error","error":"hash mismatch","startedAt":"2026-08-02T10:00:00Z"} ]` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } records, err := loadJobHistoryRecords(path) if err != nil { t.Fatalf("failed to load records: %v", err) } if len(records) != 2 { t.Fatalf("expected 2 records, got %d", len(records)) } if records[0].ID != "2" || records[1].ID != "1" { t.Errorf("expected newest first, got order %s, %s", records[0].ID, records[1].ID) } } func TestLoadJobHistoryRecordsCorrupt(t *testing.T) { path := filepath.Join(t.TempDir(), "job-history.json") if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil { t.Fatal(err) } if _, err := loadJobHistoryRecords(path); err == nil { t.Error("expected error for corrupt file") } } func TestJobHistoryItemDisplay(t *testing.T) { started := time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC) record := &jobHistoryRecord{ Type: "update", AppName: "mpv", Status: "error", Error: "hash mismatch", StartedAt: started, } item := jobHistoryItem{record: record} if title := item.Title(); !strings.Contains(title, "✗") || !strings.Contains(title, "Update mpv") { t.Errorf("unexpected title: %q", title) } if desc := item.Description(); !strings.Contains(desc, "hash mismatch") { t.Errorf("expected error in description, got %q", desc) } record.Status = "complete" record.Error = "" record.Message = "Done" item = jobHistoryItem{record: record} if title := item.Title(); !strings.Contains(title, "✓") { t.Errorf("expected success icon, got %q", title) } if desc := item.Description(); !strings.Contains(desc, "Done") { t.Errorf("expected message in description, got %q", desc) } } // newJobHistoryModel sandboxes the user config and returns a model in the job // history state with one persisted record. func newJobHistoryModel(t *testing.T) *Model { t.Helper() sandboxConfigHome(t) writeJobHistory(t, `[ {"id":"1","type":"install","methodId":"mpv-flatpak","appName":"MPV","status":"complete", "message":"Installation complete","output":["line one","line two"], "startedAt":"2026-08-01T10:00:00Z","completedAt":"2026-08-01T10:05:00Z"} ]`) m := NewModel() m.setupJobHistoryList() m.state = StateJobHistory return m } func TestJobHistoryListLoadsRecords(t *testing.T) { m := newJobHistoryModel(t) if m.jobHistoryLoadErr != "" { t.Fatalf("unexpected load error: %s", m.jobHistoryLoadErr) } items := m.jobHistoryList.Items() if len(items) != 1 { t.Fatalf("expected 1 history item, got %d", len(items)) } if title := items[0].(jobHistoryItem).Title(); !strings.Contains(title, "Install MPV") { t.Errorf("unexpected item title: %q", title) } } func TestJobHistoryEnterShowsDetailAndEscReturns(t *testing.T) { m := newJobHistoryModel(t) m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if m.state != StateJobHistoryDetail { t.Fatalf("expected StateJobHistoryDetail, got %v", m.state) } if m.selectedJobRecord == nil { t.Fatal("expected selected job record") } content := m.jobDetailContent(m.selectedJobRecord) for _, want := range []string{"Install MPV", "complete", "mpv-flatpak", "line one", "line two"} { if !strings.Contains(content, want) { t.Errorf("expected detail content to contain %q", want) } } m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEsc}) if m.state != StateJobHistory { t.Fatalf("expected StateJobHistory after Esc, got %v", m.state) } if m.selectedJobRecord != nil { t.Error("expected selected job record cleared") } m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEsc}) if m.state != StateMainMenu { t.Fatalf("expected StateMainMenu after Esc, got %v", m.state) } } func TestJobHistoryEmptyAndCorrupt(t *testing.T) { sandboxConfigHome(t) // No history file: empty list, no error. m := NewModel() m.setupJobHistoryList() if m.jobHistoryLoadErr != "" { t.Errorf("expected no load error for missing file, got %q", m.jobHistoryLoadErr) } if len(m.jobHistoryList.Items()) != 0 { t.Errorf("expected empty list, got %d items", len(m.jobHistoryList.Items())) } // Corrupt file surfaces a load error. writeJobHistory(t, "{corrupt") m.setupJobHistoryList() if m.jobHistoryLoadErr == "" { t.Error("expected load error for corrupt file") } } func TestMainMenuHasHistoryAndLogsEntries(t *testing.T) { sandboxConfigHome(t) m := NewModel() m.RefreshMainMenu() ids := map[string]bool{} for _, item := range m.menuList.Items() { ids[item.(menuItem).id] = true } if !ids["job-history"] { t.Error("expected job-history entry in main menu") } if !ids["view-logs"] { t.Error("expected view-logs entry in main menu") } }