package tui import ( "bytes" "errors" "fmt" "regexp" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/profile" "proxyctl/internal/target" ) func TestCompleteModelRendersResponsiveTerminalSizeClasses(t *testing.T) { module := responsiveLayoutModule() t.Run("wide 120x36", func(t *testing.T) { model := resizedModel(t, module, themeModeNoColor, 120, 36) view := model.View() if !sameLine(view, "Targets", "Target details") { t.Fatalf("wide layout did not place list and details side by side:\n%s", view) } assertUsableFrame(t, view, 120, 36, "Shell", "Shell summary", "Active Profile: office") }) t.Run("narrow 80x24", func(t *testing.T) { model := resizedModel(t, module, themeModeNoColor, 80, 24) view := model.View() if sameLine(view, "Targets", "Target details") || strings.Index(view, "Target details") < strings.Index(view, "Shell") { t.Fatalf("narrow layout did not stack list above details:\n%s", view) } assertUsableFrame(t, view, 80, 24, "Shell", "Shell summary", "Active Profile: office") }) t.Run("below minimum 59x15", func(t *testing.T) { model := resizedModel(t, module, themeModeNoColor, 59, 15) view := model.View() assertUsableFrame(t, view, 59, 15, "Terminal too small", "Minimum size: 60×16", "Current size: 59×15") if strings.Contains(view, "Shell summary") || strings.Contains(view, "Targets\n") { t.Fatalf("minimum-size message was mixed with the application interface:\n%s", view) } }) } func TestCompleteModelResizePreservesValidSelectionAndFormFocus(t *testing.T) { module := responsiveLayoutModule() model := resizedModel(t, module, themeModeNoColor, 120, 36) model = updateKey(model, "down") model = resize(model, 80, 24) if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") { t.Fatalf("Target selection did not survive wide-to-narrow resize:\n%s", view) } model = resize(model, 120, 36) if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") { t.Fatalf("Target selection did not survive narrow-to-wide resize:\n%s", view) } model = updateKey(model, "p") model = updateKey(model, "e") model = updateKey(model, "ctrl+t") for range 4 { model = updateKey(model, "tab") } for _, size := range []struct{ width, height int }{{60, 16}, {120, 36}, {80, 24}} { model = resize(model, size.width, size.height) view := model.View() assertUsableFrame(t, view, size.width, size.height, "> Bypass:", "Ctrl+S: save") } } func TestCompleteModelSemanticThemeSupportsDarkLightAndNoColor(t *testing.T) { module := responsiveLayoutModule() dark := resizedModel(t, module, themeModeDark, 80, 24).View() light := resizedModel(t, module, themeModeLight, 80, 24).View() plain := resizedModel(t, module, themeModeNoColor, 80, 24).View() for name, view := range map[string]string{"dark": dark, "light": light, "NO_COLOR": plain} { for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} { if !strings.Contains(view, semantic) { t.Fatalf("%s theme lost non-color semantic %q:\n%s", name, semantic, view) } } } if !strings.Contains(dark, "\x1b[") || !strings.Contains(light, "\x1b[") { t.Fatalf("light or dark semantic theme emitted no styling\ndark: %q\nlight: %q", dark, light) } if dark == light { t.Fatal("light and dark themes rendered identically") } if ansiPattern.MatchString(plain) { t.Fatalf("NO_COLOR rendering contained ANSI styling: %q", plain) } } func TestNewModelUsesInjectedBubbleTeaOutputForTerminalCapabilities(t *testing.T) { var bubbleTeaOutput bytes.Buffer model := resize(NewModel(responsiveLayoutModule(), WithOutput(&bubbleTeaOutput)), 80, 24) view := model.View() if ansiPattern.MatchString(view) { t.Fatalf("non-terminal Bubble Tea output received ANSI styling: %q", view) } for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} { if !strings.Contains(view, semantic) { t.Fatalf("injected-output rendering lost semantic %q:\n%s", semantic, view) } } } func TestCompleteModelRespectsNoColorEnvironment(t *testing.T) { t.Setenv("NO_COLOR", "1") model := resize(NewModel(responsiveLayoutModule()), 80, 24) view := model.View() if ansiPattern.MatchString(view) { t.Fatalf("NO_COLOR environment produced ANSI styling: %q", view) } for _, semantic := range []string{"Disabled", "External", ">"} { if !strings.Contains(view, semantic) { t.Fatalf("NO_COLOR environment lost semantic %q:\n%s", semantic, view) } } } func TestCompleteModelDefensivelyRedactsEveryRenderedPage(t *testing.T) { const ( secret = "foundation-secret" credential = "https://alice:" + secret + "@proxy.example:8443" redacted = "https://alice:REDACTED@proxy.example:8443" ) module := &recordingModule{snapshot: app.Snapshot{ ActiveProfile: credential, Profiles: []profile.Profile{{ Name: credential, Mode: profile.SingleMode, SingleURL: credential, Bypass: []string{credential}, }}, Targets: []app.TargetSnapshot{{ ID: credential, Description: target.Description{Name: credential, Summary: credential, Details: credential}, Available: true, State: target.External, Evidence: credential, AvailabilityEvidence: credential, }}, }} model := resizedModel(t, module, themeModeNoColor, 120, 36) frames := []string{model.View()} model = updateKey(model, "?") frames = append(frames, model.View()) model = updateKey(model, "q") model = updateKey(model, "p") frames = append(frames, model.View()) model = updateKey(model, "e") frames = append(frames, model.View()) model.status = "operation failed for " + credential frames = append(frames, model.View()) for i, frame := range frames { if strings.Contains(frame, secret) { t.Fatalf("frame %d exposed a credential:\n%s", i, frame) } if !strings.Contains(frame, redacted) { t.Fatalf("frame %d did not apply the presentation redaction path:\n%s", i, frame) } } } func TestCompleteModelRedactsApplicationErrorsBeforeRendering(t *testing.T) { const secret = "result-secret" initial := app.Snapshot{Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Disabled}}} module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Err: errors.New("failed https://bob:" + secret + "@proxy.example:8080")}} model := resizedModel(t, module, themeModeNoColor, 80, 24) updated, cmd := model.Update(keyMessage("r")) model = updated.(Model) updated, _ = model.Update(cmd()) model = updated.(Model) model = updateKey(model, "e") view := model.View() if strings.Contains(view, secret) || !strings.Contains(view, "https://bob:REDACTED@proxy.example:8080") { t.Fatalf("Application error bypassed presentation redaction:\n%s", view) } } func TestCompleteModelNarrowShellKeepsLastProductionTargetStatusAndActionsVisible(t *testing.T) { snapshot := productionScaleSnapshot() longNotice := "Refresh completed; selected Target remains actionable. " + strings.Repeat("Additional redacted diagnostic context. ", 12) module := &targetModule{snapshot: snapshot, result: app.Result{Snapshot: snapshot, Notice: longNotice}} model := resizedModel(t, module, themeModeNoColor, 80, 24) for range len(snapshot.Targets) - 1 { model = updateKey(model, "down") } updated, refresh := model.Update(keyMessage("r")) model = updated.(Model) updated, _ = model.Update(refresh()) model = updated.(Model) assertUsableFrame(t, model.View(), 80, 24, "> ! Drifted Docker daemon", "State: ! Drifted", "Docker daemon selected evidence.", "Status: Refresh completed; selected Target remains actionable.", "Space: Resolve", "p: manage Proxy Profiles", ) model = resize(model, 120, 36) model = resize(model, 80, 24) assertUsableFrame(t, model.View(), 80, 24, "> ! Drifted Docker daemon", "State: ! Drifted", "Docker daemon selected evidence.", "Space: Resolve", ) } func TestCompleteModelLongProfileListKeepsLastSelectionDetailsAndActionsVisibleAcrossResize(t *testing.T) { profiles := make([]profile.Profile, 32) for i := range profiles { profiles[i] = profile.Profile{ Name: fmt.Sprintf("regional-office-%02d", i), Mode: profile.SingleMode, SingleURL: fmt.Sprintf("http://proxy-%02d.example:8080", i), } } module := &recordingModule{snapshot: app.Snapshot{ActiveProfile: profiles[0].Name, Profiles: profiles}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "p") for range len(profiles) - 1 { model = updateKey(model, "down") } for _, size := range []struct{ width, height int }{{80, 24}, {120, 36}, {80, 24}} { model = resize(model, size.width, size.height) assertUsableFrame(t, model.View(), size.width, size.height, "> regional-office-31", "Proxy: http://proxy-31.example:8080", "a/Enter: activate", "q/Esc: back", ) } } func productionScaleSnapshot() app.Snapshot { return app.Snapshot{ ActiveProfile: "office", Profiles: []profile.Profile{{Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080"}}, Targets: []app.TargetSnapshot{ {ID: "shell", Description: target.Description{Name: "Shell", Summary: "Shell summary", Details: strings.Repeat("Shell detail. ", 30)}, Available: true, State: target.Enabled, Evidence: strings.Repeat("Shell evidence. ", 20)}, {ID: "apt", Description: target.Description{Name: "APT", Summary: "APT summary", Details: strings.Repeat("APT detail. ", 30)}, Available: true, State: target.Disabled, Evidence: strings.Repeat("APT evidence. ", 20)}, {ID: "docker-containers", Description: target.Description{Name: "Docker containers/builds", Summary: "Docker client summary", Details: strings.Repeat("Docker client detail. ", 30)}, Available: true, State: target.External, Evidence: strings.Repeat("Docker client evidence. ", 20)}, {ID: "gnome", Description: target.Description{Name: "GNOME", Summary: "GNOME summary", Details: strings.Repeat("GNOME detail. ", 30)}, Available: true, State: target.Enabled, Evidence: strings.Repeat("GNOME evidence. ", 20)}, {ID: "docker-daemon", Description: target.Description{Name: "Docker daemon", Summary: "Docker daemon selected summary", Details: strings.Repeat("Docker daemon selected detail. ", 30)}, Available: true, State: target.Drifted, Evidence: strings.Repeat("Docker daemon selected evidence. ", 20)}, }, } } var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) func responsiveLayoutModule() *recordingModule { return &recordingModule{snapshot: app.Snapshot{ ActiveProfile: "office", Profiles: []profile.Profile{{Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080"}}, Targets: []app.TargetSnapshot{ {ID: "shell", Description: target.Description{Name: "Shell", Summary: "Shell summary", Details: "Shell details"}, Available: true, State: target.Disabled, Evidence: "Shell evidence"}, {ID: "apt", Description: target.Description{Name: "APT", Summary: "APT summary", Details: "APT details"}, Available: true, State: target.External, Evidence: "APT evidence"}, }, }} } func resizedModel(t *testing.T, module app.InteractiveModule, mode themeMode, width, height int) Model { t.Helper() return resize(NewModel(module, withTheme(mode)), width, height) } func resize(model Model, width, height int) Model { updated, _ := model.Update(tea.WindowSizeMsg{Width: width, Height: height}) return updated.(Model) } func sameLine(view, first, second string) bool { for _, line := range strings.Split(view, "\n") { if strings.Contains(line, first) && strings.Contains(line, second) { return true } } return false } func assertUsableFrame(t *testing.T, view string, width, height int, semantics ...string) { t.Helper() lines := strings.Split(strings.TrimSuffix(view, "\n"), "\n") if len(lines) > height { t.Fatalf("frame has %d lines, terminal height is %d:\n%s", len(lines), height, view) } for _, line := range lines { if len([]rune(line)) > width { t.Fatalf("frame line is wider than %d columns: %q", width, line) } } for _, semantic := range semantics { if !strings.Contains(view, semantic) { t.Fatalf("frame missing %q:\n%s", semantic, view) } } }