package tui import ( "strings" "testing" "charm.land/lipgloss/v2" "superwork-tui/internal/issue" ) // TestBoardView_NeverExceedsTerminalHeight reproduces the interactive overflow // bug: with double-width CJK titles, cards wrap inside the fixed-width column box // to more rows than windowCards budgeted (it measured pre-wrap heights), so the // board grew taller than the terminal and the altscreen renderer pushed headers // and short columns off the top. The board must always fit m.height. func TestBoardView_NeverExceedsTerminalHeight(t *testing.T) { mk := func(n int, col issue.Column, title string) issue.Issue { return issue.Issue{Number: n, Title: title, Column: col, PR: "240", Branch: "feature/abc"} } var issues []issue.Issue for i := 0; i < 11; i++ { issues = append(issues, mk(238-i, issue.ColumnTodo, "支持部署时使用客户自定义域名并自动签发证书与续期")) } for i := 0; i < 4; i++ { issues = append(issues, mk(239-i, issue.ColumnReview, "分销裂变系统V1:一级分销 + 邀请注册归因 + 订单返佣 + T+30 结算")) } for i := 0; i < 101; i++ { issues = append(issues, mk(226-i, issue.ColumnDone, "充值页面前端完全重构为电商风格(含 PC/移动端专用 UI)")) } for _, dims := range [][2]int{{250, 55}, {200, 50}, {180, 40}, {160, 30}} { for _, colIdx := range []int{0, 3} { m := Model{ state: stateLoaded, issues: issues, buckets: bucketize(issues), width: dims[0], height: dims[1], colIdx: colIdx, statusMsg: "webhook:端口 17421 已被占用", hasChanges: true, } out := m.boardView() if h := lipgloss.Height(out); h > m.height { t.Errorf("dims=%v colIdx=%d: boardView height %d exceeds terminal height %d", dims, colIdx, h, m.height) } } } } func TestBoardView_ShowsAllNonEmptyColumns(t *testing.T) { var issues []issue.Issue for i := 0; i < 11; i++ { issues = append(issues, issue.Issue{Number: 1000 + i, Title: "TODOCARD", Column: issue.ColumnTodo}) } for i := 0; i < 4; i++ { issues = append(issues, issue.Issue{Number: 2000 + i, Title: "REVIEWCARD", Column: issue.ColumnReview}) } for i := 0; i < 101; i++ { issues = append(issues, issue.Issue{Number: 3000 + i, Title: "DONECARD", Column: issue.ColumnDone}) } m := Model{ state: stateLoaded, issues: issues, buckets: bucketize(issues), width: 200, height: 50, } out := m.boardView() for _, want := range []string{"TODOCARD", "REVIEWCARD", "DONECARD"} { if !strings.Contains(out, want) { t.Errorf("%s missing from boardView", want) } } } // Realistic: long wrapping Chinese titles, badges, screenshot-like dimensions, // and selection on the done column (rightmost). Reproduces the real board shape. func TestBoardView_RealisticDistribution(t *testing.T) { mk := func(n int, col issue.Column, title string) issue.Issue { return issue.Issue{ Number: n, Title: title, Column: col, SpecFile: "docs/superpowers/specs/x/spec.md", PlanFile: "docs/superpowers/plans/x/plan.md", PR: "240", Branch: "feature/abc123", } } var issues []issue.Issue issues = append(issues, mk(238, issue.ColumnTodo, "TODOMARK 支持部署时使用客户自定义域名")) for i := 0; i < 10; i++ { issues = append(issues, mk(220-i, issue.ColumnTodo, "✨ 裂变机制:邀请拉新双向激励(邀请码 + token credit 返利)")) } issues = append(issues, mk(239, issue.ColumnReview, "REVIEWMARK 分销裂变系统V1")) for i := 0; i < 3; i++ { issues = append(issues, mk(232-i, issue.ColumnReview, "✨ 实现 Claude Code /rewind 效果:会话与代码/沙箱状态回退")) } for i := 0; i < 101; i++ { issues = append(issues, mk(222-i, issue.ColumnDone, "DONEMARK 用户内容免责协议")) } for _, dims := range [][2]int{{250, 55}, {200, 50}, {180, 40}} { for _, colIdx := range []int{0, 3} { m := Model{ state: stateLoaded, issues: issues, buckets: bucketize(issues), width: dims[0], height: dims[1], colIdx: colIdx, } out := m.boardView() for _, want := range []string{"TODOMARK", "REVIEWMARK", "DONEMARK"} { if !strings.Contains(out, want) { t.Errorf("dims=%v colIdx=%d: %q missing from boardView", dims, colIdx, want) } } } } }