···11-# Freeze
11+# shutter
2233A [birdie](https://github.com/giacomocavalieri/birdie) and [insta](https://github.com/mitsuhiko/insta) inspired snapshot testing library for Go.
44···21212222func TestSomething(t *testing.T) {
2323 result := SomeFunction("foo")
2424- shutter.Snap(t, result)
2424+ shutter.Snap(t, "test title", result)
2525}
2626```
27272828### Advanced Usage: Scrubbers and Ignore Patterns
29293030-Freeze supports data scrubbing and field filtering to handle dynamic or sensitive data in snapshots.
3030+shutter supports data scrubbing and field filtering to handle dynamic or sensitive data in snapshots.
31313232#### Scrubbers
3333···3636```go
3737func TestUserAPI(t *testing.T) {
3838 user := api.GetUser("123")
3939-3939+4040 // Replace UUIDs and timestamps with placeholders
4141- shutter.SnapWithOptions(t, "user", []shutter.SnapshotOption{
4141+ shutter.Snap(t, "user", user,
4242 shutter.ScrubUUIDs(),
4343 shutter.ScrubTimestamps(),
4444- }, user)
4444+ )
4545}
4646```
47474848**Built-in Scrubbers:**
4949+4950- `ScrubUUIDs()` - Replaces UUIDs with `<UUID>`
5051- `ScrubTimestamps()` - Replaces ISO8601 timestamps with `<TIMESTAMP>`
5152- `ScrubEmails()` - Replaces email addresses with `<EMAIL>`
···7879```go
7980func TestAPIResponse(t *testing.T) {
8081 response := api.GetData()
8181-8282+8283 // Ignore sensitive fields and null values
8383- shutter.SnapJSONWithOptions(t, "response", response, []shutter.SnapshotOption{
8484+ shutter.SnapJSON(t, "response", response,
8485 shutter.IgnoreSensitiveKeys(),
8586 shutter.IgnoreNullValues(),
8687 shutter.IgnoreKeys("created_at", "updated_at"),
8787- })
8888+ )
8889}
8990```
90919192**Built-in Ignore Patterns:**
9393+9294- `IgnoreSensitiveKeys()` - Ignores common sensitive keys (password, token, api_key, etc.)
9395- `IgnoreEmptyValues()` - Ignores fields with empty string values
9496- `IgnoreNullValues()` - Ignores fields with null values
···121123```go
122124func TestComplexData(t *testing.T) {
123125 data := generateTestData()
124124-125125- shutter.SnapWithOptions(t, "data", []shutter.SnapshotOption{
126126+127127+ shutter.Snap(t, "data", data,
126128 // Scrubbers
127129 shutter.ScrubUUIDs(),
128130 shutter.ScrubTimestamps(),
129131 shutter.ScrubEmails(),
130130-132132+131133 // Ignore patterns
132134 shutter.IgnoreSensitiveKeys(),
133135 shutter.IgnoreKeys("debug_info"),
134136 shutter.IgnoreNullValues(),
135135- }, data)
137137+ )
136138}
137139```
138140139141#### API Reference
140142141141-Three snapshot functions support options:
143143+All snapshot functions support options as variadic parameters:
142144143145```go
144146// For general values (structs, maps, slices, etc.)
145145-shutter.SnapWithOptions(t, "title", []shutter.SnapshotOption{...}, value)
147147+shutter.Snap(t, "title", value, options...)
146148147149// For JSON strings
148148-shutter.SnapJSONWithOptions(t, "title", jsonString, []shutter.SnapshotOption{...})
150150+shutter.SnapJSON(t, "title", jsonString, options...)
149151150152// For plain strings
151151-shutter.SnapStringWithOptions(t, "title", content, []shutter.SnapshotOption{...})
153153+shutter.SnapString(t, "title", content, options...)
152154```
153155154156### Reviewing Snapshots
···159161go run github.com/ptdewey/shutter/cmd/shutter review
160162```
161163162162-Freeze can also be used programmatically:
164164+shutter can also be used programmatically:
163165164166```go
165167// Example: tools/shutter/main.go
···179181go run tools/shutter/main.go
180182```
181183182182-Freeze also includes (in a separate Go module) a [Bubbletea](https://github.com/charmbracelet/bubbletea) TUI in [cmd/tui/main.go](./cmd/tui/main.go).
184184+shutter also includes (in a separate Go module) a [Bubbletea](https://github.com/charmbracelet/bubbletea) TUI in [cmd/tui/main.go](./cmd/tui/main.go).
183185(The TUI is shipped in a separate module to make the added dependencies optional)
184186185187### TUI Usage
···215217## Other Libraries
216218217219- [go-snaps](https://github.com/gkampitakis/go-snaps)
218218- - Freeze uses the diff implementation from `go-snaps`.
220220+ - shutter uses the diff implementation from `go-snaps`.
219221- [cupaloy](https://github.com/bradleyjkemp/cupaloy)
-58
api.go
···11-package shutter
22-33-import (
44- "github.com/ptdewey/shutter/internal/diff"
55- "github.com/ptdewey/shutter/internal/files"
66- "github.com/ptdewey/shutter/internal/pretty"
77-)
88-99-// Snapshot represents a captured test snapshot with metadata.
1010-type Snapshot = files.Snapshot
1111-1212-// DiffLine represents a line in a diff comparison.
1313-type DiffLine = diff.DiffLine
1414-1515-const (
1616- // DiffShared indicates a line that is unchanged in both versions.
1717- DiffShared = diff.DiffShared
1818- // DiffOld indicates a line that was removed.
1919- DiffOld = diff.DiffOld
2020- // DiffNew indicates a line that was added.
2121- DiffNew = diff.DiffNew
2222-)
2323-2424-// Deserialize parses a raw snapshot file string into a Snapshot struct.
2525-func Deserialize(raw string) (*Snapshot, error) {
2626- return files.Deserialize(raw)
2727-}
2828-2929-// SaveSnapshot writes a snapshot to disk with the specified state ("new" or "accepted").
3030-func SaveSnapshot(snap *Snapshot, state string) error {
3131- return files.SaveSnapshot(snap, state)
3232-}
3333-3434-// ReadSnapshot reads a snapshot from disk for the given test name and state.
3535-func ReadSnapshot(testName string, state string) (*Snapshot, error) {
3636- return files.ReadSnapshot(testName, state)
3737-}
3838-3939-// SnapshotFileName returns the snapshot file name for a given test name.
4040-func SnapshotFileName(testName string) string {
4141- return files.SnapshotFileName(testName)
4242-}
4343-4444-// Histogram computes a line-by-line diff between two strings using the histogram algorithm.
4545-func Histogram(old, new string) []DiffLine {
4646- return diff.Histogram(old, new)
4747-}
4848-4949-// NewSnapshotBox formats a new snapshot as a pretty-printed box for display.
5050-func NewSnapshotBox(snap *Snapshot) string {
5151- return pretty.NewSnapshotBox(snap)
5252-}
5353-5454-// DiffSnapshotBox formats a diff between old and new snapshots as a pretty-printed box.
5555-func DiffSnapshotBox(oldSnap, newSnap *Snapshot) string {
5656- diffLines := diff.Histogram(oldSnap.Content, newSnap.Content)
5757- return pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines)
5858-}
-36
config.go
···11-package shutter
22-33-// SnapshotOption is a function that configures a SnapshotConfig.
44-type SnapshotOption func(*SnapshotConfig)
55-66-// SnapshotConfig holds configuration for snapshot scrubbing and filtering.
77-type SnapshotConfig struct {
88- Scrubbers []Scrubber
99- Ignore []IgnorePattern
1010-}
1111-1212-// newSnapshotConfig creates a new SnapshotConfig with the given options applied.
1313-func newSnapshotConfig(opts []SnapshotOption) *SnapshotConfig {
1414- config := &SnapshotConfig{
1515- Scrubbers: []Scrubber{},
1616- Ignore: []IgnorePattern{},
1717- }
1818- for _, opt := range opts {
1919- opt(config)
2020- }
2121- return config
2222-}
2323-2424-// WithScrubber adds a custom scrubber to the configuration.
2525-func WithScrubber(scrubber Scrubber) SnapshotOption {
2626- return func(c *SnapshotConfig) {
2727- c.Scrubbers = append(c.Scrubbers, scrubber)
2828- }
2929-}
3030-3131-// WithIgnorePattern adds a custom ignore pattern to the configuration.
3232-func WithIgnorePattern(pattern IgnorePattern) SnapshotOption {
3333- return func(c *SnapshotConfig) {
3434- c.Ignore = append(c.Ignore, pattern)
3535- }
3636-}
+39-20
ignore.go
···66 "strings"
77)
8899-// IgnorePattern determines whether a key-value pair should be excluded
1010-// from the snapshot. This is primarily used for JSON and map structures.
1111-type IgnorePattern interface {
1212- ShouldIgnore(key, value string) bool
1313-}
1414-159// exactKeyValueIgnore ignores exact key-value matches.
1610type exactKeyValueIgnore struct {
1711 key string
···2216 return e.key == key && (e.value == "*" || e.value == value)
2317}
24181919+func (e *exactKeyValueIgnore) Apply(content string) string {
2020+ // Ignore patterns are applied during JSON transformation, not string scrubbing
2121+ return content
2222+}
2323+2524// IgnoreKeyValue creates an ignore pattern that matches exact key-value pairs.
2625// Use "*" as the value to ignore any value for the given key.
2726func IgnoreKeyValue(key, value string) SnapshotOption {
2828- return WithIgnorePattern(&exactKeyValueIgnore{
2727+ return &exactKeyValueIgnore{
2928 key: key,
3029 value: value,
3131- })
3030+ }
3231}
33323433// regexKeyValueIgnore ignores key-value pairs matching regex patterns.
···4342 return keyMatch && valueMatch
4443}
45444545+func (r *regexKeyValueIgnore) Apply(content string) string {
4646+ return content
4747+}
4848+4649// IgnoreKeyPattern creates an ignore pattern using regex patterns for keys and values.
4750// Pass empty string for keyPattern or valuePattern to match any key or value.
4851func IgnoreKeyPattern(keyPattern, valuePattern string) SnapshotOption {
···5356 if valuePattern != "" {
5457 valueRe = regexp.MustCompile(valuePattern)
5558 }
5656- return WithIgnorePattern(®exKeyValueIgnore{
5959+ return ®exKeyValueIgnore{
5760 keyPattern: keyRe,
5861 valuePattern: valueRe,
5959- })
6262+ }
6063}
61646265// keyOnlyIgnore ignores any key matching the pattern, regardless of value.
···6871 return slices.Contains(k.keys, key)
6972}
70737474+func (k *keyOnlyIgnore) Apply(content string) string {
7575+ return content
7676+}
7777+7178// IgnoreKeys creates an ignore pattern that ignores the specified keys
7279// regardless of their values.
7380func IgnoreKeys(keys ...string) SnapshotOption {
7474- return WithIgnorePattern(&keyOnlyIgnore{
8181+ return &keyOnlyIgnore{
7582 keys: keys,
7676- })
8383+ }
7784}
78857986// regexKeyIgnore ignores keys matching a regex pattern.
···8592 return r.pattern.MatchString(key)
8693}
87949595+func (r *regexKeyIgnore) Apply(content string) string {
9696+ return content
9797+}
9898+8899// IgnoreKeysMatching creates an ignore pattern that ignores keys matching
89100// the given regex pattern.
90101func IgnoreKeysMatching(pattern string) SnapshotOption {
91102 re := regexp.MustCompile(pattern)
9292- return WithIgnorePattern(®exKeyIgnore{
103103+ return ®exKeyIgnore{
93104 pattern: re,
9494- })
105105+ }
95106}
9610797108// Common ignore patterns for sensitive data
···103114104115// IgnoreSensitiveKeys ignores common sensitive key names like password, token, etc.
105116func IgnoreSensitiveKeys() SnapshotOption {
106106- return WithIgnorePattern(&keyOnlyIgnore{
117117+ return &keyOnlyIgnore{
107118 keys: sensitiveKeys,
108108- })
119119+ }
109120}
110121111122// valueOnlyIgnore ignores any value matching the pattern, regardless of key.
···117128 return slices.Contains(v.values, value)
118129}
119130131131+func (v *valueOnlyIgnore) Apply(content string) string {
132132+ return content
133133+}
134134+120135// IgnoreValues creates an ignore pattern that ignores the specified values
121136// regardless of their keys.
122137func IgnoreValues(values ...string) SnapshotOption {
123123- return WithIgnorePattern(&valueOnlyIgnore{
138138+ return &valueOnlyIgnore{
124139 values: values,
125125- })
140140+ }
126141}
127142128143// customIgnore allows users to provide a custom ignore function.
···134149 return c.ignoreFunc(key, value)
135150}
136151152152+func (c *customIgnore) Apply(content string) string {
153153+ return content
154154+}
155155+137156// CustomIgnore creates an ignore pattern using a custom function.
138157func CustomIgnore(ignoreFunc func(key, value string) bool) SnapshotOption {
139139- return WithIgnorePattern(&customIgnore{
158158+ return &customIgnore{
140159 ignoreFunc: ignoreFunc,
141141- })
160160+ }
142161}
143162144163// IgnoreEmptyValues ignores fields with empty string values.