Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,16 @@ Every subsequent test run will compare the actual value against the inline snaps

`go-snaps` allows passing configuration for overriding

- the directory where snapshots are stored, _relative or absolute path_
- the filename where snapshots are stored
- the snapshot file's extension (_regardless the extension the filename will include the `.snaps` inside the filename_)
- programmatically control whether to update snapshots. _You can find an example usage at [examples](/examples/examples_test.go#13)_
- json config's json format configuration:
- the directory where snapshots are stored, _relative or absolute path_ `snaps.Dir("my_dir")`
- the filename where snapshots are stored `snaps.Filename("my_file")`
- the snapshot file's extension (_regardless the extension the filename will include the `.snaps` inside the filename_) `snaps.Ext(".json")`
- programmatically control whether to update snapshots. _You can find an example usage at [examples](/examples/examples_test.go#13)_ `snaps.Update(true)`
- json config's json format configuration: `snaps.JSON(snaps.JSONConfig{...})`
- `Width`: The maximum width in characters before wrapping json output (default: 80)
- `Indent`: The indentation string to use for nested structures (default: 1 spaces)
- `SortKeys`: Whether to sort json object keys alphabetically (default: true)
- a custom serializer function for non-structured snapshots `snaps.Serializer(func(any) string {...})`
- a helper serializer function `snaps.Raw()` that uses `fmt.Sprint` to serialize the value as is without any formatting or indentation.

```go
t.Run("snapshot tests", func(t *testing.T) {
Expand All @@ -335,6 +337,11 @@ t.Run("snapshot tests", func(t *testing.T) {
snaps.Filename("json_file"),
snaps.Ext(".json"),
snaps.Update(false),
snaps.Serializer(func(v any) string {
// custom serializer logic
return fmt.Sprintf("custom: %v", v)
}),
// or snaps.Raw() for no formatting
snaps.JSON(snaps.JSONConfig{
Width: 80,
Indent: " ",
Expand Down
128 changes: 128 additions & 0 deletions snaps/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package snaps

import (
"fmt"

"github.com/tidwall/pretty"
)

var defaultConfig = Config{
snapsDir: "__snapshots__",
}

type Config struct {
filename string
snapsDir string
extension string
update *bool
json *JSONConfig
serializer func(any) string
}

type JSONConfig struct {
// Width is a max column width for single line arrays
// Default: see defaultPrettyJSONOptions.Width for detail
Width int
// Indent is the nested indentation
// Default: see defaultPrettyJSONOptions.Indent for detail
Indent string
// SortKeys will sort the keys alphabetically
// Default: see defaultPrettyJSONOptions.SortKeys for detail
SortKeys bool
}

func (j *JSONConfig) getPrettyJSONOptions() *pretty.Options {
if j == nil {
return defaultPrettyJSONOptions
}
return &pretty.Options{
Width: j.Width,
Indent: j.Indent,
SortKeys: j.SortKeys,
}
}

// Update determines whether to update snapshots or not
//
// It respects if running on CI.
func Update(u bool) func(*Config) {
return func(c *Config) {
c.update = &u
}
}

// Specify a custom serializer function to convert the received value to a string before saving it in the snapshot file.
//
// Note: this is only used for non-structured snapshots e.g. MatchSnapshot, MatchStandaloneSnapshot, MatchInlineSnapshot.
func Serializer(s func(any) string) func(*Config) {
return func(c *Config) {
c.serializer = s
}
}

// Raw is a utility function for setting serializer to fmt.Sprint
//
// For more complex custom serialization logic, use snaps.Serializer instead of snaps.Raw
func Raw() func(*Config) {
return func(c *Config) {
c.serializer = func(v any) string {
return fmt.Sprint(v)
}
}
}

// Specify snapshot file name
//
// default: test's filename
//
// this doesn't change the file extension see `snap.Ext`
func Filename(name string) func(*Config) {
return func(c *Config) {
c.filename = name
}
}

// Specify folder name where snapshots are stored
//
// default: __snapshots__
//
// Accepts absolute paths
func Dir(dir string) func(*Config) {
return func(c *Config) {
c.snapsDir = dir
}
}

// Specify file name extension
//
// default: .snap
//
// Note: even if you specify a different extension the file still contain .snap
// e.g. if you specify .txt the file will be .snap.txt
func Ext(ext string) func(*Config) {
return func(c *Config) {
c.extension = ext
}
}

// Specify json format configuration
//
// default: see defaultPrettyJSONOptions for default json config
func JSON(json JSONConfig) func(*Config) {
return func(c *Config) {
c.json = &json
}
}

// Create snaps with configuration
//
// e.g snaps.WithConfig(snaps.Filename("my_test")).MatchSnapshot(t, "hello world")
func WithConfig(args ...func(*Config)) *Config {
s := defaultConfig

for _, arg := range args {
arg(&s)
}

return &s
}
165 changes: 165 additions & 0 deletions snaps/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package snaps

import (
"fmt"
"testing"

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

func TestWithConfig(t *testing.T) {
t.Run("returns default config when no options provided", func(t *testing.T) {
c := WithConfig()

test.Equal(t, "__snapshots__", c.snapsDir)
test.Equal(t, "", c.filename)
test.Equal(t, "", c.extension)
test.Nil(t, c.update)
test.Nil(t, c.json)
test.Nil(t, c.serializer)
})

t.Run("Filename", func(t *testing.T) {
c := WithConfig(Filename("my_test"))
test.Equal(t, "my_test", c.filename)
})

t.Run("Dir", func(t *testing.T) {
c := WithConfig(Dir("my_dir"))
test.Equal(t, "my_dir", c.snapsDir)
})

t.Run("Ext", func(t *testing.T) {
c := WithConfig(Ext(".txt"))
test.Equal(t, ".txt", c.extension)
})

t.Run("Update", func(t *testing.T) {
c := WithConfig(Update(true))
test.Equal(t, true, *c.update)

c = WithConfig(Update(false))
test.Equal(t, false, *c.update)
})

t.Run("JSON", func(t *testing.T) {
c := WithConfig(JSON(JSONConfig{SortKeys: true, Indent: " ", Width: 80}))
test.Equal(t, true, c.json.SortKeys)
test.Equal(t, " ", c.json.Indent)
test.Equal(t, 80, c.json.Width)
})

t.Run("Printer", func(t *testing.T) {
fn := func(v any) string { return fmt.Sprint(v) }
c := WithConfig(Serializer(fn))
test.Equal(t, "hello", c.serializer("hello"))
})

t.Run("multiple options are all applied", func(t *testing.T) {
c := WithConfig(Filename("my_test"), Dir("my_dir"), Ext(".txt"), Update(true))
test.Equal(t, "my_test", c.filename)
test.Equal(t, "my_dir", c.snapsDir)
test.Equal(t, ".txt", c.extension)
test.Equal(t, true, *c.update)
})

t.Run("does not mutate defaultConfig", func(t *testing.T) {
_ = WithConfig(Filename("my_test"), Dir("my_dir"), Update(true))

test.Equal(t, "__snapshots__", defaultConfig.snapsDir)
test.Equal(t, "", defaultConfig.filename)
test.Nil(t, defaultConfig.update)
test.Nil(t, defaultConfig.serializer)
})
}

func TestTakeSnapshot(t *testing.T) {
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
result := defaultConfig.takeSnapshot([]any{10, "hello world"})

test.Equal(t, "int(10)\nhello world", result)
})

t.Run("uses custom printer", func(t *testing.T) {
c := WithConfig(Serializer(func(v any) string {
return fmt.Sprintf("custom:%v", v)
}))

result := c.takeSnapshot([]any{"world"})

test.Equal(t, "custom:world", result)
})

t.Run("calls printer once per value", func(t *testing.T) {
calls := 0
c := WithConfig(Serializer(func(v any) string {
calls++
return fmt.Sprint(v)
}))

value := c.takeSnapshot([]any{"a", "b", "c"})

test.Equal(t, 3, calls)
test.Equal(t, "a\nb\nc", value)
})

t.Run("uses fmt.Sprint", func(t *testing.T) {
c := WithConfig(Raw())

result := c.takeSnapshot([]any{[]int{1, 2, 3}})

test.Equal(t, "[1 2 3]", result)
})
}

func TestTakeStandaloneSnapshot(t *testing.T) {
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
result := defaultConfig.takeStandaloneSnapshot("hello world")

test.Equal(t, "hello world", result)
})

t.Run("uses custom printer", func(t *testing.T) {
c := WithConfig(Serializer(func(v any) string {
return fmt.Sprintf("custom:%v", v)
}))

result := c.takeStandaloneSnapshot("world")

test.Equal(t, "custom:world", result)
})

t.Run("uses fmt.Sprint", func(t *testing.T) {
c := WithConfig(Raw())

result := c.takeStandaloneSnapshot([]int{1, 2, 3})

test.Equal(t, "[1 2 3]", result)
})
}

func TestTakeInlineSnapshot(t *testing.T) {
t.Run("falls back to pretty.Sprint when no printer set", func(t *testing.T) {
result := defaultConfig.takeInlineSnapshot("hello world")

test.Equal(t, "hello world", result)
})

t.Run("uses custom printer", func(t *testing.T) {
c := WithConfig(Serializer(func(v any) string {
return fmt.Sprintf("custom:%v", v)
}))

result := c.takeInlineSnapshot("world")

test.Equal(t, "custom:world", result)
})

t.Run("uses fmt.Sprint", func(t *testing.T) {
c := WithConfig(Raw())

result := c.takeInlineSnapshot([]int{1, 2, 3})

test.Equal(t, "[1 2 3]", result)
})
}
10 changes: 9 additions & 1 deletion snaps/matchInlineSnapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func MatchInlineSnapshot(t testingT, received any, inlineSnap inlineSnapshot) {

func matchInlineSnapshot(c *Config, t testingT, received any, inlineSnap inlineSnapshot) {
t.Helper()
snapshot := pretty.Sprint(received)
snapshot := c.takeInlineSnapshot(received)
filename, line := baseCaller(1)

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

func (c *Config) takeInlineSnapshot(received any) string {
if c.serializer != nil {
return c.serializer(received)
}

return pretty.Sprint(received)
}

// registerInlineCallIdx is expected to be called once per file and before getting modified
// it parses the file and registers all MatchInlineSnapshot call line numbers
func registerInlineCallIdx(filename string) error {
Expand Down
Loading