Approval-based snapshot testing library for Go (mirror)
at main 164 lines 5.2 kB view raw
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}