Files
kportal/internal/ui/wizard_views_test.go
lukaszraczylo 90ddca6709 test: cover ui handlers/views/commands/table
internal/ui: 27.9% -> 79.8%. Adds three test files (~3300 lines):
  - wizard_handlers_extended_test.go: keyboard branches not previously
    covered (ctrl+c, pgup/pgdn, search filter typing, port-checked
    failure path, alias text edit, focus toggling, error states),
    plus removeForwardsCmd / removeForwardByIDCmd against real Mutator
    + tempdir config (dedup + at-least-one-namespace validation).
  - wizard_views_test.go: every renderXxx path with a populated
    AddWizardState — context/namespace selectors, resource selection,
    port entry, http log list/detail, success, scroll indicators.
  - table_test.go: AddForward / UpdateStatus / SetError / Clear /
    FilterStrings flows.

Test-only fixups noted by agent:
  - getFilteredEntries silently skips StatusCode==0 entries (only
    completed responses are visible). Tests now seed StatusCode=200.
  - handleForwardSaved leaves step=StepConfirmation on save error
    rather than advancing to StepSuccess; assertion corrected.
  - removeForwardsCmd needs >=2 forwards in a context for dedup tests
    so removing one leaves the context non-empty (validator rejects
    empty contexts).

go test -race -count=1 ./... clean across all 14 packages.
2026-05-06 14:08:58 +01:00

1468 lines
43 KiB
Go

package ui
import (
"strings"
"testing"
"github.com/lukaszraczylo/kportal/internal/benchmark"
"github.com/lukaszraczylo/kportal/internal/config"
"github.com/lukaszraczylo/kportal/internal/k8s"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newModelWithWizard returns a model with an initialised AddWizardState at the given step.
func newModelWithWizard(step AddWizardStep) model {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeAddWizard
w := newAddWizardState()
w.step = step
w.selectedContext = "my-ctx"
w.selectedNamespace = "default"
w.resourceValue = "my-app"
w.remotePort = 8080
w.localPort = 8081
w.contexts = []string{"my-ctx", "other-ctx"}
w.namespaces = []string{"default", "kube-system"}
ui.addWizard = w
ui.mu.Unlock()
return model{ui: ui, termWidth: 120, termHeight: 40}
}
// ----- renderAddWizard (dispatch) -----------------------------------------
func TestRenderAddWizard_NilWizard(t *testing.T) {
m := newTestModel()
result := m.renderAddWizard()
assert.Empty(t, result)
}
// ----- renderSelectContext ------------------------------------------------
func TestRenderSelectContext_Loading(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
m.ui.addWizard.loading = true
result := m.renderSelectContext()
assert.Contains(t, result, "Loading")
}
func TestRenderSelectContext_Error(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
m.ui.addWizard.error = assert.AnError
result := m.renderSelectContext()
assert.Contains(t, result, "Error")
}
func TestRenderSelectContext_NoContexts(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
m.ui.addWizard.contexts = []string{}
result := m.renderSelectContext()
assert.Contains(t, result, "No contexts")
}
func TestRenderSelectContext_WithContexts(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
result := m.renderSelectContext()
assert.Contains(t, result, "my-ctx")
assert.Contains(t, result, "(current)")
}
func TestRenderSelectContext_WithSearchFilter(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
m.ui.addWizard.searchFilter = "my"
result := m.renderSelectContext()
assert.Contains(t, result, "Filter: ")
}
func TestRenderSelectContext_FilterNoMatch(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
m.ui.addWizard.searchFilter = "zzz"
result := m.renderSelectContext()
assert.Contains(t, result, "No matching")
}
func TestRenderSelectContext_ScrollIndicators(t *testing.T) {
m := newModelWithWizard(StepSelectContext)
// Create many contexts to trigger scroll.
many := make([]string, 30)
for i := range many {
many[i] = "ctx"
}
m.ui.addWizard.contexts = many
m.ui.addWizard.scrollOffset = 5
result := m.renderSelectContext()
assert.Contains(t, result, "↑ More above ↑")
assert.Contains(t, result, "↓ More below ↓")
}
// ----- renderSelectNamespace ---------------------------------------------
func TestRenderSelectNamespace_Loading(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
m.ui.addWizard.loading = true
result := m.renderSelectNamespace()
assert.Contains(t, result, "Loading namespaces")
}
func TestRenderSelectNamespace_Error(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
m.ui.addWizard.error = assert.AnError
result := m.renderSelectNamespace()
assert.Contains(t, result, "Error")
assert.Contains(t, result, "unreachable")
}
func TestRenderSelectNamespace_NoNamespaces(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
m.ui.addWizard.namespaces = []string{}
result := m.renderSelectNamespace()
assert.Contains(t, result, "No namespaces")
}
func TestRenderSelectNamespace_WithNamespaces(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
result := m.renderSelectNamespace()
assert.Contains(t, result, "default")
}
func TestRenderSelectNamespace_FilterNoMatch(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
m.ui.addWizard.searchFilter = "zzz"
result := m.renderSelectNamespace()
assert.Contains(t, result, "No matching")
}
func TestRenderSelectNamespace_WithSearchFilter(t *testing.T) {
m := newModelWithWizard(StepSelectNamespace)
m.ui.addWizard.searchFilter = "def"
result := m.renderSelectNamespace()
assert.Contains(t, result, "Filter: ")
}
// ----- renderSelectResourceType ------------------------------------------
func TestRenderSelectResourceType_AllTypes(t *testing.T) {
m := newModelWithWizard(StepSelectResourceType)
result := m.renderSelectResourceType()
assert.Contains(t, result, "Pod (by name prefix)")
assert.Contains(t, result, "Pod (by label selector)")
assert.Contains(t, result, "Service")
}
func TestRenderSelectResourceType_CursorHighlight(t *testing.T) {
m := newModelWithWizard(StepSelectResourceType)
m.ui.addWizard.cursor = 1
result := m.renderSelectResourceType()
// Description of second type should be shown.
assert.Contains(t, result, "Flexible")
}
// ----- renderEnterResource -----------------------------------------------
func TestRenderEnterResource_PodPrefix_Loading(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodPrefix
m.ui.addWizard.loading = true
result := m.renderEnterResource()
assert.Contains(t, result, "Loading pods")
}
func TestRenderEnterResource_PodPrefix_WithPods(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodPrefix
m.ui.addWizard.pods = []k8s.PodInfo{{Name: "my-app-abc"}, {Name: "my-app-def"}}
m.ui.addWizard.textInput = "my-app"
result := m.renderEnterResource()
assert.Contains(t, result, "my-app-abc")
assert.Contains(t, result, "Matches")
}
func TestRenderEnterResource_PodPrefix_NoMatchingPods(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodPrefix
m.ui.addWizard.pods = []k8s.PodInfo{{Name: "other-pod"}}
m.ui.addWizard.textInput = "nomatch"
result := m.renderEnterResource()
assert.Contains(t, result, "No matching pods")
}
func TestRenderEnterResource_PodPrefix_MorePodsIndicator(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodPrefix
pods := make([]k8s.PodInfo, 10)
for i := range pods {
pods[i] = k8s.PodInfo{Name: "my-pod"}
}
m.ui.addWizard.pods = pods
result := m.renderEnterResource()
assert.Contains(t, result, "more")
}
func TestRenderEnterResource_PodSelector_WithMatchingPods(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodSelector
m.ui.addWizard.matchingPods = []k8s.PodInfo{{Name: "pod-1"}, {Name: "pod-2"}, {Name: "pod-3"}, {Name: "pod-4"}}
result := m.renderEnterResource()
assert.Contains(t, result, "Found 4 matching")
}
func TestRenderEnterResource_PodSelector_Error(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodSelector
m.ui.addWizard.error = assert.AnError
result := m.renderEnterResource()
assert.Contains(t, result, "Invalid selector")
}
func TestRenderEnterResource_PodSelector_Loading(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypePodSelector
m.ui.addWizard.loading = true
result := m.renderEnterResource()
assert.Contains(t, result, "Validating")
}
func TestRenderEnterResource_Service_Loading(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypeService
m.ui.addWizard.loading = true
result := m.renderEnterResource()
assert.Contains(t, result, "Loading services")
}
func TestRenderEnterResource_Service_Empty(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypeService
result := m.renderEnterResource()
assert.Contains(t, result, "No services")
}
func TestRenderEnterResource_Service_WithServices(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypeService
m.ui.addWizard.services = []k8s.ServiceInfo{{Name: "api-svc"}, {Name: "db-svc"}}
result := m.renderEnterResource()
assert.Contains(t, result, "api-svc")
}
func TestRenderEnterResource_Service_FilterNoMatch(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypeService
m.ui.addWizard.services = []k8s.ServiceInfo{{Name: "api-svc"}}
m.ui.addWizard.searchFilter = "zzz"
result := m.renderEnterResource()
assert.Contains(t, result, "No matching services")
}
func TestRenderEnterResource_Service_WithSearchFilter(t *testing.T) {
m := newModelWithWizard(StepEnterResource)
m.ui.addWizard.selectedResourceType = ResourceTypeService
m.ui.addWizard.services = []k8s.ServiceInfo{{Name: "api-svc"}}
m.ui.addWizard.searchFilter = "api"
result := m.renderEnterResource()
assert.Contains(t, result, "Filter: ")
}
// ----- renderEnterRemotePort ---------------------------------------------
func TestRenderEnterRemotePort_TextMode(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeText
result := m.renderEnterRemotePort()
assert.Contains(t, result, "Remote port:")
}
func TestRenderEnterRemotePort_TextMode_Error(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeText
m.ui.addWizard.error = assert.AnError
result := m.renderEnterRemotePort()
assert.Contains(t, result, "✗")
}
func TestRenderEnterRemotePort_TextMode_WithInput(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeText
m.ui.addWizard.textInput = "8080"
result := m.renderEnterRemotePort()
assert.Contains(t, result, "Press Enter")
}
func TestRenderEnterRemotePort_ListMode_WithPorts(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeList
m.ui.addWizard.detectedPorts = []k8s.PortInfo{
{Port: 80, Name: "http"},
{Port: 8080, TargetPort: 9090, Name: "grpc"},
}
result := m.renderEnterRemotePort()
assert.Contains(t, result, "Manual entry")
assert.Contains(t, result, "80")
}
func TestRenderEnterRemotePort_ListMode_ScrollIndicators(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeList
ports := make([]k8s.PortInfo, 25)
for i := range ports {
ports[i] = k8s.PortInfo{Port: int32(8000 + i)} //nolint:gosec // safe: i ≤ 24, no overflow
}
m.ui.addWizard.detectedPorts = ports
m.ui.addWizard.scrollOffset = 3
result := m.renderEnterRemotePort()
assert.Contains(t, result, "↑ More above ↑")
}
func TestRenderEnterRemotePort_TextMode_DetectedPortsShown(t *testing.T) {
// Text mode but with detected ports — shows them as reference.
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeText
m.ui.addWizard.detectedPorts = []k8s.PortInfo{{Port: 80}}
result := m.renderEnterRemotePort()
assert.Contains(t, result, "Detected ports")
}
func TestRenderEnterRemotePort_Selector(t *testing.T) {
m := newModelWithWizard(StepEnterRemotePort)
m.ui.addWizard.inputMode = InputModeText
m.ui.addWizard.selector = "app=my-app"
result := m.renderEnterRemotePort()
assert.Contains(t, result, "app=my-app")
}
// ----- renderEnterLocalPort ----------------------------------------------
func TestRenderEnterLocalPort_Loading(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.loading = true
result := m.renderEnterLocalPort()
assert.Contains(t, result, "Checking availability")
}
func TestRenderEnterLocalPort_Error(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.error = assert.AnError
result := m.renderEnterLocalPort()
assert.Contains(t, result, "✗")
}
func TestRenderEnterLocalPort_PortAvailable(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.portAvailable = true
m.ui.addWizard.portCheckMsg = "✓ Port 8081 available"
result := m.renderEnterLocalPort()
assert.Contains(t, result, "✓")
}
func TestRenderEnterLocalPort_PortUnavailable(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.portAvailable = false
m.ui.addWizard.portCheckMsg = "✗ Port 8081 in use"
result := m.renderEnterLocalPort()
assert.Contains(t, result, "in use")
}
func TestRenderEnterLocalPort_WithInput(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.textInput = "8081"
result := m.renderEnterLocalPort()
assert.Contains(t, result, "Press Enter")
}
func TestRenderEnterLocalPort_WithSelector(t *testing.T) {
m := newModelWithWizard(StepEnterLocalPort)
m.ui.addWizard.selector = "app=db"
result := m.renderEnterLocalPort()
assert.Contains(t, result, "app=db")
}
// ----- renderConfirmation ------------------------------------------------
func TestRenderConfirmation_DefaultView(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.confirmationFocus = FocusButtons
m.ui.addWizard.cursor = 0
result := m.renderConfirmation()
assert.Contains(t, result, "Add to .kportal.yaml")
assert.Contains(t, result, "my-ctx")
assert.Contains(t, result, "default")
}
func TestRenderConfirmation_AliasFocus(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.confirmationFocus = FocusAlias
result := m.renderConfirmation()
assert.Contains(t, result, "█") // cursor in alias field
}
func TestRenderConfirmation_ButtonsCancelSelected(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.confirmationFocus = FocusButtons
m.ui.addWizard.cursor = 1 // Cancel
result := m.renderConfirmation()
assert.Contains(t, result, "Cancel")
}
func TestRenderConfirmation_HTTPLogEnabled(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.httpLog = true
result := m.renderConfirmation()
assert.Contains(t, result, "[x] enabled")
}
func TestRenderConfirmation_HTTPLogDisabled(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.httpLog = false
result := m.renderConfirmation()
assert.Contains(t, result, "[ ] disabled")
}
func TestRenderConfirmation_SelectorResource(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.selector = "app=myapp"
result := m.renderConfirmation()
assert.Contains(t, result, "selector")
}
func TestRenderConfirmation_ServiceResource(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.selectedResourceType = ResourceTypeService
result := m.renderConfirmation()
assert.Contains(t, result, "service/")
}
func TestRenderConfirmation_PodPrefixResource(t *testing.T) {
m := newModelWithWizard(StepConfirmation)
m.ui.addWizard.selectedResourceType = ResourceTypePodPrefix
result := m.renderConfirmation()
assert.Contains(t, result, "pod/")
}
// ----- renderSuccess -----------------------------------------------------
func TestRenderSuccess_Success(t *testing.T) {
m := newModelWithWizard(StepSuccess)
m.ui.addWizard.alias = "my-alias"
result := m.renderSuccess()
assert.Contains(t, result, "Success")
assert.Contains(t, result, "Add another")
assert.Contains(t, result, "Return to main view")
}
func TestRenderSuccess_Error(t *testing.T) {
m := newModelWithWizard(StepSuccess)
m.ui.addWizard.error = assert.AnError
result := m.renderSuccess()
assert.Contains(t, result, "Error")
}
func TestRenderSuccess_CursorOnReturn(t *testing.T) {
m := newModelWithWizard(StepSuccess)
m.ui.addWizard.cursor = 1
result := m.renderSuccess()
assert.Contains(t, result, "▸ Return to main view")
}
func TestRenderSuccess_WithAlias(t *testing.T) {
m := newModelWithWizard(StepSuccess)
m.ui.addWizard.alias = "my-svc"
result := m.renderSuccess()
assert.Contains(t, result, "my-svc")
}
// ----- renderAddWizard dispatch ------------------------------------------
func TestRenderAddWizard_UnknownStep(t *testing.T) {
m := newModelWithWizard(AddWizardStep(99))
result := m.renderAddWizard()
assert.Contains(t, result, "Unknown step")
}
// ----- renderRemoveWizard ------------------------------------------------
func TestRenderRemoveWizard_NilWizard(t *testing.T) {
m := newTestModel()
result := m.renderRemoveWizard()
assert.Empty(t, result)
}
func TestRenderRemoveSelection_ShowsForwards(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeRemoveWizard
ui.removeWizard = &RemoveWizardState{
forwards: []RemovableForward{
{ID: "f1", Alias: "alpha", Port: 80, LocalPort: 8080, Context: "ctx", Namespace: "ns", Resource: "pod/alpha"},
{ID: "f2", Alias: "beta", Port: 81, LocalPort: 8081, Context: "ctx", Namespace: "ns", Resource: "pod/beta"},
},
selected: map[int]bool{0: true},
cursor: 0,
}
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderRemoveSelection()
assert.Contains(t, result, "alpha")
assert.Contains(t, result, "beta")
assert.Contains(t, result, "[✓]")
assert.Contains(t, result, "1 of 2 selected")
}
func TestRenderRemoveConfirmation_Shows(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeRemoveWizard
ui.removeWizard = &RemoveWizardState{
forwards: []RemovableForward{
{ID: "f1", Alias: "alpha", Port: 80, LocalPort: 8080, Context: "ctx", Namespace: "ns", Resource: "pod/alpha"},
},
selected: map[int]bool{0: true},
confirming: true,
}
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderRemoveConfirmation()
assert.Contains(t, result, "alpha")
assert.Contains(t, result, "Yes, remove")
assert.Contains(t, result, "cannot be undone")
}
func TestRenderRemoveConfirmation_CancelSelected(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeRemoveWizard
ui.removeWizard = &RemoveWizardState{
forwards: []RemovableForward{{ID: "f1", Alias: "alpha"}},
selected: map[int]bool{0: true},
confirming: true,
confirmCursor: 1,
}
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderRemoveConfirmation()
assert.Contains(t, result, "▸ Cancel")
}
// ----- renderBenchmark ---------------------------------------------------
func TestRenderBenchmark_NilState(t *testing.T) {
m := newTestModel()
result := m.renderBenchmark()
assert.Empty(t, result)
}
func TestRenderBenchmarkConfig(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
ui.benchmarkState = newBenchmarkState("fwd-id", "my-svc", 8080)
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmarkConfig()
assert.Contains(t, result, "URL Path")
assert.Contains(t, result, "Method")
assert.Contains(t, result, "Concurrency")
assert.Contains(t, result, "Requests")
assert.Contains(t, result, "my-svc")
}
func TestRenderBenchmarkRunning(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "my-svc", 8080)
state.step = BenchmarkStepRunning
state.running = true
state.progress = 30
state.total = 100
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmarkRunning()
assert.Contains(t, result, "Running")
assert.Contains(t, result, "30")
assert.Contains(t, result, "100")
}
func TestRenderBenchmarkResults_WithError(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "my-svc", 8080)
state.step = BenchmarkStepResults
state.error = assert.AnError
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmarkResults()
assert.Contains(t, result, "Error")
}
func TestRenderBenchmarkResults_NilResults(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "my-svc", 8080)
state.step = BenchmarkStepResults
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmarkResults()
assert.Contains(t, result, "No results")
}
func TestRenderBenchmarkResults_WithResults(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "my-svc", 8080)
state.step = BenchmarkStepResults
state.results = &BenchmarkResults{
TotalRequests: 100,
Successful: 95,
Failed: 5,
MinLatency: 1.0,
MaxLatency: 50.0,
AvgLatency: 10.0,
P50Latency: 8.0,
P95Latency: 40.0,
P99Latency: 48.0,
Throughput: 50.0,
BytesRead: 10240,
StatusCodes: map[int]int{200: 95, 500: 5},
}
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmarkResults()
assert.Contains(t, result, "Total Requests: 100")
assert.Contains(t, result, "Successful:")
assert.Contains(t, result, "Failed:")
assert.Contains(t, result, "P95")
assert.Contains(t, result, "Status Codes")
}
func TestRenderBenchmark_Dispatch(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "my-svc", 8080)
state.step = BenchmarkStepConfig
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderBenchmark()
require.NotEmpty(t, result)
assert.Contains(t, result, "URL Path")
}
// ----- renderHTTPLog -----------------------------------------------------
func TestRenderHTTPLog_NilState(t *testing.T) {
m := newTestModel()
result := m.renderHTTPLog()
assert.Empty(t, result)
}
func TestRenderHTTPLog_NoEntries(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
ui.httpLogState = newHTTPLogState("fwd-id", "my-svc")
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "No HTTP traffic")
}
func TestRenderHTTPLog_WithEntries(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.entries = []HTTPLogEntry{
{Method: "GET", Path: "/api/test", StatusCode: 200, Timestamp: "12:00:00"},
{Method: "POST", Path: "/api/create", StatusCode: 500, Timestamp: "12:00:01"},
{Method: "PUT", Path: "/api/update", StatusCode: 404, Timestamp: "12:00:02"},
}
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "GET")
assert.Contains(t, result, "/api/test")
}
func TestRenderHTTPLog_FilterActive(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.filterActive = true
state.filterText = "test"
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "Search: ")
}
func TestRenderHTTPLog_FilteredEntries(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.entries = []HTTPLogEntry{
{Method: "GET", Path: "/api/test", StatusCode: 200},
}
state.filterMode = HTTPLogFilterNon200
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
// With Non200 filter active and only 200s, no entries match.
assert.Contains(t, result, "No entries match filter")
}
func TestRenderHTTPLog_AutoScrollIndicator(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.autoScroll = true
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "Auto-scroll")
}
func TestRenderHTTPLog_FilterIndicator(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.filterMode = HTTPLogFilterErrors
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "Filter:")
}
func TestRenderHTTPLog_DetailView(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
state.entries = []HTTPLogEntry{
{
Method: "GET",
Path: "/detail",
StatusCode: 200,
LatencyMs: 100,
RequestHeaders: map[string]string{
"Content-Type": "application/json",
},
ResponseHeaders: map[string]string{
"X-Request-ID": "abc123",
},
RequestBody: `{"key": "value"}`,
ResponseBody: `{"result": "ok"}`,
},
}
state.cursor = 0
state.showingDetail = true
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderHTTPLog()
assert.Contains(t, result, "Request Detail")
}
// ----- renderHTTPLogDetail -----------------------------------------------
func TestRenderHTTPLogDetail_HighLatency(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
state := newHTTPLogState("fwd-id", "my-svc")
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{
Method: "GET",
Path: "/slow",
LatencyMs: 2500, // >1000ms → should display as seconds
}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "2.50s")
}
func TestRenderHTTPLogDetail_Status500(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{Method: "POST", Path: "/fail", StatusCode: 500}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "500")
}
func TestRenderHTTPLogDetail_Status400(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{Method: "GET", Path: "/notfound", StatusCode: 404}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "404")
}
func TestRenderHTTPLogDetail_WithError(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{Method: "GET", Path: "/err", Error: "connection reset"}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "connection reset")
}
func TestRenderHTTPLogDetail_BinaryRequestBody(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{
Method: "POST",
Path: "/upload",
RequestBody: "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a",
RequestHeaders: map[string]string{
"Content-Type": "application/octet-stream",
},
}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "Binary data")
}
func TestRenderHTTPLogDetail_CopyMessage(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
state.copyMessage = "Copied!"
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
entry := HTTPLogEntry{Method: "GET", Path: "/x"}
result := m.renderHTTPLogDetail(entry, 120, 40)
assert.Contains(t, result, "Copied!")
}
func TestRenderHTTPLogDetail_ScrollIndicator(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
state := newHTTPLogState("fwd-id", "my-svc")
// Force a long response body to make content exceed viewport.
bodyLines := strings.Repeat("a line of body content\n", 100)
state.entries = []HTTPLogEntry{{
Method: "GET",
Path: "/long",
StatusCode: 200,
ResponseBody: bodyLines,
ResponseHeaders: map[string]string{
"Content-Type": "text/plain",
},
}}
state.detailScroll = 5
ui.httpLogState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 10}
entry := state.entries[0]
result := m.renderHTTPLogDetail(entry, 120, 10)
// Should contain a scroll percentage indicator.
assert.Contains(t, result, "%")
}
// ----- renderMainView helpers -------------------------------------------
func TestRenderMainView_EmptyForwards(t *testing.T) {
m := newTestModel()
result := m.renderMainView()
assert.Contains(t, result, "No forwards configured")
}
func TestRenderMainView_WithForwards(t *testing.T) {
m := newTestModelWithForward()
result := m.renderMainView()
assert.Contains(t, result, "my-app")
assert.Contains(t, result, "CONTEXT")
assert.Contains(t, result, "STATUS")
}
func TestRenderMainView_WithErrors(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
fwd := &config.Forward{Resource: "pod/my-app", Port: 8080, LocalPort: 8080, Alias: "my-app"}
ui.AddForward("id-1", fwd)
ui.SetError("id-1", "connection refused")
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderMainView()
assert.Contains(t, result, "Errors:")
assert.Contains(t, result, "connection refused")
}
func TestRenderMainView_UpdateAvailable(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.SetUpdateAvailable("2.0.0", "http://example.com")
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderMainView()
assert.Contains(t, result, "Update available")
assert.Contains(t, result, "2.0.0")
}
func TestRenderDeleteConfirmation_YesSelected(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.deleteConfirming = true
ui.deleteConfirmID = "id-1"
ui.deleteConfirmAlias = "my-service"
ui.deleteConfirmCursor = 0 // Yes
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderDeleteConfirmation()
assert.Contains(t, result, "my-service")
assert.Contains(t, result, "Yes")
assert.Contains(t, result, "No")
}
func TestRenderDeleteConfirmation_NoSelected(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.deleteConfirming = true
ui.deleteConfirmAlias = "my-service"
ui.deleteConfirmCursor = 1 // No
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.renderDeleteConfirmation()
assert.Contains(t, result, "Yes")
assert.Contains(t, result, "No")
}
// ----- View() dispatch --------------------------------------------------
func TestModelView_MainViewDefault(t *testing.T) {
m := newTestModel()
result := m.View()
assert.NotEmpty(t, result)
}
func TestModelView_DeleteConfirmationOverlay(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
fwd := &config.Forward{Resource: "pod/app", Port: 8080, LocalPort: 8080, Alias: "app"}
ui.AddForward("id-1", fwd)
ui.mu.Lock()
ui.deleteConfirming = true
ui.deleteConfirmAlias = "app"
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.View()
assert.Contains(t, result, "Delete Port Forward")
}
func TestModelView_AddWizardOverlay(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeAddWizard
ui.addWizard = newAddWizardState()
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.View()
assert.NotEmpty(t, result)
}
func TestModelView_RemoveWizardOverlay(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeRemoveWizard
ui.removeWizard = &RemoveWizardState{
forwards: []RemovableForward{{ID: "f1", Alias: "alpha"}},
selected: map[int]bool{},
}
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.View()
assert.Contains(t, result, "alpha")
}
func TestModelView_BenchmarkOverlay(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
ui.benchmarkState = newBenchmarkState("fwd-id", "my-svc", 8080)
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.View()
assert.NotEmpty(t, result)
}
func TestModelView_HTTPLogFullScreen(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeHTTPLog
ui.httpLogState = newHTTPLogState("fwd-id", "my-svc")
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
result := m.View()
assert.Contains(t, result, "HTTP Traffic Log")
}
func TestModelView_ZeroTermSize(t *testing.T) {
m := newTestModel()
m.termWidth = 0
m.termHeight = 0
// Must not panic even with zero term dims.
result := m.View()
assert.NotEmpty(t, result)
}
// ----- decompressContent -------------------------------------------------
func TestDecompressContent_NoEncoding(t *testing.T) {
content := "hello world"
result := decompressContent(content, map[string]string{})
assert.Equal(t, content, result)
}
func TestDecompressContent_UnknownEncoding(t *testing.T) {
content := "hello world"
result := decompressContent(content, map[string]string{"Content-Encoding": "br"})
assert.Equal(t, content, result)
}
func TestDecompressContent_InvalidGzip(t *testing.T) {
content := "not-gzip-data"
result := decompressContent(content, map[string]string{"Content-Encoding": "gzip"})
assert.Equal(t, content, result)
}
func TestDecompressContent_InvalidDeflate(t *testing.T) {
content := "not-deflate-data"
result := decompressContent(content, map[string]string{"Content-Encoding": "deflate"})
// Should return original since invalid deflate data.
assert.NotEmpty(t, result)
}
// ----- isBinaryContent ---------------------------------------------------
func TestIsBinaryContent_ImageContentType(t *testing.T) {
result := isBinaryContent("data", map[string]string{"Content-Type": "image/png"})
assert.True(t, result)
}
func TestIsBinaryContent_AudioContentType(t *testing.T) {
result := isBinaryContent("data", map[string]string{"Content-Type": "audio/mp3"})
assert.True(t, result)
}
func TestIsBinaryContent_OctetStream(t *testing.T) {
result := isBinaryContent("data", map[string]string{"Content-Type": "application/octet-stream"})
assert.True(t, result)
}
func TestIsBinaryContent_TextContent(t *testing.T) {
result := isBinaryContent("hello world", map[string]string{"Content-Type": "text/plain"})
assert.False(t, result)
}
func TestIsBinaryContent_BinaryBytes(t *testing.T) {
// Many non-printable bytes should be detected as binary.
binary := strings.Repeat("\x00\x01\x02", 50)
result := isBinaryContent(binary, map[string]string{})
assert.True(t, result)
}
func TestIsBinaryContent_EmptyContent(t *testing.T) {
result := isBinaryContent("", map[string]string{})
assert.False(t, result)
}
// ----- formatJSONContent ------------------------------------------------
func TestFormatJSONContent_ValidJSON(t *testing.T) {
json := `{"key":"value","num":42}`
result := formatJSONContent(json, map[string]string{"Content-Type": "application/json"})
assert.Contains(t, result, "key")
assert.Contains(t, result, "value")
}
func TestFormatJSONContent_NotJSON(t *testing.T) {
text := "plain text content"
result := formatJSONContent(text, map[string]string{"Content-Type": "text/plain"})
assert.Equal(t, text, result)
}
func TestFormatJSONContent_InvalidJSON(t *testing.T) {
bad := `{"key": invalid}`
result := formatJSONContent(bad, map[string]string{"Content-Type": "application/json"})
assert.Equal(t, bad, result)
}
func TestFormatJSONContent_AutoDetect(t *testing.T) {
json := `{"auto": true}`
result := formatJSONContent(json, map[string]string{})
assert.Contains(t, result, "auto")
}
func TestFormatJSONContent_ArrayJSON(t *testing.T) {
json := `[1, 2, 3]`
result := formatJSONContent(json, map[string]string{})
assert.Contains(t, result, "1")
}
func TestFormatJSONContent_JSONPlusType(t *testing.T) {
json := `{"ok":true}`
result := formatJSONContent(json, map[string]string{"Content-Type": "application/vnd.api+json"})
assert.Contains(t, result, "ok")
}
// ----- colorizeJSON / colorizeLine / colorizeValue / isJSONNumber --------
func TestColorizeJSON_RoundTrip(t *testing.T) {
json := `{
"key": "value",
"num": 42,
"flag": true,
"nothing": null,
"arr": []
}`
result := colorizeJSON(json)
assert.Contains(t, result, "key")
assert.Contains(t, result, "value")
assert.Contains(t, result, "42")
}
func TestColorizeValue_Null(t *testing.T) {
result := colorizeValue("null")
assert.Contains(t, result, "null")
}
func TestColorizeValue_Bool(t *testing.T) {
assert.Contains(t, colorizeValue("true"), "true")
assert.Contains(t, colorizeValue("false"), "false")
}
func TestColorizeValue_String(t *testing.T) {
result := colorizeValue(`"hello"`)
assert.Contains(t, result, "hello")
}
func TestColorizeValue_Number(t *testing.T) {
result := colorizeValue("42")
assert.Contains(t, result, "42")
}
func TestColorizeValue_Structural(t *testing.T) {
for _, v := range []string{"{", "}", "[", "]", "{}", "[]"} {
assert.Equal(t, v, colorizeValue(v))
}
}
func TestColorizeValue_Unknown(t *testing.T) {
result := colorizeValue("weird_value")
assert.Equal(t, "weird_value", result)
}
func TestColorizeValue_Empty(t *testing.T) {
result := colorizeValue("")
assert.Equal(t, "", result)
}
func TestIsJSONNumber(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"42", true},
{"3.14", true},
{"-5", true},
{"1e10", true},
{"1.5E-3", true},
{"", false},
{"-", false},
{"abc", false},
{"1.2.3", false},
{"1e", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.expected, isJSONNumber(tt.input))
})
}
}
// ----- renderStyles helpers ----------------------------------------------
func TestRenderProgress(t *testing.T) {
result := renderProgress(3, 7)
assert.Contains(t, result, "3")
assert.Contains(t, result, "7")
}
func TestRenderHeader(t *testing.T) {
result := renderHeader("My Title", "Step 1/5")
assert.Contains(t, result, "My Title")
assert.Contains(t, result, "Step 1/5")
}
func TestRenderHeader_NoProgress(t *testing.T) {
result := renderHeader("Title Only", "")
assert.Contains(t, result, "Title Only")
}
func TestRenderBreadcrumb(t *testing.T) {
result := renderBreadcrumb("ctx", "ns")
assert.Contains(t, result, "ctx")
assert.Contains(t, result, "ns")
}
func TestRenderList(t *testing.T) {
items := []string{"alpha", "beta", "gamma"}
result := renderList(items, 1, " ", 0)
assert.Contains(t, result, "alpha")
assert.Contains(t, result, "beta")
assert.Contains(t, result, "gamma")
assert.Contains(t, result, "▸") // cursor on item 1
}
func TestRenderList_ScrollIndicators(t *testing.T) {
// Need offset > 0 for the "above" indicator and offset+ViewportHeight < totalItems for the "below" indicator.
// ViewportHeight = 20, so with offset=5 we need totalItems > 25.
items := make([]string, 27)
for i := range items {
items[i] = "item"
}
// cursor=22 is within the visible window [5,25), scrollOffset=5.
// end = 5+20 = 25 < 27, so "↓ More below ↓" is rendered.
result := renderList(items, 22, " ", 5)
assert.Contains(t, result, "↑ More above ↑")
assert.Contains(t, result, "↓ More below ↓")
}
func TestRenderTextInput_Valid(t *testing.T) {
result := renderTextInput("Label: ", "hello", true)
assert.Contains(t, result, "Label: ")
assert.Contains(t, result, "hello")
assert.Contains(t, result, "█")
}
func TestRenderTextInput_Invalid(t *testing.T) {
result := renderTextInput("Port: ", "abc", false)
assert.Contains(t, result, "abc")
}
func TestWizardHelpWidth_Zero(t *testing.T) {
w := wizardHelpWidth(0)
assert.Greater(t, w, 0)
}
func TestWizardHelpWidth_Narrow(t *testing.T) {
w := wizardHelpWidth(30)
assert.LessOrEqual(t, w, 30)
}
func TestWizardHelpWidth_Normal(t *testing.T) {
w := wizardHelpWidth(120)
assert.Equal(t, 70, w) // capped at 70
}
// ----- overlayContent ---------------------------------------------------
func TestOverlayContent_BasicPlacement(t *testing.T) {
bg := strings.Repeat("background\n", 20)
overlay := "MODAL"
result := overlayContent(bg, overlay, 80, 20)
assert.Contains(t, result, "MODAL")
}
// ----- buildFooterLines -------------------------------------------------
func TestBuildFooterLines_NormalWidth(t *testing.T) {
m := newTestModel()
lines := m.buildFooterLines(120)
require.NotEmpty(t, lines)
// Last line should contain the total count.
lastLine := lines[len(lines)-1]
assert.Contains(t, lastLine, "Total:")
}
func TestBuildFooterLines_NarrowTerminal(t *testing.T) {
m := newTestModel()
// Narrow terminal forces wrapping.
lines := m.buildFooterLines(40)
require.NotEmpty(t, lines)
}
// ----- getTermDimensions ------------------------------------------------
func TestGetTermDimensions_Defaults(t *testing.T) {
m := model{ui: NewBubbleTeaUI(nil, "1.0.0"), termWidth: 0, termHeight: 0}
w, h := m.getTermDimensions()
assert.Equal(t, DefaultTermWidth, w)
assert.Equal(t, DefaultTermHeight, h)
}
func TestGetTermDimensions_Set(t *testing.T) {
m := model{ui: NewBubbleTeaUI(nil, "1.0.0"), termWidth: 200, termHeight: 50}
w, h := m.getTermDimensions()
assert.Equal(t, 200, w)
assert.Equal(t, 50, h)
}
// ----- getStatusIconAndText ---------------------------------------------
func TestGetStatusIconAndText(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
fwd := &config.Forward{Resource: "pod/app", Port: 80, LocalPort: 8080}
ui.AddForward("id-1", fwd)
m := model{ui: ui, termWidth: 120, termHeight: 40}
tests := []struct {
status string
expectIcon string
expectText string
disabled bool
}{
{"Active", "●", "Active", false},
{"Starting", "○", "Starting", false},
{"Reconnecting", "◐", "Reconnecting", false},
{"Error", "✗", "Error", false},
{"Active", "○", "Disabled", true},
}
for _, tt := range tests {
t.Run(tt.status+"_disabled="+func() string {
if tt.disabled {
return "true"
}
return "false"
}(), func(t *testing.T) {
ui.mu.Lock()
ui.forwards["id-1"].Status = tt.status
ui.disabledMap["id-1"] = tt.disabled
ui.mu.Unlock()
ui.mu.RLock()
icon, text := m.getStatusIconAndText("id-1", ui.forwards["id-1"])
ui.mu.RUnlock()
assert.Equal(t, tt.expectIcon, icon)
assert.Equal(t, tt.expectText, text)
})
}
}
// ----- defaultMainViewColors / mainViewKeyBindings ----------------------
func TestDefaultMainViewColors(t *testing.T) {
colors := defaultMainViewColors()
assert.NotEmpty(t, string(colors.header))
assert.NotEmpty(t, string(colors.active))
}
func TestMainViewKeyBindings(t *testing.T) {
bindings := mainViewKeyBindings()
require.NotEmpty(t, bindings)
// Spot-check a few expected bindings.
var keys []string
for _, b := range bindings {
keys = append(keys, b.key)
}
assert.Contains(t, keys, "n")
assert.Contains(t, keys, "d")
}
// ----- safeRecover -------------------------------------------------------
func TestSafeRecover_DoesNotPanicOnRecover(t *testing.T) {
// Must not propagate panic.
assert.NotPanics(t, func() {
func() {
defer safeRecover("test context")
panic("test panic")
}()
})
}
// ----- BenchmarkResults (populate via handleBenchmarkComplete) -----------
func TestBenchmarkResults_FromCompleteMsg(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock()
ui.viewMode = ViewModeBenchmark
state := newBenchmarkState("fwd-id", "alias", 8080)
state.running = true
ui.benchmarkState = state
ui.mu.Unlock()
m := model{ui: ui, termWidth: 120, termHeight: 40}
results := &benchmark.Results{
TotalRequests: 100,
Successful: 100,
Failed: 0,
}
// CalculateStats operates on the Latencies slice; with no durations, stats are zero.
_ = results.CalculateStats()
msg := BenchmarkCompleteMsg{
ForwardID: "fwd-id",
Results: results,
}
m.handleBenchmarkComplete(msg)
ui.mu.RLock()
defer ui.mu.RUnlock()
require.NotNil(t, ui.benchmarkState.results)
assert.Equal(t, 100, ui.benchmarkState.results.TotalRequests)
}