584 lines
18 KiB
Go
584 lines
18 KiB
Go
package screens
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/santifer/career-ops/dashboard/internal/model"
|
|
"github.com/santifer/career-ops/dashboard/internal/theme"
|
|
)
|
|
|
|
func tabIndexForFilter(t *testing.T, filter string) int {
|
|
t.Helper()
|
|
|
|
for i, tab := range pipelineTabs {
|
|
if tab.filter == filter {
|
|
return i
|
|
}
|
|
}
|
|
|
|
t.Fatalf("expected pipeline tabs to include filter %q", filter)
|
|
return -1
|
|
}
|
|
|
|
func TestWithReloadedDataPreservesStateAndSelection(t *testing.T) {
|
|
initialApps := []model.CareerApplication{
|
|
{
|
|
Company: "Acme",
|
|
Role: "Backend Engineer",
|
|
Status: "Evaluated",
|
|
Score: 4.2,
|
|
ReportPath: "reports/001-acme.md",
|
|
},
|
|
{
|
|
Company: "Beta",
|
|
Role: "Platform Engineer",
|
|
Status: "Applied",
|
|
Score: 4.6,
|
|
ReportPath: "reports/002-beta.md",
|
|
},
|
|
}
|
|
|
|
pm := NewPipelineModel(
|
|
theme.NewTheme("catppuccin-mocha"),
|
|
initialApps,
|
|
model.PipelineMetrics{Total: len(initialApps)},
|
|
"..",
|
|
120,
|
|
40,
|
|
)
|
|
pm.sortMode = sortCompany
|
|
pm.activeTab = 0
|
|
pm.viewMode = "flat"
|
|
pm.applyFilterAndSort()
|
|
pm.cursor = 1
|
|
pm.reportCache["reports/002-beta.md"] = reportSummary{tldr: "cached"}
|
|
|
|
refreshedApps := []model.CareerApplication{
|
|
initialApps[0],
|
|
initialApps[1],
|
|
{
|
|
Company: "Gamma",
|
|
Role: "AI Engineer",
|
|
Status: "Interview",
|
|
Score: 4.8,
|
|
ReportPath: "reports/003-gamma.md",
|
|
},
|
|
}
|
|
|
|
reloaded := pm.WithReloadedData(refreshedApps, model.PipelineMetrics{Total: len(refreshedApps)})
|
|
|
|
if reloaded.sortMode != sortCompany {
|
|
t.Fatalf("expected sort mode %q, got %q", sortCompany, reloaded.sortMode)
|
|
}
|
|
if reloaded.viewMode != "flat" {
|
|
t.Fatalf("expected view mode to stay flat, got %q", reloaded.viewMode)
|
|
}
|
|
if got := len(reloaded.filtered); got != 3 {
|
|
t.Fatalf("expected 3 filtered apps after refresh, got %d", got)
|
|
}
|
|
if app, ok := reloaded.CurrentApp(); !ok || app.ReportPath != "reports/002-beta.md" {
|
|
t.Fatalf("expected selection to stay on beta app, got %+v (ok=%v)", app, ok)
|
|
}
|
|
if reloaded.reportCache["reports/002-beta.md"].tldr != "cached" {
|
|
t.Fatal("expected cached report summaries to survive refresh")
|
|
}
|
|
}
|
|
|
|
func TestRenderAppLineIncludesDateColumn(t *testing.T) {
|
|
pm := NewPipelineModel(
|
|
theme.NewTheme("catppuccin-mocha"),
|
|
nil,
|
|
model.PipelineMetrics{},
|
|
"..",
|
|
120,
|
|
40,
|
|
)
|
|
|
|
line := pm.renderAppLine(model.CareerApplication{
|
|
Number: 42,
|
|
Date: "2026-04-13",
|
|
Company: "Anthropic",
|
|
Role: "Forward Deployed Engineer",
|
|
Status: "Applied",
|
|
Score: 4.5,
|
|
}, false)
|
|
|
|
if !strings.Contains(line, "2026-04-13") {
|
|
t.Fatalf("expected rendered line to include date column, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "#42") {
|
|
t.Fatalf("expected rendered line to include tracker number marker, got %q", line)
|
|
}
|
|
}
|
|
|
|
func TestSearchFiltersByCompanyRoleAndNotes(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6, Notes: "payments infra"},
|
|
{Company: "Anthropic", Role: "AI Safety Engineer", Status: "Applied", Score: 4.8, Notes: "policy work"},
|
|
{Company: "Acme Corp", Role: "Senior PM, Voice AI", Status: "Evaluated", Score: 4.2, Notes: "Series B in Madrid"},
|
|
{Company: "Globex", Role: "Platform Engineer", Status: "Applied", Score: 3.9, Notes: "remote-first"},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
pm.activeTab = tabIndexForFilter(t, filterAll)
|
|
|
|
// Match by company substring (case-insensitive).
|
|
pm.searchQuery = "stripe"
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Company != "Stripe" {
|
|
t.Fatalf("expected 1 match for 'stripe', got %+v", pm.filtered)
|
|
}
|
|
|
|
// Match by role substring.
|
|
pm.searchQuery = "voice ai"
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Company != "Acme Corp" {
|
|
t.Fatalf("expected 1 match for 'voice ai', got %+v", pm.filtered)
|
|
}
|
|
|
|
// Match by notes substring.
|
|
pm.searchQuery = "madrid"
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Company != "Acme Corp" {
|
|
t.Fatalf("expected 1 match for notes 'madrid', got %+v", pm.filtered)
|
|
}
|
|
|
|
// Empty query restores everything.
|
|
pm.searchQuery = ""
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != len(apps) {
|
|
t.Fatalf("expected empty query to restore all rows, got %d/%d", len(pm.filtered), len(apps))
|
|
}
|
|
}
|
|
|
|
func TestSearchComposesWithActiveTab(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6},
|
|
{Company: "Stripe", Role: "Frontend Engineer", Status: "Applied", Score: 4.5},
|
|
{Company: "Anthropic", Role: "AI Engineer", Status: "Applied", Score: 4.8},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
pm.activeTab = tabIndexForFilter(t, filterApplied)
|
|
pm.searchQuery = "stripe"
|
|
pm.applyFilterAndSort()
|
|
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Role != "Frontend Engineer" {
|
|
t.Fatalf("expected applied+stripe to leave only Frontend Engineer, got %+v", pm.filtered)
|
|
}
|
|
}
|
|
|
|
func TestSearchIsCaseInsensitive(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Anthropic", Role: "AI Engineer", Status: "Evaluated", Score: 4.8},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
for _, q := range []string{"anthropic", "ANTHROPIC", "AnThRoPiC"} {
|
|
pm.searchQuery = q
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 {
|
|
t.Fatalf("expected case-insensitive match for %q, got %d rows", q, len(pm.filtered))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSearchEnterCommitsAndEscClearsCommittedQuery(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6},
|
|
{Company: "Anthropic", Role: "AI Engineer", Status: "Evaluated", Score: 4.8},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
|
|
// Open input and type "stripe".
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
if !pm.searchInput {
|
|
t.Fatal("expected `/` to open search input")
|
|
}
|
|
for _, r := range "stripe" {
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
|
|
}
|
|
if pm.searchQuery != "stripe" {
|
|
t.Fatalf("expected query to live-update to 'stripe', got %q", pm.searchQuery)
|
|
}
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Company != "Stripe" {
|
|
t.Fatalf("expected live filter to leave only Stripe, got %+v", pm.filtered)
|
|
}
|
|
|
|
// Enter commits — input closes, query stays.
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
if pm.searchInput {
|
|
t.Fatal("expected Enter to close input")
|
|
}
|
|
if pm.searchQuery != "stripe" {
|
|
t.Fatalf("expected Enter to keep committed query, got %q", pm.searchQuery)
|
|
}
|
|
|
|
// Esc on a committed query clears the search and restores the full list.
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
if pm.searchQuery != "" {
|
|
t.Fatalf("expected Esc to clear committed query, got %q", pm.searchQuery)
|
|
}
|
|
if len(pm.filtered) != len(apps) {
|
|
t.Fatalf("expected Esc to restore full list, got %d/%d", len(pm.filtered), len(apps))
|
|
}
|
|
}
|
|
|
|
func TestSearchEscInInputCancelsAndClears(t *testing.T) {
|
|
// Use multiple rows so the test catches a regression where Esc clears the query
|
|
// but forgets to re-apply the filter — the visible count would stay at 1
|
|
// otherwise even though the underlying state went stale.
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6},
|
|
{Company: "Globex", Role: "Platform Engineer", Status: "Evaluated", Score: 4.0},
|
|
{Company: "Anthropic", Role: "AI Engineer", Status: "Evaluated", Score: 4.8},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
pm.searchInput = true
|
|
pm.searchQuery = "stri"
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 {
|
|
t.Fatalf("setup expected 1 row matching 'stri', got %d", len(pm.filtered))
|
|
}
|
|
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
if pm.searchInput {
|
|
t.Fatal("expected Esc in input mode to close input")
|
|
}
|
|
if pm.searchQuery != "" {
|
|
t.Fatalf("expected Esc in input mode to clear in-progress query, got %q", pm.searchQuery)
|
|
}
|
|
if len(pm.filtered) != len(apps) {
|
|
t.Fatalf("expected Esc to re-expand filtered list to %d rows, got %d", len(apps), len(pm.filtered))
|
|
}
|
|
}
|
|
|
|
func TestSearchResetsCursorOnQueryChange(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Acme", Role: "Backend Engineer", Status: "Evaluated", Score: 4.0},
|
|
{Company: "Beta", Role: "Frontend Engineer", Status: "Evaluated", Score: 4.1},
|
|
{Company: "Gamma", Role: "AI Engineer", Status: "Evaluated", Score: 4.2},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
pm.cursor = 2
|
|
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}})
|
|
|
|
if pm.cursor != 0 {
|
|
t.Fatalf("expected cursor to reset to 0 on query change, got %d", pm.cursor)
|
|
}
|
|
if pm.scrollOffset != 0 {
|
|
t.Fatalf("expected scrollOffset to reset to 0 on query change, got %d", pm.scrollOffset)
|
|
}
|
|
}
|
|
|
|
func TestSearchStatePreservedAcrossReload(t *testing.T) {
|
|
initial := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend", Status: "Evaluated", Score: 4.6},
|
|
{Company: "Acme", Role: "AI", Status: "Evaluated", Score: 4.0},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), initial, model.PipelineMetrics{Total: len(initial)}, "..", 120, 40)
|
|
pm.searchQuery = "stripe"
|
|
pm.applyFilterAndSort()
|
|
|
|
refreshed := append([]model.CareerApplication{}, initial...)
|
|
refreshed = append(refreshed, model.CareerApplication{Company: "Globex", Role: "Platform", Status: "Applied", Score: 4.3})
|
|
|
|
reloaded := pm.WithReloadedData(refreshed, model.PipelineMetrics{Total: len(refreshed)})
|
|
|
|
if reloaded.searchQuery != "stripe" {
|
|
t.Fatalf("expected refresh to preserve search query, got %q", reloaded.searchQuery)
|
|
}
|
|
if len(reloaded.filtered) != 1 || reloaded.filtered[0].Company != "Stripe" {
|
|
t.Fatalf("expected refresh+search to keep filter applied, got %+v", reloaded.filtered)
|
|
}
|
|
}
|
|
|
|
func TestRejectedAndDiscardedTabsFilterCorrectly(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{
|
|
Company: "Acme",
|
|
Role: "Backend Engineer",
|
|
Status: "Rejected",
|
|
Score: 3.4,
|
|
ReportPath: "reports/001-acme.md",
|
|
},
|
|
{
|
|
Company: "Beta",
|
|
Role: "Platform Engineer",
|
|
Status: "Discarded",
|
|
Score: 2.1,
|
|
ReportPath: "reports/002-beta.md",
|
|
},
|
|
{
|
|
Company: "Gamma",
|
|
Role: "AI Engineer",
|
|
Status: "Applied",
|
|
Score: 4.6,
|
|
ReportPath: "reports/003-gamma.md",
|
|
},
|
|
}
|
|
|
|
pm := NewPipelineModel(
|
|
theme.NewTheme("catppuccin-mocha"),
|
|
apps,
|
|
model.PipelineMetrics{Total: len(apps)},
|
|
"..",
|
|
120,
|
|
40,
|
|
)
|
|
|
|
pm.activeTab = tabIndexForFilter(t, filterRejected)
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Status != "Rejected" {
|
|
t.Fatalf("expected rejected tab to isolate rejected rows, got %+v", pm.filtered)
|
|
}
|
|
|
|
pm.activeTab = tabIndexForFilter(t, filterDiscarded)
|
|
pm.applyFilterAndSort()
|
|
if len(pm.filtered) != 1 || pm.filtered[0].Status != "Discarded" {
|
|
t.Fatalf("expected discarded tab to isolate discarded rows, got %+v", pm.filtered)
|
|
}
|
|
}
|
|
|
|
// Regression: with no committed search query, Esc must NOT close the screen.
|
|
// The help bar advertises only `q quit`, so Esc quitting silently was a bug
|
|
// that surfaced as accidental exits when users hit Esc to "back out" of the UI.
|
|
func TestEscWithoutQueryIsNoOp(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
if pm.searchQuery != "" {
|
|
t.Fatalf("setup expected empty search query, got %q", pm.searchQuery)
|
|
}
|
|
|
|
pm, cmd := pm.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
if cmd != nil {
|
|
// PipelineClosedMsg used to fire here; ensure it doesn't anymore.
|
|
if msg := cmd(); msg != nil {
|
|
if _, ok := msg.(PipelineClosedMsg); ok {
|
|
t.Fatalf("expected Esc with no query to be a no-op, got PipelineClosedMsg")
|
|
}
|
|
t.Fatalf("expected Esc with no query to return nil cmd, got %T", msg)
|
|
}
|
|
}
|
|
if pm.searchInput {
|
|
t.Fatal("Esc with no query should not toggle searchInput")
|
|
}
|
|
}
|
|
|
|
// Regression: typing during search input must not synchronously fan out to
|
|
// loadCurrentReport. Reading reports per keystroke caused visible UI lag, so
|
|
// the load is deferred to commit (Enter) / cancel (Esc) instead.
|
|
func TestSearchTypingDoesNotLoadReports(t *testing.T) {
|
|
apps := []model.CareerApplication{
|
|
{Company: "Stripe", Role: "Backend Engineer", Status: "Evaluated", Score: 4.6, ReportPath: "reports/001-stripe.md"},
|
|
{Company: "Anthropic", Role: "AI Engineer", Status: "Evaluated", Score: 4.8, ReportPath: "reports/002-anthropic.md"},
|
|
}
|
|
|
|
pm := NewPipelineModel(theme.NewTheme("catppuccin-mocha"), apps, model.PipelineMetrics{Total: len(apps)}, "..", 120, 40)
|
|
|
|
pm, _ = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
if !pm.searchInput {
|
|
t.Fatal("expected `/` to open search input")
|
|
}
|
|
|
|
// Typing must not trigger PipelineLoadReportMsg.
|
|
for _, r := range "stri" {
|
|
var cmd tea.Cmd
|
|
pm, cmd = pm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
|
|
if cmd != nil {
|
|
if msg := cmd(); msg != nil {
|
|
if _, ok := msg.(PipelineLoadReportMsg); ok {
|
|
t.Fatalf("typing rune %q should not emit PipelineLoadReportMsg", string(r))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Backspace must not trigger PipelineLoadReportMsg either.
|
|
pm, cmd := pm.Update(tea.KeyMsg{Type: tea.KeyBackspace})
|
|
if cmd != nil {
|
|
if msg := cmd(); msg != nil {
|
|
if _, ok := msg.(PipelineLoadReportMsg); ok {
|
|
t.Fatal("Backspace during search input should not emit PipelineLoadReportMsg")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ctrl+U must not trigger PipelineLoadReportMsg either.
|
|
pm, cmd = pm.Update(tea.KeyMsg{Type: tea.KeyCtrlU})
|
|
if cmd != nil {
|
|
if msg := cmd(); msg != nil {
|
|
if _, ok := msg.(PipelineLoadReportMsg); ok {
|
|
t.Fatal("Ctrl+U during search input should not emit PipelineLoadReportMsg")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func previewModelWith(t *testing.T, app model.CareerApplication) PipelineModel {
|
|
t.Helper()
|
|
|
|
pm := NewPipelineModel(
|
|
theme.NewTheme("catppuccin-mocha"),
|
|
[]model.CareerApplication{app},
|
|
model.PipelineMetrics{Total: 1},
|
|
"..",
|
|
120,
|
|
40,
|
|
)
|
|
pm.applyFilterAndSort()
|
|
pm.cursor = 0
|
|
return pm
|
|
}
|
|
|
|
func TestPreviewKeepsDiscardReasonWhenTlDrIsCached(t *testing.T) {
|
|
app := model.CareerApplication{
|
|
Company: "Acme",
|
|
Role: "Backend Engineer",
|
|
Status: "Descartado 2026-03-12",
|
|
Notes: "took too long to respond",
|
|
ReportPath: "reports/001-acme.md",
|
|
}
|
|
pm := previewModelWith(t, app)
|
|
pm.reportCache[app.ReportPath] = reportSummary{tldr: "great team, fast pace"}
|
|
|
|
preview := pm.renderPreview()
|
|
|
|
if !strings.Contains(preview, "great team, fast pace") {
|
|
t.Fatalf("expected preview to keep the cached TL;DR, got %q", preview)
|
|
}
|
|
// Regression for #787: before the Outcome line, a cached TL;DR replaced the
|
|
// notes entirely and the discard reason disappeared from the preview.
|
|
if !strings.Contains(preview, "took too long to respond") {
|
|
t.Fatalf("expected preview to keep the discard reason alongside the TL;DR, got %q", preview)
|
|
}
|
|
if !strings.Contains(preview, "Descartado 2026-03-12") {
|
|
t.Fatalf("expected preview to show the closing status, got %q", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewOutcomeShownWithoutReportSummary(t *testing.T) {
|
|
pm := previewModelWith(t, model.CareerApplication{
|
|
Company: "Beta",
|
|
Role: "Platform Engineer",
|
|
Status: "SKIP",
|
|
Notes: "geo blocker",
|
|
})
|
|
|
|
preview := pm.renderPreview()
|
|
|
|
if !strings.Contains(preview, "Outcome:") || !strings.Contains(preview, "geo blocker") {
|
|
t.Fatalf("expected outcome line with notes for skipped app, got %q", preview)
|
|
}
|
|
if strings.Count(preview, "geo blocker") != 1 {
|
|
t.Fatalf("expected notes to appear exactly once, got %q", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewOutcomeOmittedForActiveApps(t *testing.T) {
|
|
app := model.CareerApplication{
|
|
Company: "Gamma",
|
|
Role: "AI Engineer",
|
|
Status: "Applied 2026-04-01",
|
|
Notes: "warm intro via referral",
|
|
ReportPath: "reports/003-gamma.md",
|
|
}
|
|
pm := previewModelWith(t, app)
|
|
pm.reportCache[app.ReportPath] = reportSummary{tldr: "strong fit"}
|
|
|
|
preview := pm.renderPreview()
|
|
|
|
if strings.Contains(preview, "Outcome:") {
|
|
t.Fatalf("expected no outcome line for an active app, got %q", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewOutcomeForStatusWithoutNotes(t *testing.T) {
|
|
pm := previewModelWith(t, model.CareerApplication{
|
|
Company: "Delta",
|
|
Role: "SRE",
|
|
Status: "**Rejected** 2026-05-02",
|
|
})
|
|
|
|
preview := pm.renderPreview()
|
|
|
|
if !strings.Contains(preview, "Rejected 2026-05-02") {
|
|
t.Fatalf("expected outcome to show the bare closing status, got %q", preview)
|
|
}
|
|
if strings.Contains(preview, "Loading preview...") {
|
|
t.Fatalf("expected outcome line to replace the loading placeholder, got %q", preview)
|
|
}
|
|
}
|
|
|
|
func TestWithReloadedDataPreservesCursorWhenAppRemoved(t *testing.T) {
|
|
initialApps := []model.CareerApplication{
|
|
{
|
|
Company: "Acme",
|
|
Role: "Backend Engineer",
|
|
Status: "Applied",
|
|
Score: 4.2,
|
|
ReportPath: "reports/001-acme.md",
|
|
},
|
|
{
|
|
Company: "Beta",
|
|
Role: "Platform Engineer",
|
|
Status: "Applied",
|
|
Score: 4.6,
|
|
ReportPath: "reports/002-beta.md",
|
|
},
|
|
{
|
|
Company: "Gamma",
|
|
Role: "AI Engineer",
|
|
Status: "Applied",
|
|
Score: 4.8,
|
|
ReportPath: "reports/003-gamma.md",
|
|
},
|
|
}
|
|
|
|
pm := NewPipelineModel(
|
|
theme.NewTheme("catppuccin-mocha"),
|
|
initialApps,
|
|
model.PipelineMetrics{Total: len(initialApps)},
|
|
"..",
|
|
120,
|
|
40,
|
|
)
|
|
pm.activeTab = tabIndexForFilter(t, filterApplied)
|
|
pm.applyFilterAndSort()
|
|
pm.cursor = 1
|
|
|
|
refreshedApps := []model.CareerApplication{
|
|
initialApps[0],
|
|
{
|
|
Company: "Beta",
|
|
Role: "Platform Engineer",
|
|
Status: "Rejected", // Changed!
|
|
Score: 4.6,
|
|
ReportPath: "reports/002-beta.md",
|
|
},
|
|
initialApps[2],
|
|
}
|
|
|
|
reloaded := pm.WithReloadedData(refreshedApps, model.PipelineMetrics{Total: len(refreshedApps)})
|
|
|
|
if got := len(reloaded.filtered); got != 2 {
|
|
t.Fatalf("expected 2 filtered apps after refresh, got %d", got)
|
|
}
|
|
if reloaded.cursor < 0 || reloaded.cursor >= len(reloaded.filtered) {
|
|
t.Fatalf("expected cursor to be within [0, %d], got %d", len(reloaded.filtered)-1, reloaded.cursor)
|
|
}
|
|
}
|
|
|