-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathui_test.go
More file actions
410 lines (373 loc) · 12.8 KB
/
ui_test.go
File metadata and controls
410 lines (373 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
package home
import (
"strings"
"testing"
"time"
"github.com/slack-go/slack"
)
// TestBuildBlocks verifies home dashboard block generation.
//
//nolint:gocognit,maintidx // Comprehensive test with many test cases - complexity acceptable
func TestBuildBlocks(t *testing.T) {
tests := []struct {
name string
dashboard *Dashboard
validate func(t *testing.T, blocks []slack.Block)
}{
{
name: "empty dashboard",
dashboard: &Dashboard{
WorkspaceOrgs: []string{"test-org"},
IncomingPRs: []PR{},
OutgoingPRs: []PR{},
},
validate: func(t *testing.T, blocks []slack.Block) {
t.Helper()
if len(blocks) == 0 {
t.Fatal("expected non-empty blocks")
}
// Should have header
foundHeader := false
for _, block := range blocks {
if hb, ok := block.(*slack.HeaderBlock); ok {
if strings.Contains(hb.Text.Text, "Ready to Review") {
foundHeader = true
}
}
}
if !foundHeader {
t.Error("expected header block with 'Ready to Review'")
}
// Verify we don't have any PR section blocks (empty dashboard)
hasPRSections := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && (strings.Contains(sb.Text.Text, "Incoming") || strings.Contains(sb.Text.Text, "Outgoing")) {
hasPRSections = true
}
}
}
if hasPRSections {
t.Error("expected no PR sections for empty dashboard")
}
// Should have Organizations section
foundOrgs := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, "Organizations") {
foundOrgs = true
}
}
}
if !foundOrgs {
t.Error("expected Organizations section")
}
// Should have dashboard link in Organizations section
foundLink := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, "ready-to-review.dev") {
foundLink = true
}
}
}
if !foundLink {
t.Error("expected dashboard link in Organizations section")
}
// Should have Updated timestamp
foundTimestamp := false
for _, block := range blocks {
if cb, ok := block.(*slack.ContextBlock); ok {
for _, elem := range cb.ContextElements.Elements {
if txt, ok := elem.(*slack.TextBlockObject); ok {
if strings.Contains(txt.Text, "Updated:") {
foundTimestamp = true
}
}
}
}
}
if !foundTimestamp {
t.Error("expected Updated timestamp")
}
},
},
{
name: "dashboard with blocked incoming PRs",
dashboard: &Dashboard{
WorkspaceOrgs: []string{"test-org"},
IncomingPRs: []PR{
{
Number: 123,
Title: "Fix critical bug",
Author: "alice",
Repository: "test-org/repo",
URL: "https://github.com/test-org/repo/pull/123",
ActionKind: "review",
IsBlocked: true,
NeedsReview: true,
UpdatedAt: time.Now().Add(-2 * time.Hour),
},
},
OutgoingPRs: []PR{},
},
validate: func(t *testing.T, blocks []slack.Block) {
t.Helper()
// Should show "1 blocked on you" in section header
foundBlocked := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, "1 blocked on you") {
foundBlocked = true
}
}
}
if !foundBlocked {
t.Error("expected 'blocked on you' message in header")
}
// Should have PR with red circle (incoming blocked indicator)
foundBlockedPR := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, ":red_circle:") {
foundBlockedPR = true
}
}
}
if !foundBlockedPR {
t.Error("expected PR with :red_circle: indicating blocked incoming PR")
}
},
},
{
name: "dashboard with outgoing PRs",
dashboard: &Dashboard{
WorkspaceOrgs: []string{"test-org"},
IncomingPRs: []PR{},
OutgoingPRs: []PR{
{
Number: 456,
Title: "Add new feature",
Author: "me",
Repository: "test-org/repo",
URL: "https://github.com/test-org/repo/pull/456",
ActionKind: "address_feedback",
IsBlocked: true,
UpdatedAt: time.Now().Add(-1 * time.Hour),
},
},
},
validate: func(t *testing.T, blocks []slack.Block) {
t.Helper()
// Should show outgoing PR section with "blocked on you" (new format)
foundOutgoing := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, "Outgoing") {
foundOutgoing = true
// Should show "1 blocked on you" in the header
if !strings.Contains(sb.Text.Text, "1 blocked on you") {
t.Error("expected '1 blocked on you' in Outgoing section header")
}
}
}
}
if !foundOutgoing {
t.Error("expected 'Outgoing' section")
}
// Should have PR with large green square (outgoing blocked indicator)
foundBlocked := false
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, ":large_green_circle:") {
foundBlocked = true
}
}
}
if !foundBlocked {
t.Error("expected PR with :large_green_circle: for blocked outgoing PR")
}
},
},
{
name: "dashboard with multiple orgs",
dashboard: &Dashboard{
WorkspaceOrgs: []string{"org1", "org2", "org3"},
IncomingPRs: []PR{},
OutgoingPRs: []PR{},
},
validate: func(t *testing.T, blocks []slack.Block) {
t.Helper()
// Should list all orgs in Organizations section
foundOrgs := 0
for _, block := range blocks {
if sb, ok := block.(*slack.SectionBlock); ok {
if sb.Text != nil && strings.Contains(sb.Text.Text, "Organizations") {
if strings.Contains(sb.Text.Text, "org1") {
foundOrgs++
}
if strings.Contains(sb.Text.Text, "org2") {
foundOrgs++
}
if strings.Contains(sb.Text.Text, "org3") {
foundOrgs++
}
}
}
}
if foundOrgs < 3 {
t.Errorf("expected all 3 orgs in Organizations section, found %d", foundOrgs)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blocks := BuildBlocks(tt.dashboard)
tt.validate(t, blocks)
})
}
}
// TestBuildBlocks_RefreshButton verifies refresh button is present.
func TestBuildBlocks_RefreshButton(t *testing.T) {
dashboard := &Dashboard{
WorkspaceOrgs: []string{"test-org"},
IncomingPRs: []PR{},
OutgoingPRs: []PR{},
}
blocks := BuildBlocks(dashboard)
// Should have action block with refresh button
foundRefresh := false
for _, block := range blocks {
if ab, ok := block.(*slack.ActionBlock); ok {
for _, elem := range ab.Elements.ElementSet {
if btn, ok := elem.(*slack.ButtonBlockElement); ok {
if btn.ActionID == "refresh_dashboard" {
foundRefresh = true
// Verify it's styled as primary
if btn.Style != "primary" {
t.Error("expected refresh button to have primary style")
}
}
}
}
}
}
if !foundRefresh {
t.Error("expected refresh button in dashboard")
}
}
// TestBuildBlocks_DividersBetweenSections verifies sections are properly separated.
func TestBuildBlocks_DividersBetweenSections(t *testing.T) {
dashboard := &Dashboard{
WorkspaceOrgs: []string{"test-org"},
IncomingPRs: []PR{
{Number: 1, Title: "PR 1", Repository: "org/repo", URL: "https://github.com/org/repo/pull/1", UpdatedAt: time.Now()},
},
OutgoingPRs: []PR{
{Number: 2, Title: "PR 2", Repository: "org/repo", URL: "https://github.com/org/repo/pull/2", UpdatedAt: time.Now()},
},
}
blocks := BuildBlocks(dashboard)
// Count dividers
dividerCount := 0
for _, block := range blocks {
if _, ok := block.(*slack.DividerBlock); ok {
dividerCount++
}
}
// With new unified format: 2 dividers (after refresh button, before footer)
// PR sections flow together without dividers between them
if dividerCount < 2 {
t.Errorf("expected at least 2 dividers, got %d", dividerCount)
}
}
// TestBuildPRSections_SortOrder verifies blocked PRs appear first, then by recency.
func TestBuildPRSections_SortOrder(t *testing.T) {
baseTime := time.Now()
// Create incoming PRs with mixed blocked/unblocked and timestamps
incoming := []PR{
{Number: 1, Title: "Oldest non-blocked", Repository: "org/repo", URL: "https://github.com/org/repo/pull/1", UpdatedAt: baseTime.Add(-4 * time.Hour), NeedsReview: false},
{Number: 2, Title: "Newest blocked", Repository: "org/repo", URL: "https://github.com/org/repo/pull/2", UpdatedAt: baseTime.Add(-1 * time.Hour), NeedsReview: true},
{Number: 3, Title: "Middle non-blocked", Repository: "org/repo", URL: "https://github.com/org/repo/pull/3", UpdatedAt: baseTime.Add(-2 * time.Hour), NeedsReview: false},
{Number: 4, Title: "Oldest blocked", Repository: "org/repo", URL: "https://github.com/org/repo/pull/4", UpdatedAt: baseTime.Add(-5 * time.Hour), NeedsReview: true},
}
// Create outgoing PRs with mixed blocked/unblocked and timestamps
outgoing := []PR{
{Number: 5, Title: "Middle non-blocked out", Repository: "org/repo", URL: "https://github.com/org/repo/pull/5", UpdatedAt: baseTime.Add(-3 * time.Hour), IsBlocked: false},
{Number: 6, Title: "Newest blocked out", Repository: "org/repo", URL: "https://github.com/org/repo/pull/6", UpdatedAt: baseTime.Add(-1 * time.Hour), IsBlocked: true},
{Number: 7, Title: "Oldest blocked out", Repository: "org/repo", URL: "https://github.com/org/repo/pull/7", UpdatedAt: baseTime.Add(-6 * time.Hour), IsBlocked: true},
{Number: 8, Title: "Newest non-blocked out", Repository: "org/repo", URL: "https://github.com/org/repo/pull/8", UpdatedAt: baseTime.Add(-2 * time.Hour), IsBlocked: false},
}
blocks := BuildPRSections(incoming, outgoing)
// Should have 2 blocks (incoming and outgoing sections)
if len(blocks) != 2 {
t.Fatalf("expected 2 blocks, got %d", len(blocks))
}
// Check incoming section order
incomingBlock, ok := blocks[0].(*slack.SectionBlock)
if !ok {
t.Fatal("expected first block to be SectionBlock")
}
incomingText := incomingBlock.Text.Text
// Verify blocked PRs appear before non-blocked
// Expected order: PR#2 (newest blocked), PR#4 (oldest blocked), PR#3 (middle non-blocked), PR#1 (oldest non-blocked)
idx2 := strings.Index(incomingText, "repo#2")
idx4 := strings.Index(incomingText, "repo#4")
idx3 := strings.Index(incomingText, "repo#3")
idx1 := strings.Index(incomingText, "repo#1")
if idx2 < 0 || idx4 < 0 || idx3 < 0 || idx1 < 0 {
t.Fatal("not all incoming PRs found in output")
}
// Blocked PRs (2, 4) should come before non-blocked PRs (3, 1)
if idx2 > idx3 || idx2 > idx1 {
t.Error("blocked PR#2 should appear before non-blocked PRs")
}
if idx4 > idx3 || idx4 > idx1 {
t.Error("blocked PR#4 should appear before non-blocked PRs")
}
// Within blocked group: PR#2 (newer) should come before PR#4 (older)
if idx2 > idx4 {
t.Error("newer blocked PR#2 should appear before older blocked PR#4")
}
// Within non-blocked group: PR#3 (newer) should come before PR#1 (older)
if idx3 > idx1 {
t.Error("newer non-blocked PR#3 should appear before older non-blocked PR#1")
}
// Check outgoing section order
outgoingBlock, ok := blocks[1].(*slack.SectionBlock)
if !ok {
t.Fatal("expected second block to be SectionBlock")
}
outgoingText := outgoingBlock.Text.Text
// Expected order: PR#6 (newest blocked), PR#7 (oldest blocked), PR#8 (newest non-blocked), PR#5 (middle non-blocked)
idx6 := strings.Index(outgoingText, "repo#6")
idx7 := strings.Index(outgoingText, "repo#7")
idx8 := strings.Index(outgoingText, "repo#8")
idx5 := strings.Index(outgoingText, "repo#5")
if idx6 < 0 || idx7 < 0 || idx8 < 0 || idx5 < 0 {
t.Fatal("not all outgoing PRs found in output")
}
// Blocked PRs (6, 7) should come before non-blocked PRs (8, 5)
if idx6 > idx8 || idx6 > idx5 {
t.Error("blocked PR#6 should appear before non-blocked PRs")
}
if idx7 > idx8 || idx7 > idx5 {
t.Error("blocked PR#7 should appear before non-blocked PRs")
}
// Within blocked group: PR#6 (newer) should come before PR#7 (older)
if idx6 > idx7 {
t.Error("newer blocked PR#6 should appear before older blocked PR#7")
}
// Within non-blocked group: PR#8 (newer) should come before PR#5 (older)
if idx8 > idx5 {
t.Error("newer non-blocked PR#8 should appear before older non-blocked PR#5")
}
// Verify color indicators
if !strings.Contains(incomingText, ":red_circle:") {
t.Error("incoming blocked PRs should use :red_circle:")
}
if !strings.Contains(outgoingText, ":large_green_circle:") {
t.Error("outgoing blocked PRs should use :large_green_circle:")
}
}