Something went wrong. Try again.
Approval-based snapshot testing library for Go (mirror)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266package shutter
import ( "fmt"
"github.com/kortschak/utter" "github.com/ptdewey/shutter/internal/review" "github.com/ptdewey/shutter/internal/snapshots" "github.com/ptdewey/shutter/internal/transform")
// snapshotFormatVersion indicates the snapshot format version used by this library.// This is automatically included in snapshot metadata for compatibility checking// when the snapshot format changes in future versions.const snapshotFormatVersion = "0.1.0"
// utterConfig is a configured instance of utter for consistent formatting.// This avoids modifying global state and ensures snapshot formatting is isolated.var utterConfig = &utter.ConfigState{ Indent: " ", ElideType: true, SortKeys: true,}
// Option is a marker interface for all snapshot options.// This allows compile-time type safety while supporting different option types.type Option interface { isOption()}
// Scrubber transforms content before snapshotting, typically to replace// dynamic or sensitive data with stable placeholders.//// Scrubbers are applied in the order they are provided. Later scrubbers// can transform the output of earlier scrubbers.//// Example://// shutter.Snap(t, "user data", user,// shutter.ScrubUUID(), // First: UUIDs -> <UUID>// shutter.ScrubEmail(), // Second: emails -> <EMAIL>// )type Scrubber interface { Option Scrub(content string) string}
// IgnorePattern determines whether a key-value pair should be excluded// from JSON snapshots. This is useful for removing fields that change// frequently or contain sensitive data.//// IgnorePatterns only work with SnapJSON. Using them with Snap or SnapString// will result in an error.type IgnorePattern interface { Option ShouldIgnore(key, value string) bool}
// Snap takes a single value, formats it, and creates a snapshot with the given title.// Complex types are formatted using a pretty-printer for readability.//// Options can be provided to scrub sensitive or dynamic data before snapshotting.// Only Scrubber options are supported; IgnorePattern options will cause an error.//// Example://// user := User{ID: "123", Email: "user@example.com"}// shutter.Snap(t, "user data", user,// shutter.ScrubUUID(),// shutter.ScrubEmail(),// )func Snap(t snapshots.T, title string, value any, opts ...Option) { t.Helper()
scrubbers, ignores := separateOptions(opts)
if len(ignores) > 0 { t.Error(fmt.Sprintf("snapshot %q: IgnorePattern options are not supported with Snap; use SnapJSON instead", title)) return }
content := formatValue(value) scrubbedContent := applyScrubbers(content, scrubbers)
snapshots.Snap(t, title, snapshotFormatVersion, scrubbedContent)}
// SnapMany takes multiple values, formats them, and creates a snapshot with the given title.// This is useful when you want to snapshot multiple related values together.//// Options can be provided to scrub sensitive or dynamic data before snapshotting.// Only Scrubber options are supported; IgnorePattern options will cause an error.//// Example://// shutter.SnapMany(t, "request and response",// []any{request, response},// shutter.ScrubUUID(),// shutter.ScrubTimestamp(),// )func SnapMany(t snapshots.T, title string, values []any, opts ...Option) { t.Helper()
scrubbers, ignores := separateOptions(opts)
if len(ignores) > 0 { t.Error(fmt.Sprintf("snapshot %q: IgnorePattern options are not supported with SnapMany; use SnapJSON instead", title)) return }
content := formatValues(values...) scrubbedContent := applyScrubbers(content, scrubbers)
snapshots.Snap(t, title, snapshotFormatVersion, scrubbedContent)}
// SnapString takes a string value and creates a snapshot with the given title.// This is useful for snapshotting generated text, logs, or other string content.//// Options can be provided to scrub sensitive or dynamic data before snapshotting.// Only Scrubber options are supported; IgnorePattern options will cause an error.//// Example://// output := generateReport()// shutter.SnapString(t, "report output", output,// shutter.ScrubTimestamp(),// )func SnapString(t snapshots.T, title string, content string, opts ...Option) { t.Helper()
scrubbers, ignores := separateOptions(opts)
if len(ignores) > 0 { t.Error(fmt.Sprintf("snapshot %q: IgnorePattern options are not supported with SnapString; use SnapJSON instead", title)) return }
scrubbedContent := applyScrubbers(content, scrubbers)
snapshots.Snap(t, title, snapshotFormatVersion, scrubbedContent)}
// SnapJSON takes a JSON string, validates it, and pretty-prints it with// consistent formatting before snapshotting. This preserves the raw JSON// format while ensuring valid JSON structure.//// Options can be provided to apply both Scrubbers and IgnorePatterns.// IgnorePatterns remove fields from the JSON structure before scrubbing.// Scrubbers then transform the remaining content.//// Example://// jsonStr := `{"id": "550e8400-...", "email": "user@example.com", "password": "secret"}`// shutter.SnapJSON(t, "user response", jsonStr,// shutter.IgnoreKey("password"), // First: remove password field// shutter.ScrubUUID(), // Second: scrub remaining UUIDs// shutter.ScrubEmail(), // Third: scrub emails// )func SnapJSON(t snapshots.T, title string, jsonStr string, opts ...Option) { t.Helper()
scrubbers, ignores := separateOptions(opts)
// Transform the JSON with ignore patterns and scrubbers transformConfig := &transform.Config{ Scrubbers: toTransformScrubbers(scrubbers), Ignore: toTransformIgnorePatterns(ignores), }
transformedJSON, err := transform.TransformJSON(jsonStr, transformConfig) if err != nil { t.Error(fmt.Sprintf("snapshot %q: failed to transform JSON: %v", title, err)) return }
snapshots.Snap(t, title, snapshotFormatVersion, transformedJSON)}
// Review launches an interactive review session to accept or reject snapshot changes.func Review() error { return review.Review()}
// AcceptAll accepts all pending snapshot changes without review.func AcceptAll() error { return review.AcceptAll()}
// RejectAll rejects all pending snapshot changes without review.func RejectAll() error { return review.RejectAll()}
// formatValue formats a single value using the configured utter instance.func formatValue(v any) string { return utterConfig.Sdump(v)}
// formatValues formats multiple values using the configured utter instance.func formatValues(values ...any) string { var result string for _, v := range values { result += formatValue(v) } return result}
// separateOptions splits options into scrubbers and ignore patterns.func separateOptions(opts []Option) (scrubbers []Scrubber, ignores []IgnorePattern) { for _, opt := range opts { switch o := opt.(type) { case IgnorePattern: ignores = append(ignores, o) case Scrubber: scrubbers = append(scrubbers, o) default: // This shouldn't happen if Option interface is properly implemented panic(fmt.Sprintf("unknown option type: %T", opt)) } } return scrubbers, ignores}
// applyScrubbers applies all scrubbers to content in sequence.func applyScrubbers(content string, scrubbers []Scrubber) string { for _, scrubber := range scrubbers { content = scrubber.Scrub(content) } return content}
// scrubberAdapter adapts a Scrubber to the transform.Scrubber interface.type scrubberAdapter struct { scrubber Scrubber}
func (s *scrubberAdapter) Scrub(content string) string { return s.scrubber.Scrub(content)}
func toTransformScrubbers(scrubbers []Scrubber) []transform.Scrubber { result := make([]transform.Scrubber, len(scrubbers)) for i, scrubber := range scrubbers { result[i] = &scrubberAdapter{scrubber: scrubber} } return result}
// ignoreAdapter adapts an IgnorePattern to the transform.IgnorePattern interface.type ignoreAdapter struct { ignore IgnorePattern}
func (i *ignoreAdapter) ShouldIgnore(key, value string) bool { return i.ignore.ShouldIgnore(key, value)}
func toTransformIgnorePatterns(ignores []IgnorePattern) []transform.IgnorePattern { result := make([]transform.IgnorePattern, len(ignores)) for i, ignore := range ignores { result[i] = &ignoreAdapter{ignore: ignore} } return result}