cli: preserve styled errors and keep selected session visible
- commit
- 88024fa6996e66e4fa9b4f21ab28abff06fa03cd
- author
- brice · · 2026-02-10 09:49 UTC
- parent
- a86fc63
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +103 -20
M internal/cli/cmd/config.go +4 -4
| @@ -117,20 +117,20 @@ func runConfigOpen(_ *cobra.Command, _ []string) error { | |||
| 117 | 117 | configFile, err := config.GetConfigFile() | |
| 118 | 118 | if err != nil { | |
| 119 | 119 | renderer := styles.NewConfigRenderer(app.Theme) | |
| 120 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("get config file: %w", err)))) | |
| 120 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("get config file: %w", err)), "\n")) | |
| 121 | 121 | } | |
| 122 | 122 | ||
| 123 | 123 | // Check if config file exists | |
| 124 | 124 | if _, statErr := os.Stat(configFile); os.IsNotExist(statErr) { | |
| 125 | 125 | renderer := styles.NewConfigRenderer(app.Theme) | |
| 126 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("config file does not exist: %s", configFile)))) | |
| 126 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("config file does not exist: %s", configFile)), "\n")) | |
| 127 | 127 | } | |
| 128 | 128 | ||
| 129 | 129 | editor := getEditor() | |
| 130 | 130 | if editor == "" { | |
| 131 | 131 | renderer := styles.NewConfigRenderer(app.Theme) | |
| 132 | 132 | const msg = "no editor found: set $EDITOR or $VISUAL environment variable" | |
| 133 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("%s", msg)))) | |
| 133 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("%s", msg)), "\n")) | |
| 134 | 134 | } | |
| 135 | 135 | ||
| 136 | 136 | // Keep this a simple, non-interactive message; the actual editor takes over the terminal. | |
| @@ -143,7 +143,7 @@ func runConfigOpen(_ *cobra.Command, _ []string) error { | |||
| 143 | 143 | ||
| 144 | 144 | if err := cmd.Run(); err != nil { | |
| 145 | 145 | renderer := styles.NewConfigRenderer(app.Theme) | |
| 146 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("open config: %w", err)))) | |
| 146 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("open config: %w", err)), "\n")) | |
| 147 | 147 | } | |
| 148 | 148 | return nil | |
| 149 | 149 | } | |
| @@ -117,20 +117,20 @@ func runConfigOpen(_ *cobra.Command, _ []string) error { | |||||
| 117 | configFile, err := config.GetConfigFile() | 117 | configFile, err := config.GetConfigFile() | ||
| 118 | if err != nil { | 118 | if err != nil { | ||
| 119 | renderer := styles.NewConfigRenderer(app.Theme) | 119 | renderer := styles.NewConfigRenderer(app.Theme) | ||
| 120 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("get config file: %w", err)))) | 120 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("get config file: %w", err)), "\n")) |
| 121 | } | 121 | } | ||
| 122 | 122 | ||||
| 123 | // Check if config file exists | 123 | // Check if config file exists | ||
| 124 | if _, statErr := os.Stat(configFile); os.IsNotExist(statErr) { | 124 | if _, statErr := os.Stat(configFile); os.IsNotExist(statErr) { | ||
| 125 | renderer := styles.NewConfigRenderer(app.Theme) | 125 | renderer := styles.NewConfigRenderer(app.Theme) | ||
| 126 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("config file does not exist: %s", configFile)))) | 126 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("config file does not exist: %s", configFile)), "\n")) |
| 127 | } | 127 | } | ||
| 128 | 128 | ||||
| 129 | editor := getEditor() | 129 | editor := getEditor() | ||
| 130 | if editor == "" { | 130 | if editor == "" { | ||
| 131 | renderer := styles.NewConfigRenderer(app.Theme) | 131 | renderer := styles.NewConfigRenderer(app.Theme) | ||
| 132 | const msg = "no editor found: set $EDITOR or $VISUAL environment variable" | 132 | const msg = "no editor found: set $EDITOR or $VISUAL environment variable" | ||
| 133 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("%s", msg)))) | 133 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("%s", msg)), "\n")) |
| 134 | } | 134 | } | ||
| 135 | 135 | ||||
| 136 | // Keep this a simple, non-interactive message; the actual editor takes over the terminal. | 136 | // Keep this a simple, non-interactive message; the actual editor takes over the terminal. | ||
| @@ -143,7 +143,7 @@ func runConfigOpen(_ *cobra.Command, _ []string) error { | |||||
| 143 | 143 | ||||
| 144 | if err := cmd.Run(); err != nil { | 144 | if err := cmd.Run(); err != nil { | ||
| 145 | renderer := styles.NewConfigRenderer(app.Theme) | 145 | renderer := styles.NewConfigRenderer(app.Theme) | ||
| 146 | - | return fmt.Errorf("%s", strings.TrimSpace(renderer.RenderError(fmt.Errorf("open config: %w", err)))) | 146 | + | return fmt.Errorf("%s", strings.TrimRight(renderer.RenderError(fmt.Errorf("open config: %w", err)), "\n")) |
| 147 | } | 147 | } | ||
| 148 | return nil | 148 | return nil | ||
| 149 | } | 149 | } | ||
M internal/cli/cmd/crashes.go +8 -4
| @@ -100,12 +100,14 @@ func runCrashesShow(_ *cobra.Command, args []string) error { | |||
| 100 | 100 | ||
| 101 | 101 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | |
| 102 | 102 | if err != nil { | |
| 103 | - | return fmt.Errorf("%s\n%s", renderer.RenderError(err), renderer.RenderHintList()) | |
| 103 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList()) | |
| 104 | + | return nil | |
| 104 | 105 | } | |
| 105 | 106 | ||
| 106 | 107 | body, err := os.ReadFile(report.MarkdownPath) | |
| 107 | 108 | if err != nil { | |
| 108 | - | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | |
| 109 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | |
| 110 | + | return nil | |
| 109 | 111 | } | |
| 110 | 112 | fmt.Print(string(body)) | |
| 111 | 113 | return nil | |
| @@ -120,12 +122,14 @@ func runCrashesIssue(_ *cobra.Command, args []string) error { | |||
| 120 | 122 | ||
| 121 | 123 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | |
| 122 | 124 | if err != nil { | |
| 123 | - | return fmt.Errorf("%s\n%s", renderer.RenderError(err), renderer.RenderHintList()) | |
| 125 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList()) | |
| 126 | + | return nil | |
| 124 | 127 | } | |
| 125 | 128 | ||
| 126 | 129 | body, err := os.ReadFile(report.MarkdownPath) | |
| 127 | 130 | if err != nil { | |
| 128 | - | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | |
| 131 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | |
| 132 | + | return nil | |
| 129 | 133 | } | |
| 130 | 134 | all := string(body) | |
| 131 | 135 | start := strings.Index(all, "## GitHub Issue Template") | |
| @@ -100,12 +100,14 @@ func runCrashesShow(_ *cobra.Command, args []string) error { | |||||
| 100 | 100 | ||||
| 101 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | 101 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | ||
| 102 | if err != nil { | 102 | if err != nil { | ||
| 103 | - | return fmt.Errorf("%s\n%s", renderer.RenderError(err), renderer.RenderHintList()) | 103 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList()) |
| 104 | + | return nil | |||
| 104 | } | 105 | } | ||
| 105 | 106 | ||||
| 106 | body, err := os.ReadFile(report.MarkdownPath) | 107 | body, err := os.ReadFile(report.MarkdownPath) | ||
| 107 | if err != nil { | 108 | if err != nil { | ||
| 108 | - | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | 109 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) |
| 110 | + | return nil | |||
| 109 | } | 111 | } | ||
| 110 | fmt.Print(string(body)) | 112 | fmt.Print(string(body)) | ||
| 111 | return nil | 113 | return nil | ||
| @@ -120,12 +122,14 @@ func runCrashesIssue(_ *cobra.Command, args []string) error { | |||||
| 120 | 122 | ||||
| 121 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | 123 | report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0]) | ||
| 122 | if err != nil { | 124 | if err != nil { | ||
| 123 | - | return fmt.Errorf("%s\n%s", renderer.RenderError(err), renderer.RenderHintList()) | 125 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList()) |
| 126 | + | return nil | |||
| 124 | } | 127 | } | ||
| 125 | 128 | ||||
| 126 | body, err := os.ReadFile(report.MarkdownPath) | 129 | body, err := os.ReadFile(report.MarkdownPath) | ||
| 127 | if err != nil { | 130 | if err != nil { | ||
| 128 | - | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) | 131 | + | _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err))) |
| 132 | + | return nil | |||
| 129 | } | 133 | } | ||
| 130 | all := string(body) | 134 | all := string(body) | ||
| 131 | start := strings.Index(all, "## GitHub Issue Template") | 135 | start := strings.Index(all, "## GitHub Issue Template") | ||
M internal/cli/cmd/sessions.go +1 -1
| @@ -107,7 +107,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error { | |||
| 107 | 107 | ||
| 108 | 108 | output, err := app.ListSessionsUC.Execute(app.Ctx(), currentSessionID, sessionsLimit) | |
| 109 | 109 | if err != nil { | |
| 110 | - | return fmt.Errorf("list sessions: %w", err) | |
| 110 | + | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("list sessions: %w", err))) | |
| 111 | 111 | } | |
| 112 | 112 | ||
| 113 | 113 | if sessionsJSON { | |
| @@ -107,7 +107,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error { | |||||
| 107 | 107 | ||||
| 108 | output, err := app.ListSessionsUC.Execute(app.Ctx(), currentSessionID, sessionsLimit) | 108 | output, err := app.ListSessionsUC.Execute(app.Ctx(), currentSessionID, sessionsLimit) | ||
| 109 | if err != nil { | 109 | if err != nil { | ||
| 110 | - | return fmt.Errorf("list sessions: %w", err) | 110 | + | return fmt.Errorf("%s", renderer.RenderError(fmt.Errorf("list sessions: %w", err))) |
| 111 | } | 111 | } | ||
| 112 | 112 | ||||
| 113 | if sessionsJSON { | 113 | if sessionsJSON { | ||
M internal/cli/model/sessions.go +39 -11
| @@ -429,18 +429,23 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string { | |||
| 429 | 429 | return t.Subtle.Render(" No saved sessions found.") | |
| 430 | 430 | } | |
| 431 | 431 | ||
| 432 | - | // Determine starting index so the selected row stays visible. | |
| 433 | - | start := 0 | |
| 434 | - | if len(m.sessions) > maxHeight { | |
| 435 | - | // Keep selection roughly centered, but always visible. | |
| 436 | - | start = m.selectedIdx - (maxHeight / 2) | |
| 437 | - | if start < 0 { | |
| 438 | - | start = 0 | |
| 439 | - | } | |
| 440 | - | maxStart := len(m.sessions) - maxHeight | |
| 441 | - | if start > maxStart { | |
| 442 | - | start = maxStart | |
| 432 | + | // Determine starting index so the selected row stays visible even when | |
| 433 | + | // rows above it are expanded and consume multiple lines. | |
| 434 | + | start := m.selectedIdx | |
| 435 | + | if start < 0 { | |
| 436 | + | start = 0 | |
| 437 | + | } | |
| 438 | + | if start >= len(m.sessions) { | |
| 439 | + | start = len(m.sessions) - 1 | |
| 440 | + | } | |
| 441 | + | used := m.rowHeight(start) | |
| 442 | + | for start > 0 { | |
| 443 | + | prevHeight := m.rowHeight(start - 1) | |
| 444 | + | if used+prevHeight > maxHeight { | |
| 445 | + | break | |
| 443 | 446 | } | |
| 447 | + | start-- | |
| 448 | + | used += prevHeight | |
| 444 | 449 | } | |
| 445 | 450 | ||
| 446 | 451 | var b strings.Builder | |
| @@ -476,6 +481,29 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string { | |||
| 476 | 481 | return strings.TrimRight(b.String(), "\n") | |
| 477 | 482 | } | |
| 478 | 483 | ||
| 484 | + | func (m SessionsModel) rowHeight(idx int) int { | |
| 485 | + | if idx < 0 || idx >= len(m.sessions) { | |
| 486 | + | return 0 | |
| 487 | + | } | |
| 488 | + | ||
| 489 | + | height := 1 // always render one row line | |
| 490 | + | if idx != m.expandedIdx { | |
| 491 | + | return height | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | info := m.sessions[idx] | |
| 495 | + | if info.State == nil { | |
| 496 | + | return height | |
| 497 | + | } | |
| 498 | + | ||
| 499 | + | details := m.renderSessionDetails(info) | |
| 500 | + | if details == "" { | |
| 501 | + | return height | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | return height + lipgloss.Height(details) | |
| 505 | + | } | |
| 506 | + | ||
| 479 | 507 | func (m SessionsModel) renderSessionRow(info entity.SessionInfo, isSelected, isExpanded bool) string { | |
| 480 | 508 | t := m.theme | |
| 481 | 509 | ||
| @@ -429,18 +429,23 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string { | |||||
| 429 | return t.Subtle.Render(" No saved sessions found.") | 429 | return t.Subtle.Render(" No saved sessions found.") | ||
| 430 | } | 430 | } | ||
| 431 | 431 | ||||
| 432 | - | // Determine starting index so the selected row stays visible. | 432 | + | // Determine starting index so the selected row stays visible even when |
| 433 | - | start := 0 | 433 | + | // rows above it are expanded and consume multiple lines. |
| 434 | - | if len(m.sessions) > maxHeight { | 434 | + | start := m.selectedIdx |
| 435 | - | // Keep selection roughly centered, but always visible. | 435 | + | if start < 0 { |
| 436 | - | start = m.selectedIdx - (maxHeight / 2) | 436 | + | start = 0 |
| 437 | - | if start < 0 { | 437 | + | } |
| 438 | - | start = 0 | 438 | + | if start >= len(m.sessions) { |
| 439 | - | } | 439 | + | start = len(m.sessions) - 1 |
| 440 | - | maxStart := len(m.sessions) - maxHeight | 440 | + | } |
| 441 | - | if start > maxStart { | 441 | + | used := m.rowHeight(start) |
| 442 | - | start = maxStart | 442 | + | for start > 0 { |
| 443 | + | prevHeight := m.rowHeight(start - 1) | |||
| 444 | + | if used+prevHeight > maxHeight { | |||
| 445 | + | break | |||
| 443 | } | 446 | } | ||
| 447 | + | start-- | |||
| 448 | + | used += prevHeight | |||
| 444 | } | 449 | } | ||
| 445 | 450 | ||||
| 446 | var b strings.Builder | 451 | var b strings.Builder | ||
| @@ -476,6 +481,29 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string { | |||||
| 476 | return strings.TrimRight(b.String(), "\n") | 481 | return strings.TrimRight(b.String(), "\n") | ||
| 477 | } | 482 | } | ||
| 478 | 483 | ||||
| 484 | + | func (m SessionsModel) rowHeight(idx int) int { | |||
| 485 | + | if idx < 0 || idx >= len(m.sessions) { | |||
| 486 | + | return 0 | |||
| 487 | + | } | |||
| 488 | + | ||||
| 489 | + | height := 1 // always render one row line | |||
| 490 | + | if idx != m.expandedIdx { | |||
| 491 | + | return height | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | info := m.sessions[idx] | |||
| 495 | + | if info.State == nil { | |||
| 496 | + | return height | |||
| 497 | + | } | |||
| 498 | + | ||||
| 499 | + | details := m.renderSessionDetails(info) | |||
| 500 | + | if details == "" { | |||
| 501 | + | return height | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | return height + lipgloss.Height(details) | |||
| 505 | + | } | |||
| 506 | + | ||||
| 479 | func (m SessionsModel) renderSessionRow(info entity.SessionInfo, isSelected, isExpanded bool) string { | 507 | func (m SessionsModel) renderSessionRow(info entity.SessionInfo, isSelected, isExpanded bool) string { | ||
| 480 | t := m.theme | 508 | t := m.theme | ||
| 481 | 509 | ||||
A internal/cli/model/sessions_test.go +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | package model | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/dumber/internal/cli/styles" | |
| 12 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.T) { | |
| 17 | + | theme := styles.NewTheme(config.DefaultConfig()) | |
| 18 | + | m := NewSessionsModel(context.Background(), theme, SessionsModelConfig{}) | |
| 19 | + | ||
| 20 | + | m.sessions = make([]entity.SessionInfo, 0, 6) | |
| 21 | + | for i := 0; i < 6; i++ { | |
| 22 | + | m.sessions = append(m.sessions, testSessionInfo(fmt.Sprintf("session-%02d", i), 1)) | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // Expand a row above the selected row with many detail lines. | |
| 26 | + | m.sessions[2] = testSessionInfo("session-02", 6) | |
| 27 | + | m.selectedIdx = 4 | |
| 28 | + | m.expandedIdx = 2 | |
| 29 | + | ||
| 30 | + | view := m.renderSessionsList(5) | |
| 31 | + | require.Contains(t, view, "session-04", "selected row should remain visible") | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func testSessionInfo(id string, tabCount int) entity.SessionInfo { | |
| 35 | + | tabs := make([]entity.TabSnapshot, 0, tabCount) | |
| 36 | + | for i := 0; i < tabCount; i++ { | |
| 37 | + | tabs = append(tabs, entity.TabSnapshot{Name: fmt.Sprintf("Tab %d", i+1)}) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | return entity.SessionInfo{ | |
| 41 | + | Session: &entity.Session{ | |
| 42 | + | ID: entity.SessionID(id), | |
| 43 | + | Type: entity.SessionTypeBrowser, | |
| 44 | + | StartedAt: time.Now().Add(-time.Hour), | |
| 45 | + | }, | |
| 46 | + | State: &entity.SessionState{Tabs: tabs}, | |
| 47 | + | TabCount: tabCount, | |
| 48 | + | PaneCount: 0, | |
| 49 | + | UpdatedAt: time.Now(), | |
| 50 | + | } | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | package model | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/dumber/internal/cli/styles" | |||
| 12 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 13 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.T) { | |||
| 17 | + | theme := styles.NewTheme(config.DefaultConfig()) | |||
| 18 | + | m := NewSessionsModel(context.Background(), theme, SessionsModelConfig{}) | |||
| 19 | + | ||||
| 20 | + | m.sessions = make([]entity.SessionInfo, 0, 6) | |||
| 21 | + | for i := 0; i < 6; i++ { | |||
| 22 | + | m.sessions = append(m.sessions, testSessionInfo(fmt.Sprintf("session-%02d", i), 1)) | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // Expand a row above the selected row with many detail lines. | |||
| 26 | + | m.sessions[2] = testSessionInfo("session-02", 6) | |||
| 27 | + | m.selectedIdx = 4 | |||
| 28 | + | m.expandedIdx = 2 | |||
| 29 | + | ||||
| 30 | + | view := m.renderSessionsList(5) | |||
| 31 | + | require.Contains(t, view, "session-04", "selected row should remain visible") | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func testSessionInfo(id string, tabCount int) entity.SessionInfo { | |||
| 35 | + | tabs := make([]entity.TabSnapshot, 0, tabCount) | |||
| 36 | + | for i := 0; i < tabCount; i++ { | |||
| 37 | + | tabs = append(tabs, entity.TabSnapshot{Name: fmt.Sprintf("Tab %d", i+1)}) | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | return entity.SessionInfo{ | |||
| 41 | + | Session: &entity.Session{ | |||
| 42 | + | ID: entity.SessionID(id), | |||
| 43 | + | Type: entity.SessionTypeBrowser, | |||
| 44 | + | StartedAt: time.Now().Add(-time.Hour), | |||
| 45 | + | }, | |||
| 46 | + | State: &entity.SessionState{Tabs: tabs}, | |||
| 47 | + | TabCount: tabCount, | |||
| 48 | + | PaneCount: 0, | |||
| 49 | + | UpdatedAt: time.Now(), | |||
| 50 | + | } | |||
| 51 | + | } | |||