Skip to content

Commit 3821513

Browse files
committed
feat: support setting printer on config
1 parent 1c47e5c commit 3821513

13 files changed

+427
-110
lines changed

snaps/config.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package snaps
2+
3+
import (
4+
"github.com/tidwall/pretty"
5+
)
6+
7+
var defaultConfig = Config{
8+
snapsDir: "__snapshots__",
9+
}
10+
11+
type Config struct {
12+
filename string
13+
snapsDir string
14+
extension string
15+
update *bool
16+
json *JSONConfig
17+
printer func(any) string
18+
}
19+
20+
type JSONConfig struct {
21+
// Width is a max column width for single line arrays
22+
// Default: see defaultPrettyJSONOptions.Width for detail
23+
Width int
24+
// Indent is the nested indentation
25+
// Default: see defaultPrettyJSONOptions.Indent for detail
26+
Indent string
27+
// SortKeys will sort the keys alphabetically
28+
// Default: see defaultPrettyJSONOptions.SortKeys for detail
29+
SortKeys bool
30+
}
31+
32+
func (j *JSONConfig) getPrettyJSONOptions() *pretty.Options {
33+
if j == nil {
34+
return defaultPrettyJSONOptions
35+
}
36+
return &pretty.Options{
37+
Width: j.Width,
38+
Indent: j.Indent,
39+
SortKeys: j.SortKeys,
40+
}
41+
}
42+
43+
// Update determines whether to update snapshots or not
44+
//
45+
// It respects if running on CI.
46+
func Update(u bool) func(*Config) {
47+
return func(c *Config) {
48+
c.update = &u
49+
}
50+
}
51+
52+
// Specify a custom printer function to convert the received value to a string before saving it in the snapshot file.
53+
//
54+
// Note: this is only used for non-structured snapshots e.g. MatchSnapshot, MatchStandaloneSnapshot, MatchInlineSnapshot.
55+
func Printer(p func(any) string) func(*Config) {
56+
return func(c *Config) {
57+
c.printer = p
58+
}
59+
}
60+
61+
// Specify snapshot file name
62+
//
63+
// default: test's filename
64+
//
65+
// this doesn't change the file extension see `snap.Ext`
66+
func Filename(name string) func(*Config) {
67+
return func(c *Config) {
68+
c.filename = name
69+
}
70+
}
71+
72+
// Specify folder name where snapshots are stored
73+
//
74+
// default: __snapshots__
75+
//
76+
// Accepts absolute paths
77+
func Dir(dir string) func(*Config) {
78+
return func(c *Config) {
79+
c.snapsDir = dir
80+
}
81+
}
82+
83+
// Specify file name extension
84+
//
85+
// default: .snap
86+
//
87+
// Note: even if you specify a different extension the file still contain .snap
88+
// e.g. if you specify .txt the file will be .snap.txt
89+
func Ext(ext string) func(*Config) {
90+
return func(c *Config) {
91+
c.extension = ext
92+
}
93+
}
94+
95+
// Specify json format configuration
96+
//
97+
// default: see defaultPrettyJSONOptions for default json config
98+
func JSON(json JSONConfig) func(*Config) {
99+
return func(c *Config) {
100+
c.json = &json
101+
}
102+
}
103+
104+
// Create snaps with configuration
105+
//
106+
// e.g snaps.WithConfig(snaps.Filename("my_test")).MatchSnapshot(t, "hello world")
107+
func WithConfig(args ...func(*Config)) *Config {
108+
s := defaultConfig
109+
110+
for _, arg := range args {
111+
arg(&s)
112+
}
113+
114+
return &s
115+
}

snaps/config_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package snaps
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/gkampitakis/go-snaps/internal/test"
8+
)
9+
10+
func TestWithConfig(t *testing.T) {
11+
t.Run("returns default config when no options provided", func(t *testing.T) {
12+
c := WithConfig()
13+
14+
test.Equal(t, "__snapshots__", c.snapsDir)
15+
test.Equal(t, "", c.filename)
16+
test.Equal(t, "", c.extension)
17+
test.Nil(t, c.update)
18+
test.Nil(t, c.json)
19+
test.Nil(t, c.printer)
20+
})
21+
22+
t.Run("Filename", func(t *testing.T) {
23+
c := WithConfig(Filename("my_test"))
24+
test.Equal(t, "my_test", c.filename)
25+
})
26+
27+
t.Run("Dir", func(t *testing.T) {
28+
c := WithConfig(Dir("my_dir"))
29+
test.Equal(t, "my_dir", c.snapsDir)
30+
})
31+
32+
t.Run("Ext", func(t *testing.T) {
33+
c := WithConfig(Ext(".txt"))
34+
test.Equal(t, ".txt", c.extension)
35+
})
36+
37+
t.Run("Update", func(t *testing.T) {
38+
c := WithConfig(Update(true))
39+
test.Equal(t, true, *c.update)
40+
41+
c = WithConfig(Update(false))
42+
test.Equal(t, false, *c.update)
43+
})
44+
45+
t.Run("JSON", func(t *testing.T) {
46+
c := WithConfig(JSON(JSONConfig{SortKeys: true, Indent: " ", Width: 80}))
47+
test.Equal(t, true, c.json.SortKeys)
48+
test.Equal(t, " ", c.json.Indent)
49+
test.Equal(t, 80, c.json.Width)
50+
})
51+
52+
t.Run("Printer", func(t *testing.T) {
53+
fn := func(v any) string { return fmt.Sprint(v) }
54+
c := WithConfig(Printer(fn))
55+
test.Equal(t, "hello", c.printer("hello"))
56+
})
57+
58+
t.Run("multiple options are all applied", func(t *testing.T) {
59+
c := WithConfig(Filename("my_test"), Dir("my_dir"), Ext(".txt"), Update(true))
60+
test.Equal(t, "my_test", c.filename)
61+
test.Equal(t, "my_dir", c.snapsDir)
62+
test.Equal(t, ".txt", c.extension)
63+
test.Equal(t, true, *c.update)
64+
})
65+
66+
t.Run("does not mutate defaultConfig", func(t *testing.T) {
67+
_ = WithConfig(Filename("my_test"), Dir("my_dir"), Update(true))
68+
69+
test.Equal(t, "__snapshots__", defaultConfig.snapsDir)
70+
test.Equal(t, "", defaultConfig.filename)
71+
test.Nil(t, defaultConfig.update)
72+
test.Nil(t, defaultConfig.printer)
73+
})
74+
}
75+
76+
func TestTakeSnapshot(t *testing.T) {
77+
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
78+
result := defaultConfig.takeSnapshot([]any{10, "hello world"})
79+
80+
test.Equal(t, "int(10)\nhello world", result)
81+
})
82+
83+
t.Run("uses custom printer", func(t *testing.T) {
84+
c := WithConfig(Printer(func(v any) string {
85+
return fmt.Sprintf("custom:%v", v)
86+
}))
87+
88+
result := c.takeSnapshot([]any{"world"})
89+
90+
test.Equal(t, "custom:world", result)
91+
})
92+
93+
t.Run("calls printer once per value", func(t *testing.T) {
94+
calls := 0
95+
c := WithConfig(Printer(func(v any) string {
96+
calls++
97+
return fmt.Sprint(v)
98+
}))
99+
100+
value := c.takeSnapshot([]any{"a", "b", "c"})
101+
102+
test.Equal(t, 3, calls)
103+
test.Equal(t, "a\nb\nc", value)
104+
})
105+
}
106+
107+
func TestTakeStandaloneSnapshot(t *testing.T) {
108+
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
109+
result := defaultConfig.takeStandaloneSnapshot("hello world")
110+
111+
test.Equal(t, "hello world", result)
112+
})
113+
114+
t.Run("uses custom printer", func(t *testing.T) {
115+
c := WithConfig(Printer(func(v any) string {
116+
return fmt.Sprintf("custom:%v", v)
117+
}))
118+
119+
result := c.takeStandaloneSnapshot("world")
120+
121+
test.Equal(t, "custom:world", result)
122+
})
123+
}
124+
125+
func TestTakeInlineSnapshot(t *testing.T) {
126+
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
127+
result := defaultConfig.takeInlineSnapshot("hello world")
128+
129+
test.Equal(t, "hello world", result)
130+
})
131+
132+
t.Run("uses custom printer", func(t *testing.T) {
133+
c := WithConfig(Printer(func(v any) string {
134+
return fmt.Sprintf("custom:%v", v)
135+
}))
136+
137+
result := c.takeInlineSnapshot("world")
138+
139+
test.Equal(t, "custom:world", result)
140+
})
141+
}

snaps/matchInlineSnapshot.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func MatchInlineSnapshot(t testingT, received any, inlineSnap inlineSnapshot) {
111111

112112
func matchInlineSnapshot(c *Config, t testingT, received any, inlineSnap inlineSnapshot) {
113113
t.Helper()
114-
snapshot := pretty.Sprint(received)
114+
snapshot := c.takeInlineSnapshot(received)
115115
filename, line := baseCaller(1)
116116

117117
// we should only register call positions if we are modifying the file and the file hasn't been registered yet.
@@ -208,6 +208,14 @@ func upsertInlineSnapshot(filename string, callerLine int, snapshot string) erro
208208
return nil
209209
}
210210

211+
func (c *Config) takeInlineSnapshot(received any) string {
212+
if c.printer != nil {
213+
return c.printer(received)
214+
}
215+
216+
return pretty.Sprint(received)
217+
}
218+
211219
// registerInlineCallIdx is expected to be called once per file and before getting modified
212220
// it parses the file and registers all MatchInlineSnapshot call line numbers
213221
func registerInlineCallIdx(filename string) error {

snaps/matchInlineSnapshot_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,49 @@
11
package snaps
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/gkampitakis/go-snaps/internal/test"
78
)
89

910
func TestMatchInlineSnapshot(t *testing.T) {
11+
t.Run("printer", func(t *testing.T) {
12+
t.Run("should use default printer", func(t *testing.T) {
13+
testEvents = newTestEvents()
14+
mockT := test.NewMockTestingT(t)
15+
16+
MatchInlineSnapshot(mockT, []string{"1", "2", "3"}, Inline(`[]string{"1", "2", "3"}`))
17+
18+
test.Equal(t, 1, testEvents.items[passed])
19+
})
20+
21+
t.Run("should pass when printer output matches inline snapshot", func(t *testing.T) {
22+
testEvents = newTestEvents()
23+
mockT := test.NewMockTestingT(t)
24+
25+
WithConfig(Printer(func(v any) string {
26+
return fmt.Sprintf("serialized:%v", v)
27+
})).MatchInlineSnapshot(mockT, "hello world", Inline("serialized:hello world"))
28+
29+
test.Equal(t, 1, testEvents.items[passed])
30+
})
31+
32+
t.Run("should fail when printer output does not match inline snapshot", func(t *testing.T) {
33+
testEvents = newTestEvents()
34+
mockT := test.NewMockTestingT(t)
35+
mockT.MockError = func(args ...any) {}
36+
37+
WithConfig(Printer(func(v any) string {
38+
return fmt.Sprintf("serialized:%v", v)
39+
})).MatchInlineSnapshot(mockT, "hello world", Inline("hello world"))
40+
41+
test.Equal(t, 1, testEvents.items[erred])
42+
})
43+
})
44+
1045
t.Run("should error in case of different input from inline snapshot", func(t *testing.T) {
46+
testEvents = newTestEvents()
1147
mockT := test.NewMockTestingT(t)
1248

1349
mockT.MockError = func(a ...any) {

snaps/matchJSON.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func matchJSON(c *Config, t testingT, input any, matchers ...match.JSONMatcher)
9797
return
9898
}
9999

100-
snapshot := takeJSONSnapshot(c, j)
100+
snapshot := c.takeJSONSnapshot(j)
101101
prevSnapshot, line, err := getPrevSnapshot(testID, snapPath)
102102
if errors.Is(err, errSnapNotFound) {
103103
if !shouldCreate(c.update) {
@@ -159,7 +159,7 @@ func validateJSON(input any) ([]byte, error) {
159159
}
160160
}
161161

162-
func takeJSONSnapshot(c *Config, b []byte) string {
162+
func (c *Config) takeJSONSnapshot(b []byte) string {
163163
return strings.TrimSuffix(string(pretty.PrettyOptions(b, c.json.getPrettyJSONOptions())), "\n")
164164
}
165165

snaps/matchSnapshot.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func matchSnapshot(c *Config, t testingT, values ...any) {
6060
testsRegistry.reset(snapPath, t.Name())
6161
})
6262

63-
snapshot := takeSnapshot(values)
63+
snapshot := c.takeSnapshot(values)
6464
prevSnapshot, line, err := getPrevSnapshot(testID, snapPath)
6565
if errors.Is(err, errSnapNotFound) {
6666
if !shouldCreate(c.update) {
@@ -108,11 +108,15 @@ func matchSnapshot(c *Config, t testingT, values ...any) {
108108
testEvents.register(updated)
109109
}
110110

111-
func takeSnapshot(objects []any) string {
111+
func (c *Config) takeSnapshot(objects []any) string {
112112
snapshots := make([]string, len(objects))
113113

114114
for i, object := range objects {
115-
snapshots[i] = pretty.Sprint(object)
115+
if c.printer != nil {
116+
snapshots[i] = c.printer(object)
117+
} else {
118+
snapshots[i] = pretty.Sprint(object)
119+
}
116120
}
117121

118122
return escapeEndChars(strings.Join(snapshots, "\n"))

0 commit comments

Comments
 (0)