diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-05 23:47:19 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-05 23:47:19 +0200 |
| commit | 77310af6f292004fbdd11eaa0bcfeaff812a365d (patch) | |
| tree | 02c0c242759efa8a9fa2dfc970515bcc6b77bc1a /internal/tui | |
| parent | b48fb545191be25e9795e79336c45c439466986c (diff) | |
Make flame tab default and fix flame hotkey routing
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/common/keys.go | 14 | ||||
| -rw-r--r-- | internal/tui/common/keys_test.go | 20 | ||||
| -rw-r--r-- | internal/tui/dashboard/model.go | 44 | ||||
| -rw-r--r-- | internal/tui/dashboard/model_test.go | 87 | ||||
| -rw-r--r-- | internal/tui/dashboard/tabs.go | 2 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model.go | 28 | ||||
| -rw-r--r-- | internal/tui/flamegraph/renderer.go | 26 | ||||
| -rw-r--r-- | internal/tui/flamegraph/renderer_test.go | 24 | ||||
| -rw-r--r-- | internal/tui/tui.go | 8 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 58 |
10 files changed, 252 insertions, 59 deletions
diff --git a/internal/tui/common/keys.go b/internal/tui/common/keys.go index 6b0ae27..ab9865d 100644 --- a/internal/tui/common/keys.go +++ b/internal/tui/common/keys.go @@ -38,13 +38,13 @@ func DefaultKeyMap() KeyMap { return KeyMap{ Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next tab")), ShiftTab: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev tab")), - One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "overview")), - Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "syscalls")), - Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "files")), - Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "processes")), - Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "lat+gaps")), - Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "stream")), - Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "flame")), + One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "flame")), + Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "overview")), + Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "syscalls")), + Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "files")), + Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "processes")), + Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "lat+gaps")), + Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "stream")), DirGroup: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "dir group")), SelectPID: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "select pid")), SelectTID: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "select tid")), diff --git a/internal/tui/common/keys_test.go b/internal/tui/common/keys_test.go index e043f9e..4284faf 100644 --- a/internal/tui/common/keys_test.go +++ b/internal/tui/common/keys_test.go @@ -24,8 +24,8 @@ func TestDefaultKeyMapIncludesDirGroupBinding(t *testing.T) { t.Fatalf("unexpected select tid binding help: key=%q desc=%q", selectTIDHelp.Key, selectTIDHelp.Desc) } - flameHelp := keys.Seven.Help() - if flameHelp.Key != "7" || flameHelp.Desc != "flame" { + flameHelp := keys.One.Help() + if flameHelp.Key != "1" || flameHelp.Desc != "flame" { t.Fatalf("unexpected flame binding help: key=%q desc=%q", flameHelp.Key, flameHelp.Desc) } } @@ -38,7 +38,7 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) { } found := false - foundSeven := false + foundOne := false for _, binding := range groups[1] { help := binding.Help() if help.Key == "d" && help.Desc == "dir group" { @@ -52,12 +52,12 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) { for _, binding := range groups[0] { help := binding.Help() - if help.Key == "7" && help.Desc == "flame" { - foundSeven = true + if help.Key == "1" && help.Desc == "flame" { + foundOne = true break } } - if !foundSeven { + if !foundOne { t.Fatalf("expected flame tab binding in dashboard full help tabs") } @@ -103,7 +103,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { short := keys.DashboardStatusHelp() found := false foundSelectTID := false - foundSeven := false + foundOne := false for _, binding := range short { help := binding.Help() if help.Key == "o" && help.Desc == "probes" { @@ -112,8 +112,8 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { if help.Key == "t" && help.Desc == "select tid" { foundSelectTID = true } - if help.Key == "7" && help.Desc == "flame" { - foundSeven = true + if help.Key == "1" && help.Desc == "flame" { + foundOne = true } } if !found { @@ -122,7 +122,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { if !foundSelectTID { t.Fatalf("expected select tid binding in dashboard short help") } - if !foundSeven { + if !foundOne { t.Fatalf("expected flame tab binding in dashboard short help") } } diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index 7807b31..24b6c8e 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -68,7 +68,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuf refreshMs = defaultRefreshMs } m := Model{ - activeTab: TabOverview, + activeTab: TabFlame, engine: engine, refreshEvery: time.Duration(refreshMs) * time.Millisecond, keys: keys, @@ -157,6 +157,11 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.showHelp = !m.showHelp return m, nil } + if m.activeTab == TabFlame && m.flamegraphModel.ConsumesKey(msg) { + next, flameCmd := m.flamegraphModel.Update(msg) + m.flamegraphModel = next.(flamegraphtui.Model) + return m, flameCmd + } handled, scrollCmd := m.handleScrollKey(msg) if scrollCmd != nil { cmd = scrollCmd @@ -167,32 +172,32 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if !handled { switch { + case key.Matches(msg, m.keys.One): + m.activeTab = TabFlame + handled = true case key.Matches(msg, m.keys.Tab): m.activeTab = nextTab(m.activeTab) handled = true case key.Matches(msg, m.keys.ShiftTab): m.activeTab = prevTab(m.activeTab) handled = true - case key.Matches(msg, m.keys.One): - m.activeTab = TabOverview - handled = true case key.Matches(msg, m.keys.Two): - m.activeTab = TabSyscalls + m.activeTab = TabOverview handled = true case key.Matches(msg, m.keys.Three): - m.activeTab = TabFiles + m.activeTab = TabSyscalls handled = true case key.Matches(msg, m.keys.Four): - m.activeTab = TabProcesses + m.activeTab = TabFiles handled = true case key.Matches(msg, m.keys.Five): - m.activeTab = TabLatency + m.activeTab = TabProcesses handled = true case key.Matches(msg, m.keys.Six): - m.activeTab = TabStream + m.activeTab = TabLatency handled = true case key.Matches(msg, m.keys.Seven): - m.activeTab = TabFlame + m.activeTab = TabStream handled = true case key.Matches(msg, m.keys.Refresh): snap := m.snapshot() @@ -206,6 +211,11 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } } if !handled { + if m.activeTab == TabFlame { + next, flameCmd := m.flamegraphModel.Update(msg) + m.flamegraphModel = next.(flamegraphtui.Model) + return m, flameCmd + } return m, nil } batch := make([]tea.Cmd, 0, 3) @@ -317,10 +327,16 @@ func (m Model) LatestSnapshot() *statsengine.Snapshot { return m.latest } -// BlocksGlobalShortcuts reports whether modal UI in the active tab should -// suppress top-level shortcuts (for example global export key handling). -func (m Model) BlocksGlobalShortcuts() bool { - return m.activeTab == TabStream && (m.streamModel.FilterModalVisible() || m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible()) +// BlocksGlobalShortcuts reports whether the active tab should suppress a +// top-level shortcut for the given key press. +func (m Model) BlocksGlobalShortcuts(msg tea.KeyPressMsg) bool { + if m.activeTab == TabStream { + return m.streamModel.FilterModalVisible() || m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible() + } + if m.activeTab == TabFlame { + return m.flamegraphModel.ConsumesKey(msg) + } + return false } // SetStreamSource updates the live stream source used by the stream tab. diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go index 46f4944..6d35d5a 100644 --- a/internal/tui/dashboard/model_test.go +++ b/internal/tui/dashboard/model_test.go @@ -31,37 +31,38 @@ func TestKeySwitchingChangesActiveTab(t *testing.T) { next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) model := next.(Model) - if model.activeTab != TabSyscalls { - t.Fatalf("expected syscalls tab, got %v", model.activeTab) + if model.activeTab != TabOverview { + t.Fatalf("expected overview tab on key 2, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab}) model = next.(Model) - if model.activeTab != TabFiles { - t.Fatalf("expected next tab to be files, got %v", model.activeTab) + if model.activeTab != TabSyscalls { + t.Fatalf("expected next tab to be syscalls, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab, Mod: tea.ModShift}) model = next.(Model) - if model.activeTab != TabSyscalls { - t.Fatalf("expected previous tab to be syscalls, got %v", model.activeTab) + if model.activeTab != TabOverview { + t.Fatalf("expected previous tab to be overview, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})}) model = next.(Model) - if model.activeTab != TabFlame { - t.Fatalf("expected flame tab on key 7, got %v", model.activeTab) + if model.activeTab != TabStream { + t.Fatalf("expected stream tab on key 7, got %v", model.activeTab) } - next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})}) + next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'1'}[0], Text: string([]rune{'1'})}) model = next.(Model) - if model.activeTab != TabStream { - t.Fatalf("expected stream tab on key 6, got %v", model.activeTab) + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab on key 1, got %v", model.activeTab) } } func TestArrowAndViKeysDoNotCycleTabs(t *testing.T) { m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabOverview next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyRight}) model := next.(Model) @@ -303,6 +304,7 @@ func TestRefreshKeyEmitsRefreshTick(t *testing.T) { snap := &statsengine.Snapshot{TotalSyscalls: 13} engine := &fakeSnapshotSource{snap: snap} m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabOverview next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})}) _ = next if cmd == nil { @@ -318,6 +320,63 @@ func TestRefreshKeyEmitsRefreshTick(t *testing.T) { } } +func TestFlameTabReceivesSlashKey(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: string([]rune{'/'})}) + model := next.(Model) + if cmd != nil { + t.Fatalf("did not expect global command for flame search key") + } + if !strings.Contains(model.View().Content, "0/0 matches") { + t.Fatalf("expected flame search footer after pressing /") + } +} + +func TestFlameTabReceivesResetAndPauseKeys(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})}) + model := next.(Model) + if !strings.Contains(model.View().Content, "[PAUSED]") { + t.Fatalf("expected flame pause key to toggle paused state") + } + + next, cmd := model.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})}) + model = next.(Model) + if cmd != nil { + t.Fatalf("expected flame reset key to be handled by flame tab without global refresh command") + } + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab to stay active after reset key") + } +} + +func TestFlameSearchConsumesNumericTabKeys(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: string([]rune{'/'})}) + model := next.(Model) + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab to stay active after opening search") + } + + next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) + model = next.(Model) + if model.activeTab != TabFlame { + t.Fatalf("expected numeric key while searching to stay in flame tab") + } +} + func TestRefreshTickEmitsStatsTickMsg(t *testing.T) { snap := &statsengine.Snapshot{TotalSyscalls: 9} engine := &fakeSnapshotSource{snap: snap} @@ -386,8 +445,8 @@ func TestStatsTickClampsGroupedFilesOffset(t *testing.T) { func TestViewRendersTabBarAndHelp(t *testing.T) { m := NewModelWithConfig(nil, nil, 1000, common.DefaultKeyMap()) out := m.View().Content - if !strings.Contains(out, "Overview") { - t.Fatalf("expected overview label in view") + if !strings.Contains(out, "Flame") { + t.Fatalf("expected flame tab label in view") } if !strings.Contains(out, "press H for help") { t.Fatalf("expected help hint text in view") @@ -437,7 +496,7 @@ func TestStreamTabViewKeepsTabAndHelpChromeVisible(t *testing.T) { m.streamModel.Refresh() out := m.View().Content - if !strings.Contains(out, "1:Overview") { + if !strings.Contains(out, "1:Flame") { t.Fatalf("expected tab bar to remain visible in stream view") } if !strings.Contains(out, "press H for help") { diff --git a/internal/tui/dashboard/tabs.go b/internal/tui/dashboard/tabs.go index 731e21f..85ce319 100644 --- a/internal/tui/dashboard/tabs.go +++ b/internal/tui/dashboard/tabs.go @@ -30,13 +30,13 @@ const ( ) var allTabs = []Tab{ + TabFlame, TabOverview, TabSyscalls, TabFiles, TabProcesses, TabLatency, TabStream, - TabFlame, } func (t Tab) String() string { diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go index 5f5a83c..5d101c2 100644 --- a/internal/tui/flamegraph/model.go +++ b/internal/tui/flamegraph/model.go @@ -208,6 +208,34 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +// ConsumesKey reports whether the flamegraph should handle a key press before +// dashboard- or app-level shortcuts. +func (m Model) ConsumesKey(msg tea.KeyPressMsg) bool { + if m.searchActive { + return true + } + switch { + case msg.String() == "/", + msg.String() == "n", + msg.String() == "N", + msg.String() == "p", + msg.String() == "r", + msg.String() == "o", + msg.String() == "?": + return true + case key.Matches(msg, m.keys.ZoomIn), + key.Matches(msg, m.keys.ZoomUndo), + key.Matches(msg, m.keys.ZoomReset), + key.Matches(msg, m.keys.MoveShallower), + key.Matches(msg, m.keys.MoveDeeper), + key.Matches(msg, m.keys.PrevSibling), + key.Matches(msg, m.keys.NextSibling): + return true + default: + return false + } +} + // View renders the flamegraph viewport. func (m Model) View() tea.View { content := RenderTerminalView(m.frames, m.width, m.height, m.selectedIdx, m.subtreeSet, m.matchIndices, m.isDark, m.searchActive, m.searchQuery) diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go index ad74173..9f31023 100644 --- a/internal/tui/flamegraph/renderer.go +++ b/internal/tui/flamegraph/renderer.go @@ -102,6 +102,10 @@ func frameName(name string, depth int) string { } func terminalFrameColor(name string) color.Color { + if semantic, ok := semanticFrameColor(name); ok { + return semantic + } + hasher := fnv.New32a() _, _ = hasher.Write([]byte(name)) h := hasher.Sum32() @@ -113,6 +117,28 @@ func terminalFrameColor(name string) color.Color { } } +func semanticFrameColor(name string) (color.Color, bool) { + label := strings.ToLower(strings.TrimSpace(name)) + switch { + case label == "": + return nil, false + case strings.Contains(label, "read"), strings.Contains(label, "pread"): + return color.RGBA{R: 78, G: 132, B: 201, A: 255}, true // read I/O: blue + case strings.Contains(label, "write"), strings.Contains(label, "pwrite"): + return color.RGBA{R: 222, G: 122, B: 58, A: 255}, true // write I/O: orange + case strings.Contains(label, "open"), strings.Contains(label, "close"), strings.Contains(label, "stat"), strings.Contains(label, "rename"), strings.Contains(label, "link"): + return color.RGBA{R: 196, G: 168, B: 72, A: 255}, true // metadata I/O: amber + case strings.HasPrefix(label, "/"), strings.Contains(label, "path:"), strings.Contains(label, "/"): + return color.RGBA{R: 88, G: 156, B: 84, A: 255}, true // file paths: green + case strings.Contains(label, "pid"), strings.Contains(label, "tid"): + return color.RGBA{R: 67, G: 151, B: 149, A: 255}, true // process/thread dimensions: teal + case strings.HasPrefix(label, "sys_"): + return color.RGBA{R: 191, G: 99, B: 74, A: 255}, true // other syscall buckets: rust + default: + return nil, false + } +} + // RenderTerminalView renders a terminal flamegraph viewport from laid out frames. func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtreeSet, matchSet map[int]bool, isDark, searchActive bool, searchQuery string) string { if width < minFlameWidth { diff --git a/internal/tui/flamegraph/renderer_test.go b/internal/tui/flamegraph/renderer_test.go index ca837fe..eb111b8 100644 --- a/internal/tui/flamegraph/renderer_test.go +++ b/internal/tui/flamegraph/renderer_test.go @@ -1,6 +1,7 @@ package flamegraph import ( + "image/color" "strings" "testing" ) @@ -108,6 +109,29 @@ func TestBuildTerminalLayoutUsesPathSeparatorAndColor(t *testing.T) { } } +func TestTerminalFrameColorSemanticPalette(t *testing.T) { + tests := []struct { + name string + label string + want color.RGBA + }{ + {name: "read", label: "sys_enter_read", want: color.RGBA{R: 78, G: 132, B: 201, A: 255}}, + {name: "write", label: "sys_enter_write", want: color.RGBA{R: 222, G: 122, B: 58, A: 255}}, + {name: "metadata", label: "sys_enter_openat", want: color.RGBA{R: 196, G: 168, B: 72, A: 255}}, + {name: "path", label: "/var/log/app.log", want: color.RGBA{R: 88, G: 156, B: 84, A: 255}}, + {name: "pid", label: "pid=1234", want: color.RGBA{R: 67, G: 151, B: 149, A: 255}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := terminalFrameColor(tc.label) + if got != tc.want { + t.Fatalf("unexpected semantic color for %q: got=%v want=%v", tc.label, got, tc.want) + } + }) + } +} + func TestRenderTerminalViewShowsNarrowMessage(t *testing.T) { out := RenderTerminalView(nil, 50, 10, 0, nil, nil, true, false, "") if !strings.Contains(out, "terminal too narrow") { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index ab719fb..328202e 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -300,18 +300,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.stopTrace() return m, tea.Quit } - if m.exportEnabled && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { + if m.exportEnabled && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) { m.exporter = m.exporter.Open() return m, nil } - if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Probes) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { + if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Probes) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) { m.probeModal = probes.NewModel(m.runtime.currentProbeManager()).SetDarkMode(m.isDark).Open() return m, nil } - if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectPID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { + if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectPID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) { return m.reselectPID() } - if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectTID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { + if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectTID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) { return m.reselectTID() } case tuiexport.RequestMsg: diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index c801b24..876fe8f 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -270,7 +270,7 @@ func TestTracingStartedRebindsEventStreamSource(t *testing.T) { next, _ = m.Update(tea.WindowSizeMsg{Width: 120, Height: 30}) m = next.(Model) - next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})}) + next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})}) m = next.(Model) next, _ = m.Update(messages.StatsTickMsg{}) m = next.(Model) @@ -295,6 +295,37 @@ func TestExportKeyOpensModalOnDashboard(t *testing.T) { } } +func TestFlamePauseKeyDoesNotTriggerPIDReselect(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})}) + updated := next.(Model) + if updated.screen != ScreenDashboard { + t.Fatalf("expected flame pause key to keep dashboard screen, got %v", updated.screen) + } + if !strings.Contains(updated.View().Content, "[PAUSED]") { + t.Fatalf("expected flame pause key to toggle flame paused state") + } +} + +func TestFlameOrderKeyDoesNotOpenProbeModal(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'o'}[0], Text: string([]rune{'o'})}) + updated := next.(Model) + if updated.probeModal.Visible() { + t.Fatalf("expected flame order key to stay in flame tab, not open probes modal") + } +} + func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) { m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -304,6 +335,9 @@ func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) { stopped := false m.traceStop = func() { stopped = true } + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) + m = next.(Model) + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})}) updated := next.(Model) @@ -336,6 +370,9 @@ func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { stopped := false m.traceStop = func() { stopped = true } + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) + m = next.(Model) + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'t'}[0], Text: string([]rune{'t'})}) updated := next.(Model) if !stopped { @@ -361,6 +398,9 @@ func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) { stopped := false m.traceStop = func() { stopped = true } + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) + m = next.(Model) + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'t'}[0], Text: string([]rune{'t'})}) updated := next.(Model) if !stopped { @@ -444,7 +484,7 @@ func TestStreamFilterModalConsumesEKeyInsteadOfOpeningExport(t *testing.T) { m.width = 120 m.height = 30 - next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})}) + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})}) m = next.(Model) next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})}) m = next.(Model) @@ -586,22 +626,22 @@ func TestDashboardTabKeysChangeActiveView(t *testing.T) { m.height = 30 out := m.View().Content - if !strings.Contains(out, "Overview: waiting for stats") { - t.Fatalf("expected overview waiting view by default") + if !strings.Contains(out, "Flame: waiting for data") { + t.Fatalf("expected flame waiting view by default") } next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) updated := next.(Model) out = updated.View().Content - if !strings.Contains(out, "Syscalls: waiting for stats") { - t.Fatalf("expected syscalls waiting view after pressing 2") + if !strings.Contains(out, "Overview: waiting for stats") { + t.Fatalf("expected overview waiting view after pressing 2") } next, _ = updated.Update(tea.KeyPressMsg{Code: tea.KeyTab}) updated = next.(Model) out = updated.View().Content - if !strings.Contains(out, "Files: waiting for stats") { - t.Fatalf("expected files waiting view after tab") + if !strings.Contains(out, "Syscalls: waiting for stats") { + t.Fatalf("expected syscalls waiting view after tab") } } @@ -619,7 +659,7 @@ func TestProbeModalViewDoesNotStackDashboardContent(t *testing.T) { if !strings.Contains(out, "Probes (") { t.Fatalf("expected probe modal content, got %q", out) } - if strings.Contains(out, "Overview: waiting for stats") { + if strings.Contains(out, "Flame: waiting for data") || strings.Contains(out, "Overview: waiting for stats") { t.Fatalf("expected probe modal to render as standalone view, got stacked dashboard content") } } |
