Approval-based snapshot testing library for Go (mirror)
1package pretty
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/ptdewey/shutter/internal/diff"
8 "github.com/ptdewey/shutter/internal/files"
9)
10
11func NewSnapshotBox(snap *files.Snapshot) string {
12 return newSnapshotBoxInternal(snap)
13}
14
15// calculateLineNumWidth returns the width needed to display line numbers
16func calculateLineNumWidth(maxLineNum int) int {
17 return len(fmt.Sprintf("%d", maxLineNum))
18}
19
20// formatColoredLine applies color to a line based on diff kind
21func formatColoredLine(line string, kind diff.DiffKind) string {
22 switch kind {
23 case diff.DiffOld:
24 return Red(line)
25 case diff.DiffNew:
26 return Green(line)
27 case diff.DiffShared:
28 return line
29 default:
30 return line
31 }
32}
33
34func DiffSnapshotBox(old, newSnapshot *files.Snapshot, diffLines []diff.DiffLine) string {
35 width := TerminalWidth()
36 snapshotFileName := files.SnapshotFileName(newSnapshot.Title) + ".snap"
37
38 var sb strings.Builder
39 sb.WriteString("─── " + "Snapshot Diff " + strings.Repeat("─", width-15) + "\n\n")
40
41 // TODO: maybe make helper functions for this, swap coloring between the key and the value
42 // TODO: maybe show the snapshot file name in gray next to the "a/r/s" options
43 // (i.e. "a accept -> snap_file_name.snap", "reject" w/strikethrough?, skip, keeps "*snap.new")
44 if newSnapshot.Title != "" {
45 sb.WriteString(Blue(" title: ") + newSnapshot.Title + "\n")
46 }
47 sb.WriteString(Blue(" test: ") + newSnapshot.Test + "\n")
48 sb.WriteString(Blue(" file: ") + snapshotFileName + "\n")
49 sb.WriteString("\n")
50 // sb.WriteString(Red(" - old snapshot\n"))
51 // sb.WriteString(Green(" + new snapshot\n"))
52 // sb.WriteString("\n")
53
54 // Calculate max line numbers for proper spacing
55 maxOldNum := 0
56 maxNewNum := 0
57 for _, dl := range diffLines {
58 if dl.OldNumber > maxOldNum {
59 maxOldNum = dl.OldNumber
60 }
61 if dl.NewNumber > maxNewNum {
62 maxNewNum = dl.NewNumber
63 }
64 }
65 // Use the larger of the two for consistent column width
66 maxLineNum := maxOldNum
67 if maxNewNum > maxLineNum {
68 maxLineNum = maxNewNum
69 }
70 lineNumWidth := calculateLineNumWidth(maxLineNum)
71
72 // Top bar with corner (account for both line number columns)
73 topBar := strings.Repeat("─", (lineNumWidth*2)+4) + "┬" +
74 strings.Repeat("─", width-(lineNumWidth*2)-1) + "\n"
75 sb.WriteString(topBar)
76
77 for _, dl := range diffLines {
78 var leftNum, rightNum, prefix, formatted string
79
80 // FIX: line number coloring is the same between old and new lines
81 switch dl.Kind {
82 case diff.DiffOld:
83 // For removed lines: show old line number on left, space on right, red -
84 leftNum = Red(fmt.Sprintf("%*d", lineNumWidth, dl.OldNumber))
85 rightNum = strings.Repeat(" ", lineNumWidth)
86 prefix = Red("-")
87 formatted = Red(dl.Line)
88 case diff.DiffNew:
89 // For added lines: space on left, new line number on right, green +
90 leftNum = strings.Repeat(" ", lineNumWidth)
91 rightNum = Green(fmt.Sprintf("%*d", lineNumWidth, dl.NewNumber))
92 prefix = Green("+")
93 formatted = Green(dl.Line)
94 case diff.DiffShared:
95 // For shared lines: show line number centered, │ separator (not gray)
96 leftNum = strings.Repeat(" ", lineNumWidth)
97 rightNum = Gray(fmt.Sprintf("%*d", lineNumWidth, dl.NewNumber))
98 prefix = "│"
99 formatted = dl.Line
100 }
101
102 // Adjust for actual display length considering ANSI codes
103 // Account for: 2 spaces padding + 2 line number columns + 2 spaces between + prefix + space
104 maxContentWidth := width - (lineNumWidth * 2) - 8
105 if len(dl.Line) > maxContentWidth {
106 truncated := dl.Line[:maxContentWidth-3] + "..."
107 formatted = formatColoredLine(truncated, dl.Kind)
108 }
109
110 display := fmt.Sprintf("%s %s %s %s", leftNum, rightNum, prefix, formatted)
111 sb.WriteString(fmt.Sprintf(" %s\n", display))
112 }
113
114 // Bottom bar with corner (account for both line number columns)
115 bottomBar := strings.Repeat("─", (lineNumWidth*2)+4) + "┴" +
116 strings.Repeat("─", width-(lineNumWidth*2)-1) + "\n"
117 sb.WriteString(bottomBar)
118
119 return sb.String()
120}
121
122func newSnapshotBoxInternal(snap *files.Snapshot) string {
123 width := TerminalWidth()
124
125 var sb strings.Builder
126 sb.WriteString("─── " + "New Snapshot " + strings.Repeat("─", width-15) + "\n\n")
127
128 if snap.Title != "" {
129 sb.WriteString(Blue(" title: ") + snap.Title + "\n")
130 }
131 if snap.Test != "" {
132 sb.WriteString(Blue(" test: ") + snap.Test + "\n")
133 }
134 if snap.FileName != "" {
135 sb.WriteString(Blue(" file: ") + snap.FileName + "\n")
136 }
137 sb.WriteString("\n")
138
139 lines := strings.Split(snap.Content, "\n")
140 numLines := len(lines)
141 lineNumWidth := calculateLineNumWidth(numLines)
142
143 topBar := strings.Repeat("─", lineNumWidth+3) + "┬" +
144 strings.Repeat("─", width-lineNumWidth-2) + "\n"
145 sb.WriteString(topBar)
146
147 for i, line := range lines {
148 lineNum := fmt.Sprintf("%*d", lineNumWidth, i+1)
149 prefix := fmt.Sprintf("%s %s", Green(lineNum), Green("+"))
150
151 if len(line) > width-len(prefix)-4 {
152 line = line[:width-len(prefix)-7] + "..."
153 }
154
155 display := fmt.Sprintf("%s %s", prefix, Green(line))
156 sb.WriteString(fmt.Sprintf(" %s\n", display))
157 }
158
159 bottomBar := strings.Repeat("─", lineNumWidth+3) + "┴" +
160 strings.Repeat("─", width-lineNumWidth-2) + "\n"
161 sb.WriteString(bottomBar)
162
163 return sb.String()
164}