diff --git a/README.md b/README.md
index a318cf8..588e0a3 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/gkampitakis/go-snaps.svg)](https://pkg.go.dev/github.com/gkampitakis/go-snaps)
-Jest-like snapshot testing in Golang
+Jest-like snapshot testing in Go
@@ -18,12 +18,13 @@
- [Installation](#installation)
- [MatchSnapshot](#matchsnapshot)
+- [MatchStandaloneSnapshot](#matchstandalonesnapshot) `New`
- [MatchJSON](#matchjson)
- - [Matchers](#matchers)
- - [match.Any](#matchany)
- - [match.Custom](#matchcustom)
- - [match.Type\[ExpectedType\]](#matchtype)
-- [MatchStandaloneSnapshot](#matchstandalonesnapshot)
+- [MatchYAML](#matchyaml) `New`
+- [Matchers](#matchers)
+ - [match.Any](#matchany)
+ - [match.Custom](#matchcustom)
+ - [match.Type\[ExpectedType\]](#matchtype)
- [Configuration](#configuration)
- [Update Snapshots](#update-snapshots)
- [Clean obsolete Snapshots](#clean-obsolete-snapshots)
@@ -85,6 +86,29 @@ name is the test file name with extension `.snap`.
So for example if your test is called `test_simple.go` when you run your tests, a snapshot file
will be created at `./__snapshots__/test_simple.snaps`.
+## MatchStandaloneSnapshot
+
+`MatchStandaloneSnapshot` will create snapshots on separate files as opposed to `MatchSnapshot` which adds multiple snapshots inside the same file.
+
+_Combined with `snaps.Ext` you can have proper syntax highlighting and better readability_
+
+```go
+// test_simple.go
+
+func TestSimple(t *testing.T) {
+ snaps.MatchStandaloneSnapshot(t, "Hello World")
+ // or create an html snapshot file
+ snaps.WithConfig(snaps.Ext(".html")).
+ MatchStandaloneSnapshot(t, "Hello World
")
+}
+```
+
+`go-snaps` saves the snapshots in `__snapshots__` directory and the file
+name is the test file name with extension `.snap`.
+
+So for example if your test is called `test_simple.go` when you run your tests, a snapshot file
+will be created at `./__snapshots__/TestSimple_1.snaps`.
+
## MatchJSON
`MatchJSON` can be used to capture data that can represent a valid json.
@@ -107,21 +131,51 @@ func TestJSON(t *testing.T) {
JSON will be saved in snapshot in pretty format for more readability and deterministic diffs.
+## MatchYAML
+
+`MatchYAML` can be used to capture data that can represent a valid yaml.
+
+You can pass a valid json in form of `string` or `[]byte` or whatever value can be passed
+successfully on `yaml.Marshal`.
+
+```go
+func TestYAML(t *testing.T) {
+ type User struct {
+ Age int
+ Email string
+ }
+
+ snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com")
+ snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com"))
+ snaps.MatchYAML(t, User{10, "mock-email"})
+}
+```
+
### Matchers
-`MatchJSON`'s third argument can accept a list of matchers. Matchers are functions that can act
+`MatchJSON`'s and `MatchYAML`'s third argument can accept a list of matchers. Matchers are functions that can act
as property matchers and test values.
You can pass the path of the property you want to match and test.
-_More information about the supported path syntax from [gjson](https://github.com/tidwall/gjson/blob/v1.17.0/SYNTAX.md)._
-
Currently `go-snaps` has three build in matchers
- `match.Any`
- `match.Custom`
- `match.Type[ExpectedType]`
+_Open to feedback for building more matchers or you can build your own [example](./examples/matchJSON_test.go#L16)._
+
+#### Path Syntax
+
+For JSON go-snaps utilises gjson.
+
+_More information about the supported path syntax from [gjson](https://github.com/tidwall/gjson/blob/v1.17.0/SYNTAX.md)._
+
+As for YAML go-snaps utilises [github.com/goccy/go-yaml#5-use-yamlpath](https://github.com/goccy/go-yaml#5-use-yamlpath).
+
+_More information about the supported syntax [PathString](https://github.com/goccy/go-yaml/blob/9cbf5d4217830fd4ad1504e9ed117c183ade0994/path.go#L17-L26)._
+
#### match.Any
Any matcher acts as a placeholder for any value. It replaces any targeted path with a
@@ -196,29 +250,6 @@ match.Type[string]("user.info").
You can see more [examples](./examples/matchJSON_test.go#L96).
-## MatchStandaloneSnapshot
-
-`MatchStandaloneSnapshot` will create snapshots on separate files as opposed to `MatchSnapshot` which adds multiple snapshots inside the same file.
-
-_Combined with `snaps.Ext` you can have proper syntax highlighting and better readability_
-
-```go
-// test_simple.go
-
-func TestSimple(t *testing.T) {
- snaps.MatchStandaloneSnapshot(t, "Hello World")
- // or create an html snapshot file
- snaps.WithConfig(snaps.Ext(".html")).
- MatchStandaloneSnapshot(t, "Hello World
")
-}
-```
-
-`go-snaps` saves the snapshots in `__snapshots__` directory and the file
-name is the test file name with extension `.snap`.
-
-So for example if your test is called `test_simple.go` when you run your tests, a snapshot file
-will be created at `./__snapshots__/TestSimple_1.snaps`.
-
## Configuration
`go-snaps` allows passing configuration for overriding
diff --git a/examples/__snapshots__/matchYAML_test.snap b/examples/__snapshots__/matchYAML_test.snap
new file mode 100755
index 0000000..4bb82a2
--- /dev/null
+++ b/examples/__snapshots__/matchYAML_test.snap
@@ -0,0 +1,29 @@
+
+[TestMatchYaml/should_match_struct_yaml - 1]
+name: John Doe
+age: 30
+email: john.doe@example.com
+address: mock-address
+time: mock-time
+
+---
+
+[TestMatchYaml/custom_matching_logic - 1]
+name: mock-user
+email: mock-user@email.com
+keys:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+
+---
+
+[TestMatchYaml/type_matcher - 1]
+data:
+---
+
+[TestMatchYaml/type_matcher - 2]
+metadata:
+---
diff --git a/examples/matchYAML_test.go b/examples/matchYAML_test.go
new file mode 100644
index 0000000..2dfa76f
--- /dev/null
+++ b/examples/matchYAML_test.go
@@ -0,0 +1,67 @@
+package examples
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/gkampitakis/go-snaps/match"
+ "github.com/gkampitakis/go-snaps/snaps"
+)
+
+func TestMatchYaml(t *testing.T) {
+ t.Run("should match struct yaml", func(t *testing.T) {
+ type User struct {
+ Name string `yaml:"name"`
+ Age int `yaml:"age"`
+ Email string `yaml:"email"`
+ Address string `yaml:"address"`
+ Time time.Time `yaml:"time"`
+ }
+
+ snaps.MatchYAML(t, User{
+ Name: "John Doe",
+ Age: 30,
+ Email: "john.doe@example.com",
+ Address: "123 Main St",
+ Time: time.Now(),
+ }, match.Any("$.time").Placeholder("mock-time"), match.Any("$.address").Placeholder("mock-address"))
+ })
+
+ t.Run("custom matching logic", func(t *testing.T) {
+ type User struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Keys []int `json:"keys"`
+ }
+
+ u := User{
+ Name: "mock-user",
+ Email: "mock-user@email.com",
+ Keys: []int{1, 2, 3, 4, 5},
+ }
+
+ snaps.MatchYAML(t, u, match.Custom("$.keys", func(val any) (any, error) {
+ keys, ok := val.([]any)
+ if !ok {
+ return nil, fmt.Errorf("expected []any but got %T", val)
+ }
+
+ if len(keys) > 5 {
+ return nil, fmt.Errorf("expected less than 5 keys")
+ }
+
+ return val, nil
+ }))
+ })
+
+ t.Run("type matcher", func(t *testing.T) {
+ snaps.MatchYAML(t, "data: 10", match.Type[uint64]("$.data"))
+
+ snaps.MatchYAML(
+ t,
+ "metadata:\n timestamp: 1687108093142",
+ match.Type[map[string]any]("$.metadata"),
+ )
+ })
+}
diff --git a/go.mod b/go.mod
index 96bcb0b..6373a3a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,19 +1,20 @@
module github.com/gkampitakis/go-snaps
-go 1.21
+go 1.22
require (
- github.com/gkampitakis/ciinfo v0.3.0
+ github.com/gkampitakis/ciinfo v0.3.1
github.com/gkampitakis/go-diff v1.3.2
+ github.com/goccy/go-yaml v1.15.13
github.com/kr/pretty v0.3.1
github.com/maruel/natural v1.1.1
- github.com/tidwall/gjson v1.17.0
+ github.com/tidwall/gjson v1.18.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
)
require (
github.com/kr/text v0.2.0 // indirect
- github.com/rogpeppe/go-internal v1.12.0 // indirect
+ github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
)
diff --git a/go.sum b/go.sum
index 80004fe..6600622 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,10 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8=
-github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
+github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo=
+github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
+github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
+github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -11,11 +13,11 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
-github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
diff --git a/match/any.go b/match/any.go
index 361a759..8947ed6 100644
--- a/match/any.go
+++ b/match/any.go
@@ -1,8 +1,10 @@
package match
import (
- "errors"
+ "bytes"
+ "github.com/gkampitakis/go-snaps/match/internal/yaml"
+ "github.com/goccy/go-yaml/parser"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -14,14 +16,22 @@ type anyMatcher struct {
name string
}
+func (a *anyMatcher) matcherError(err error, path string) MatcherError {
+ return MatcherError{
+ Reason: err,
+ Matcher: a.name,
+ Path: path,
+ }
+}
+
/*
Any matcher acts as a placeholder for any value
It replaces any targeted path with a placeholder string
- Any("user.name")
- // or with multiple paths
Any("user.name", "user.email")
+ // or for yaml
+ Any("$.user.name", "$.user.email")
*/
func Any(paths ...string) *anyMatcher {
return &anyMatcher{
@@ -38,41 +48,65 @@ func (a *anyMatcher) Placeholder(p any) *anyMatcher {
return a
}
-// ErrOnMissingPath determines if matcher will fail in case of trying to access a json path
+// ErrOnMissingPath determines if matcher will fail in case of trying to access a path
// that doesn't exist
func (a *anyMatcher) ErrOnMissingPath(e bool) *anyMatcher {
a.errOnMissingPath = e
return a
}
+// YAML is intended to be called internally on snaps.MatchYAML for applying Any matchers
+func (a anyMatcher) YAML(b []byte) ([]byte, []MatcherError) {
+ var errs []MatcherError
+
+ f, err := parser.ParseBytes(b, parser.ParseComments)
+ if err != nil {
+ return b, []MatcherError{a.matcherError(err, "*")}
+ }
+
+ for _, p := range a.paths {
+ path, _, exists, err := yaml.Get(f, p)
+ if err != nil {
+ errs = append(errs, a.matcherError(err, p))
+
+ continue
+ }
+ if !exists {
+ if a.errOnMissingPath {
+ errs = append(errs, a.matcherError(errPathNotFound, p))
+ }
+
+ continue
+ }
+
+ if err := yaml.Update(f, path, a.placeholder); err != nil {
+ errs = append(errs, a.matcherError(err, p))
+
+ continue
+ }
+ }
+
+ return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), errs
+}
+
// JSON is intended to be called internally on snaps.MatchJSON for applying Any matchers
-func (a anyMatcher) JSON(s []byte) ([]byte, []MatcherError) {
+func (a anyMatcher) JSON(b []byte) ([]byte, []MatcherError) {
var errs []MatcherError
- json := s
+ json := b
for _, path := range a.paths {
r := gjson.GetBytes(json, path)
if !r.Exists() {
if a.errOnMissingPath {
- errs = append(errs, MatcherError{
- Reason: errors.New("path does not exist"),
- Matcher: a.name,
- Path: path,
- })
+ errs = append(errs, a.matcherError(errPathNotFound, path))
}
+
continue
}
- j, err := sjson.SetBytesOptions(json, path, a.placeholder, &sjson.Options{
- Optimistic: true,
- ReplaceInPlace: true,
- })
+ j, err := sjson.SetBytesOptions(json, path, a.placeholder, setJSONOptions)
if err != nil {
- errs = append(errs, MatcherError{
- Reason: err,
- Matcher: a.name,
- Path: path,
- })
+ errs = append(errs, a.matcherError(err, path))
continue
}
diff --git a/match/any_test.go b/match/any_test.go
index 902b0c3..a49b7dc 100644
--- a/match/any_test.go
+++ b/match/any_test.go
@@ -99,4 +99,68 @@ func TestAnyMatcher(t *testing.T) {
},
)
})
+
+ t.Run("YAML", func(t *testing.T) {
+ y := []byte(`user:
+ name: mock-user
+ email: mock-email
+date: 16/10/2022
+`)
+
+ t.Run("should return error in case of missing path", func(t *testing.T) {
+ a := Any("$.user.missing")
+ res, errs := a.YAML(y)
+
+ test.Equal(t, y, res)
+ test.Equal(t, 1, len(errs))
+
+ err := errs[0]
+
+ test.Equal(t, "path does not exist", err.Reason.Error())
+ test.Equal(t, "Any", err.Matcher)
+ test.Equal(t, "$.user.missing", err.Path)
+ })
+
+ t.Run("should aggregate errors", func(t *testing.T) {
+ a := Any("$.user.missing.key", "$.user.missing.key1")
+ res, errs := a.YAML(y)
+
+ test.Equal(t, y, res)
+ test.Equal(t, 2, len(errs))
+ })
+
+ t.Run("should replace value and return new yaml", func(t *testing.T) {
+ a := Any("$.user.email", "$.date", "$.missing.key").ErrOnMissingPath(false)
+ res, errs := a.YAML(y)
+ expected := `user:
+ name: mock-user
+ email:
+date:
+`
+
+ test.Equal(t, 0, len(errs))
+ test.Equal(t, expected, string(res))
+ })
+
+ t.Run(
+ "should replace value and return new yaml with different placeholder",
+ func(t *testing.T) {
+ a := Any(
+ "$.user.email",
+ "$.date",
+ "$.missing.key",
+ ).ErrOnMissingPath(false).
+ Placeholder(10)
+ res, errs := a.YAML(y)
+ expected := `user:
+ name: mock-user
+ email: 10
+date: 10
+`
+
+ test.Equal(t, 0, len(errs))
+ test.Equal(t, expected, string(res))
+ },
+ )
+ })
}
diff --git a/match/custom.go b/match/custom.go
index a5fa41f..bef2a6c 100644
--- a/match/custom.go
+++ b/match/custom.go
@@ -1,8 +1,10 @@
package match
import (
- "errors"
+ "bytes"
+ "github.com/gkampitakis/go-snaps/match/internal/yaml"
+ "github.com/goccy/go-yaml/parser"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -14,6 +16,14 @@ type customMatcher struct {
path string
}
+func (c *customMatcher) matcherError(err error) []MatcherError {
+ return []MatcherError{{
+ Reason: err,
+ Matcher: c.name,
+ Path: c.path,
+ }}
+}
+
type CustomCallback func(val any) (any, error)
/*
@@ -28,13 +38,22 @@ Custom matcher allows you to bring your own validation and placeholder value.
return "some number", nil
})
- The callback func value for JSON can be on of these types:
+ The callback func value for JSON can be one of these types:
bool // for JSON booleans
float64 // for JSON numbers
string // for JSON string literals
nil // for JSON null
map[string]any // for JSON objects
[]any // for JSON arrays
+
+ The callback func value for YAML can be one of these types:
+ bool // for YAML booleans
+ float64 // for YAML float numbers
+ uint64 // for YAML integer numbers
+ string // for YAML string literals
+ nil // for YAML null
+ map[string]any // for YAML objects
+ []any // for YAML arrays
*/
func Custom(path string, callback CustomCallback) *customMatcher {
return &customMatcher{
@@ -52,41 +71,62 @@ func (c *customMatcher) ErrOnMissingPath(e bool) *customMatcher {
return c
}
+// YAML is intended to be called internally on snaps.MatchYAML for applying Custom matcher
+func (c *customMatcher) YAML(b []byte) ([]byte, []MatcherError) {
+ f, err := parser.ParseBytes(b, parser.ParseComments)
+ if err != nil {
+ return nil, c.matcherError(err)
+ }
+
+ path, node, exists, err := yaml.Get(f, c.path)
+ if err != nil {
+ return nil, c.matcherError(err)
+ }
+ if !exists {
+ if c.errOnMissingPath {
+ return nil, c.matcherError(errPathNotFound)
+ }
+
+ return b, nil
+ }
+
+ value, err := yaml.GetValue(node)
+ if err != nil {
+ return nil, c.matcherError(err)
+ }
+
+ result, err := c.callback(value)
+ if err != nil {
+ return nil, c.matcherError(err)
+ }
+
+ if err := yaml.Update(f, path, result); err != nil {
+ return nil, c.matcherError(err)
+ }
+
+ return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), nil
+}
+
// JSON is intended to be called internally on snaps.MatchJSON for applying Custom matcher
-func (c *customMatcher) JSON(s []byte) ([]byte, []MatcherError) {
- r := gjson.GetBytes(s, c.path)
+func (c *customMatcher) JSON(b []byte) ([]byte, []MatcherError) {
+ r := gjson.GetBytes(b, c.path)
if !r.Exists() {
if c.errOnMissingPath {
- return nil, []MatcherError{{
- Reason: errors.New("path does not exist"),
- Matcher: c.name,
- Path: c.path,
- }}
+ return nil, c.matcherError(errPathNotFound)
}
- return s, nil
+ return b, nil
}
value, err := c.callback(r.Value())
if err != nil {
- return nil, []MatcherError{{
- Reason: err,
- Matcher: c.name,
- Path: c.path,
- }}
+ return nil, c.matcherError(err)
}
- s, err = sjson.SetBytesOptions(s, c.path, value, &sjson.Options{
- Optimistic: true,
- ReplaceInPlace: true,
- })
+ b, err = sjson.SetBytesOptions(b, c.path, value, setJSONOptions)
if err != nil {
- return nil, []MatcherError{{
- Reason: err,
- Matcher: c.name,
- Path: c.path,
- }}
+ return nil, c.matcherError(err)
}
- return s, nil
+ return b, nil
}
diff --git a/match/custom_test.go b/match/custom_test.go
index 7212724..fb77a9c 100644
--- a/match/custom_test.go
+++ b/match/custom_test.go
@@ -18,7 +18,7 @@ func TestCustomMatcher(t *testing.T) {
test.Equal(t, c.name, "Custom")
})
- t.Run("should allow overrding values", func(t *testing.T) {
+ t.Run("should allow overriding values", func(t *testing.T) {
c := Custom("path", func(val any) (any, error) {
return nil, nil
}).ErrOnMissingPath(false)
@@ -100,4 +100,74 @@ func TestCustomMatcher(t *testing.T) {
test.Nil(t, errs)
})
})
+
+ t.Run("YAML", func(t *testing.T) {
+ y := []byte(`
+user:
+ name: mock-user
+ email: mock-email
+date: 16/10/2022
+`)
+
+ t.Run("should return error in case of missing path", func(t *testing.T) {
+ c := Custom("$.missing.key", func(val any) (any, error) {
+ return nil, nil
+ })
+
+ res, errs := c.YAML(y)
+
+ test.Nil(t, res)
+ test.Equal(t, 1, len(errs))
+
+ err := errs[0]
+
+ test.Equal(t, "path does not exist", err.Reason.Error())
+ test.Equal(t, "Custom", err.Matcher)
+ test.Equal(t, "$.missing.key", err.Path)
+ })
+
+ t.Run("should ignore error in case of missing path", func(t *testing.T) {
+ c := Custom("$.missing.key", func(val any) (any, error) {
+ return nil, nil
+ }).ErrOnMissingPath(false)
+
+ res, errs := c.YAML(y)
+ test.Equal(t, y, res)
+ test.Nil(t, errs)
+ })
+
+ t.Run("should return error from custom callback", func(t *testing.T) {
+ c := Custom("$.user.email", func(val any) (any, error) {
+ return nil, errors.New("custom error")
+ })
+
+ res, errs := c.YAML(y)
+
+ test.Nil(t, res)
+ test.Equal(t, 1, len(errs))
+
+ err := errs[0]
+
+ test.Equal(t, "custom error", err.Reason.Error())
+ test.Equal(t, "Custom", err.Matcher)
+ test.Equal(t, "$.user.email", err.Path)
+ })
+
+ t.Run("should apply value from custom callback to yaml", func(t *testing.T) {
+ c := Custom("$.user.email", func(val any) (any, error) {
+ return "replaced email", nil
+ })
+
+ res, errs := c.YAML(y)
+
+ expected := `user:
+ name: mock-user
+ email: replaced email
+date: 16/10/2022
+`
+
+ test.Equal(t, expected, string(res))
+ test.Nil(t, errs)
+ })
+ })
}
diff --git a/match/internal/yaml/yaml.go b/match/internal/yaml/yaml.go
new file mode 100644
index 0000000..6b22881
--- /dev/null
+++ b/match/internal/yaml/yaml.go
@@ -0,0 +1,70 @@
+package yaml
+
+import (
+ "bytes"
+ "errors"
+ "strings"
+
+ "github.com/goccy/go-yaml"
+ "github.com/goccy/go-yaml/ast"
+)
+
+// GetValue returns the value of the node.
+func GetValue(node ast.Node) (interface{}, error) {
+ data, err := node.MarshalYAML()
+ if err != nil {
+ return nil, err
+ }
+
+ var value interface{}
+ if err := yaml.Unmarshal(data, &value); err != nil {
+ return nil, err
+ }
+
+ return value, nil
+}
+
+// Get takes an ast.File and a string representing a path
+// and returns the yaml.Path, the node and a bool indicating if the node exists.
+func Get(f *ast.File, p string) (*yaml.Path, ast.Node, bool, error) {
+ path, err := yaml.PathString(p)
+ if err != nil {
+ return nil, nil, false, err
+ }
+
+ node, err := path.FilterFile(f)
+ if err != nil {
+ if errors.Is(err, yaml.ErrNotFoundNode) {
+ return path, nil, false, nil
+ }
+
+ return path, nil, false, err
+ }
+
+ return path, node, true, nil
+}
+
+// Update marshals the value and replaces the file at the path provided with the new value.
+func Update(f *ast.File, path *yaml.Path, value interface{}) error {
+ b, err := yaml.Marshal(value)
+ if err != nil {
+ return err
+ }
+
+ return path.ReplaceWithReader(f, bytes.NewReader(b))
+}
+
+// MarshalFile returns the representation of the ast.File to a byte slice.
+func MarshalFile(f *ast.File, addNewLine bool) []byte {
+ docs := make([]string, 0, len(f.Docs))
+
+ for _, doc := range f.Docs {
+ docs = append(docs, doc.String())
+ }
+
+ if addNewLine {
+ docs = append(docs, "")
+ }
+
+ return []byte(strings.Join(docs, "\n"))
+}
diff --git a/match/type.go b/match/type.go
index 37d55a2..5f532e7 100644
--- a/match/type.go
+++ b/match/type.go
@@ -1,9 +1,11 @@
package match
import (
- "errors"
+ "bytes"
"fmt"
+ "github.com/gkampitakis/go-snaps/match/internal/yaml"
+ "github.com/goccy/go-yaml/parser"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -15,14 +17,22 @@ type typeMatcher[ExpectedType any] struct {
expectedType any
}
+func (t *typeMatcher[ExpectedType]) matcherError(err error, path string) MatcherError {
+ return MatcherError{
+ Reason: err,
+ Matcher: t.name,
+ Path: path,
+ }
+}
+
/*
Type matcher evaluates types that are passed in a snapshot
It replaces any targeted path with placeholder in the form of ``
- match.Type[string]("user.info")
- // or with multiple paths
- match.Type[float64]("user.age", "data.items")
+ match.Type[string]("user.info", "user.age")
+ // or for yaml
+ match.Type[string]("$.user.info", "$.user.age")
*/
func Type[ExpectedType any](paths ...string) *typeMatcher[ExpectedType] {
return &typeMatcher[ExpectedType]{
@@ -33,36 +43,77 @@ func Type[ExpectedType any](paths ...string) *typeMatcher[ExpectedType] {
}
}
-// ErrOnMissingPath determines if matcher will fail in case of trying to access a json path
+// ErrOnMissingPath determines if matcher will fail in case of trying to access a path
// that doesn't exist
func (t *typeMatcher[T]) ErrOnMissingPath(e bool) *typeMatcher[T] {
t.errOnMissingPath = e
return t
}
-func (t typeMatcher[ExpectedType]) JSON(s []byte) ([]byte, []MatcherError) {
+// YAML is intended to be called internally on snaps.MatchJSON for applying Type matchers
+func (t typeMatcher[ExpectedType]) YAML(b []byte) ([]byte, []MatcherError) {
+ var errs []MatcherError
+
+ f, err := parser.ParseBytes(b, parser.ParseComments)
+ if err != nil {
+ return b, []MatcherError{t.matcherError(err, "*")}
+ }
+
+ for _, p := range t.paths {
+ path, node, exists, err := yaml.Get(f, p)
+ if err != nil {
+ errs = append(errs, t.matcherError(err, p))
+
+ continue
+ }
+ if !exists {
+ if t.errOnMissingPath {
+ errs = append(errs, t.matcherError(errPathNotFound, p))
+ }
+
+ continue
+ }
+
+ value, err := yaml.GetValue(node)
+ if err != nil {
+ errs = append(errs, t.matcherError(err, p))
+
+ continue
+ }
+
+ if err := typeCheck[ExpectedType](value); err != nil {
+ errs = append(errs, t.matcherError(err, p))
+
+ continue
+ }
+
+ if err := yaml.Update(f, path, typePlaceholder(value)); err != nil {
+ errs = append(errs, t.matcherError(err, p))
+
+ continue
+ }
+ }
+
+ return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), errs
+}
+
+// JSON is intended to be called internally on snaps.MatchJSON for applying Type matchers
+func (t typeMatcher[ExpectedType]) JSON(b []byte) ([]byte, []MatcherError) {
var errs []MatcherError
- json := s
+ json := b
for _, path := range t.paths {
r := gjson.GetBytes(json, path)
if !r.Exists() {
if t.errOnMissingPath {
- errs = append(errs, MatcherError{
- Reason: errors.New("path does not exist"),
- Matcher: t.name,
- Path: path,
- })
+ errs = append(errs, t.matcherError(errPathNotFound, path))
}
+
continue
}
- if _, ok := r.Value().(ExpectedType); !ok {
- errs = append(errs, MatcherError{
- Reason: fmt.Errorf("expected type %T, received %T", *new(ExpectedType), r.Value()),
- Matcher: t.name,
- Path: path,
- })
+ if err := typeCheck[ExpectedType](r.Value()); err != nil {
+ errs = append(errs, t.matcherError(err, path))
continue
}
@@ -70,18 +121,11 @@ func (t typeMatcher[ExpectedType]) JSON(s []byte) ([]byte, []MatcherError) {
j, err := sjson.SetBytesOptions(
json,
path,
- fmt.Sprintf("", r.Value()),
- &sjson.Options{
- Optimistic: true,
- ReplaceInPlace: true,
- },
+ typePlaceholder(r.Value()),
+ setJSONOptions,
)
if err != nil {
- errs = append(errs, MatcherError{
- Reason: err,
- Matcher: t.name,
- Path: path,
- })
+ errs = append(errs, t.matcherError(err, path))
continue
}
@@ -91,3 +135,15 @@ func (t typeMatcher[ExpectedType]) JSON(s []byte) ([]byte, []MatcherError) {
return json, errs
}
+
+func typeCheck[ExpectedType any](value interface{}) error {
+ if _, ok := value.(ExpectedType); !ok {
+ return fmt.Errorf("expected type %T, received %T", *new(ExpectedType), value)
+ }
+
+ return nil
+}
+
+func typePlaceholder(value interface{}) string {
+ return fmt.Sprintf("", value)
+}
diff --git a/match/type_test.go b/match/type_test.go
index 371a127..a0e423f 100644
--- a/match/type_test.go
+++ b/match/type_test.go
@@ -88,4 +88,50 @@ func TestTypeMatcher(t *testing.T) {
test.Equal(t, "expected type int, received float64", errs[1].Reason.Error())
})
})
+
+ t.Run("YAML", func(t *testing.T) {
+ y := []byte(`user:
+ name: mock-user
+ email: mock-email
+ age: 29
+date: 16/10/2022
+`)
+
+ t.Run("should return error in case of missing path", func(t *testing.T) {
+ tm := Type[string]("$.user.missing")
+ res, errs := tm.YAML(y)
+
+ test.Equal(t, string(y), string(res))
+ test.Equal(t, 1, len(errs))
+
+ err := errs[0]
+
+ test.Equal(t, "path does not exist", err.Reason.Error())
+ test.Equal(t, "Type", err.Matcher)
+ test.Equal(t, "$.user.missing", err.Path)
+ })
+
+ t.Run("should aggregate errors", func(t *testing.T) {
+ tm := Type[string]("$.user.missing", "$.user.missing_key")
+ res, errs := tm.YAML(y)
+
+ test.Equal(t, y, res)
+ test.Equal(t, 2, len(errs))
+ })
+
+ t.Run("should evaluate passed type and replace yaml", func(t *testing.T) {
+ tm := Type[string]("$.user.name", "$.date")
+ res, errs := tm.YAML(y)
+
+ expected := `user:
+ name:
+ email: mock-email
+ age: 29
+date:
+`
+
+ test.Nil(t, errs)
+ test.Equal(t, expected, string(res))
+ })
+ })
}
diff --git a/match/utils.go b/match/utils.go
index 96f0d8e..a1cabb2 100644
--- a/match/utils.go
+++ b/match/utils.go
@@ -1,9 +1,27 @@
package match
+import (
+ "errors"
+
+ "github.com/tidwall/sjson"
+)
+
+var (
+ errPathNotFound = errors.New("path does not exist")
+ setJSONOptions = &sjson.Options{
+ Optimistic: true,
+ ReplaceInPlace: true,
+ }
+)
+
type JSONMatcher interface {
JSON([]byte) ([]byte, []MatcherError)
}
+type YAMLMatcher interface {
+ YAML([]byte) ([]byte, []MatcherError)
+}
+
// internal Error struct returned from Matchers
type MatcherError struct {
Reason error
diff --git a/snaps/matchJSON.go b/snaps/matchJSON.go
index 10d78a7..9907a66 100644
--- a/snaps/matchJSON.go
+++ b/snaps/matchJSON.go
@@ -25,14 +25,14 @@ MatchJSON verifies the input matches the most recent snap file.
Input can be a valid json string or []byte or whatever value can be passed
successfully on `json.Marshal`.
- MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
- MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`))
- MatchJSON(t, User{10, "mock-email"})
+ snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
+ snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`))
+ snaps.MatchJSON(t, User{10, "mock-email"})
MatchJSON also supports passing matchers as a third argument. Those matchers can act either as
validators or placeholders for data that might change on each invocation e.g. dates.
- MatchJSON(t, User{created: time.Now(), email: "mock-email"}, match.Any("created"))
+ snaps.MatchJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created"))
*/
func (c *Config) MatchJSON(t testingT, input any, matchers ...match.JSONMatcher) {
t.Helper()
@@ -45,14 +45,14 @@ MatchJSON verifies the input matches the most recent snap file.
Input can be a valid json string or []byte or whatever value can be passed
successfully on `json.Marshal`.
- MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
- MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`))
- MatchJSON(t, User{10, "mock-email"})
+ snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
+ snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`))
+ snaps.MatchJSON(t, User{10, "mock-email"})
MatchJSON also supports passing matchers as a third argument. Those matchers can act either as
validators or placeholders for data that might change on each invocation e.g. dates.
- MatchJSON(t, User{created: time.Now(), email: "mock-email"}, match.Any("created"))
+ snaps.MatchJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created"))
*/
func MatchJSON(t testingT, input any, matchers ...match.JSONMatcher) {
t.Helper()
diff --git a/snaps/matchJSON_test.go b/snaps/matchJSON_test.go
index 6f30d93..4d113cf 100644
--- a/snaps/matchJSON_test.go
+++ b/snaps/matchJSON_test.go
@@ -31,7 +31,7 @@ func TestMatchJSON(t *testing.T) {
input: `{"user":"mock-name","items":[5,1,3,4]}`,
},
{
- name: "string",
+ name: "byte",
input: []byte(`{"user":"mock-name","items":[5,1,3,4]}`),
},
{
diff --git a/snaps/matchYAML.go b/snaps/matchYAML.go
new file mode 100644
index 0000000..79f8937
--- /dev/null
+++ b/snaps/matchYAML.go
@@ -0,0 +1,189 @@
+package snaps
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/gkampitakis/go-snaps/internal/colors"
+ "github.com/gkampitakis/go-snaps/match"
+ "github.com/goccy/go-yaml"
+)
+
+var yamlEncodeOptions = []yaml.EncodeOption{
+ yaml.Indent(2),
+ yaml.IndentSequence(true),
+}
+
+/*
+MatchYAML verifies the input matches the most recent snap file.
+Input can be a valid json string or []byte or whatever value can be passed
+successfully on `yaml.Marshal`.
+
+ snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com")
+ snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com"))
+ snaps.MatchYAML(t, User{10, "mock-email"})
+
+MatchYAML also supports passing matchers as a third argument. Those matchers can act either as
+validators or placeholders for data that might change on each invocation e.g. dates.
+
+ snaps.MatchYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created"))
+*/
+func (c *Config) MatchYAML(t testingT, input any, matchers ...match.YAMLMatcher) {
+ t.Helper()
+
+ matchYAML(c, t, input, matchers...)
+}
+
+/*
+MatchYAML verifies the input matches the most recent snap file.
+Input can be a valid json string or []byte or whatever value can be passed
+successfully on `yaml.Marshal`.
+
+ snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com")
+ snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com"))
+ snaps.MatchYAML(t, User{10, "mock-email"})
+
+MatchYAML also supports passing matchers as a third argument. Those matchers can act either as
+validators or placeholders for data that might change on each invocation e.g. dates.
+
+ snaps.MatchYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created"))
+*/
+func MatchYAML(t testingT, input any, matchers ...match.YAMLMatcher) {
+ t.Helper()
+
+ matchYAML(&defaultConfig, t, input, matchers...)
+}
+
+func matchYAML(c *Config, t testingT, input any, matchers ...match.YAMLMatcher) {
+ t.Helper()
+
+ snapPath, snapPathRel := snapshotPath(c, t.Name(), false)
+ testID := testsRegistry.getTestID(snapPath, t.Name())
+ t.Cleanup(func() {
+ testsRegistry.reset(snapPath, t.Name())
+ })
+
+ y, err := validateYAML(input)
+ if err != nil {
+ handleError(t, err)
+ return
+ }
+
+ y, matchersErrors := applyYAMLMatchers(y, matchers...)
+ if len(matchersErrors) > 0 {
+ s := strings.Builder{}
+
+ for _, err := range matchersErrors {
+ colors.Fprint(
+ &s,
+ colors.Red,
+ fmt.Sprintf(
+ "\n%smatch.%s(\"%s\") - %s",
+ errorSymbol,
+ err.Matcher,
+ err.Path,
+ err.Reason,
+ ),
+ )
+ }
+
+ handleError(t, s.String())
+ return
+ }
+
+ snapshot := takeYAMLSnapshot(y)
+ prevSnapshot, line, err := getPrevSnapshot(testID, snapPath)
+ if errors.Is(err, errSnapNotFound) {
+ if isCI {
+ handleError(t, err)
+ return
+ }
+
+ err := addNewSnapshot(testID, snapshot, snapPath)
+ if err != nil {
+ handleError(t, err)
+ return
+ }
+
+ t.Log(addedMsg)
+ testEvents.register(added)
+ return
+ }
+ if err != nil {
+ handleError(t, err)
+ return
+ }
+
+ diff := prettyDiff(
+ unescapeEndChars(prevSnapshot),
+ unescapeEndChars(snapshot),
+ snapPathRel,
+ line,
+ )
+ if diff == "" {
+ testEvents.register(passed)
+ return
+ }
+
+ if !shouldUpdate(c.update) {
+ handleError(t, diff)
+ return
+ }
+
+ if err = updateSnapshot(testID, snapshot, snapPath); err != nil {
+ handleError(t, err)
+ return
+ }
+
+ t.Log(updatedMsg)
+ testEvents.register(updated)
+}
+
+func validateYAML(input any) ([]byte, error) {
+ var out interface{}
+
+ switch y := input.(type) {
+ case string:
+ err := yaml.Unmarshal([]byte(y), &out)
+ if err != nil {
+ return nil, fmt.Errorf("invalid yaml: %w", err)
+ }
+
+ return []byte(y), nil
+ case []byte:
+ err := yaml.Unmarshal(y, &out)
+ if err != nil {
+ return nil, fmt.Errorf("invalid yaml: %w", err)
+ }
+
+ return y, nil
+ default:
+ data, err := yaml.MarshalWithOptions(input, yamlEncodeOptions...)
+ if err != nil {
+ return nil, fmt.Errorf("invalid yaml: %w", err)
+ }
+
+ return data, nil
+ }
+}
+
+func applyYAMLMatchers(b []byte, matchers ...match.YAMLMatcher) ([]byte, []match.MatcherError) {
+ errors := []match.MatcherError{}
+
+ for _, m := range matchers {
+ y, errs := m.YAML(b)
+ if len(errs) > 0 {
+ errors = append(errors, errs...)
+ continue
+ }
+
+ b = y
+ }
+
+ return b, errors
+}
+
+func takeYAMLSnapshot(b []byte) string {
+ return escapeEndChars(string(b))
+}
diff --git a/snaps/matchYAML_test.go b/snaps/matchYAML_test.go
new file mode 100644
index 0000000..39592ff
--- /dev/null
+++ b/snaps/matchYAML_test.go
@@ -0,0 +1,207 @@
+package snaps
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/gkampitakis/go-snaps/internal/test"
+ "github.com/gkampitakis/go-snaps/match"
+)
+
+const yamlFilename = "matchYAML_test.snap"
+
+func TestMatchYAML(t *testing.T) {
+ t.Run("should create yaml snapshot", func(t *testing.T) {
+ expected := `user: mock-name
+items:
+ - 5
+ - 1
+ - 3
+ - 4
+`
+
+ for _, tc := range []struct {
+ name string
+ input any
+ }{
+ {
+ name: "string",
+ input: "user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n",
+ },
+ {
+ name: "byte",
+ input: []byte("user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n"),
+ },
+ {
+ name: "marshal object",
+ input: struct {
+ User string `yaml:"user"`
+ Items []int `yaml:"items"`
+ }{
+ User: "mock-name",
+ Items: []int{5, 1, 3, 4},
+ },
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ snapPath := setupSnapshot(t, yamlFilename, false)
+
+ mockT := test.NewMockTestingT(t)
+ mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) }
+
+ MatchYAML(mockT, tc.input)
+
+ snap, line, err := getPrevSnapshot("[mock-name - 1]", snapPath)
+
+ test.NoError(t, err)
+ test.Equal(t, 2, line)
+ test.Equal(t, expected, snap)
+ test.Equal(t, 1, testEvents.items[added])
+ // clean up function called
+ test.Equal(t, 0, testsRegistry.running[snapPath]["mock-name"])
+ test.Equal(t, 1, testsRegistry.cleanup[snapPath]["mock-name"])
+ })
+ }
+
+ t.Run("should validate yaml", func(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ input any
+ err string
+ }{
+ {
+ name: "string",
+ input: "key1: \"value1\nkey2: \"value2\"",
+ err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined
+ 1 | key1: "value1
+> 2 | key2: "value2"
+ ^
+`,
+ },
+ {
+ name: "byte",
+ input: []byte("key1: \"value1\nkey2: \"value2\""),
+ err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined
+ 1 | key1: "value1
+> 2 | key2: "value2"
+ ^
+`,
+ },
+ {
+ name: "struct",
+ input: make(chan struct{}),
+ err: "invalid yaml: unknown value type chan struct {}",
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ setupSnapshot(t, yamlFilename, false)
+
+ mockT := test.NewMockTestingT(t)
+ mockT.MockError = func(args ...any) {
+ test.Equal(t, tc.err, (args[0].(error)).Error())
+ }
+
+ MatchYAML(mockT, tc.input)
+ })
+ }
+ })
+
+ t.Run("matchers", func(t *testing.T) {
+ t.Run("should apply matches in order", func(t *testing.T) {
+ setupSnapshot(t, yamlFilename, false)
+
+ mockT := test.NewMockTestingT(t)
+ mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) }
+ mockT.MockError = func(a ...any) {
+ fmt.Println(a)
+ }
+
+ c1 := func(val any) (any, error) {
+ return map[string]any{"key2": nil}, nil
+ }
+ c2 := func(val any) (any, error) {
+ return map[string]any{"key3": nil}, nil
+ }
+ c3 := func(val any) (any, error) {
+ return map[string]any{"key4": nil}, nil
+ }
+
+ MatchYAML(mockT, "key1: \"\"",
+ match.Custom("$.key1", c1),
+ match.Custom("$.key1.key2", c2),
+ match.Custom("$.key1.key2.key3", c3),
+ )
+ })
+
+ t.Run("should aggregate errors from matchers", func(t *testing.T) {
+ setupSnapshot(t, yamlFilename, false)
+
+ mockT := test.NewMockTestingT(t)
+ mockT.MockError = func(args ...any) {
+ test.Equal(t,
+ "\x1b[31;1m\n✕ match.Custom(\"$.age\") - mock error"+
+ "\x1b[0m\x1b[31;1m\n✕ match.Any(\"$.missing.key.1\") - path does not exist"+
+ "\x1b[0m\x1b[31;1m\n✕ match.Any(\"$.missing.key.2\") - path does not exist\x1b[0m",
+ args[0],
+ )
+ }
+
+ c := func(val any) (any, error) {
+ return nil, errors.New("mock error")
+ }
+ MatchYAML(
+ mockT,
+ `age: 10`,
+ match.Custom("$.age", c),
+ match.Any("$.missing.key.1", "$.missing.key.2"),
+ )
+ })
+ })
+
+ t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) {
+ setupSnapshot(t, yamlFilename, true)
+
+ mockT := test.NewMockTestingT(t)
+ mockT.MockError = func(args ...any) {
+ test.Equal(t, errSnapNotFound, args[0].(error))
+ }
+
+ MatchYAML(mockT, "")
+
+ test.Equal(t, 1, testEvents.items[erred])
+ })
+
+ t.Run("should update snapshot when 'shouldUpdate'", func(t *testing.T) {
+ snapPath := setupSnapshot(t, yamlFilename, false, true)
+ printerExpectedCalls := []func(received any){
+ func(received any) { test.Equal(t, addedMsg, received.(string)) },
+ func(received any) { test.Equal(t, updatedMsg, received.(string)) },
+ }
+ mockT := test.NewMockTestingT(t)
+ mockT.MockLog = func(args ...any) {
+ printerExpectedCalls[0](args[0])
+
+ // shift
+ printerExpectedCalls = printerExpectedCalls[1:]
+ }
+
+ // First call for creating the snapshot
+ MatchYAML(mockT, "value: hello world")
+ test.Equal(t, 1, testEvents.items[added])
+
+ // Resetting registry to emulate the same MatchSnapshot call
+ testsRegistry = newRegistry()
+
+ // Second call with different params
+ MatchYAML(mockT, "value: bye world")
+
+ test.Equal(
+ t,
+ "\n[mock-name - 1]\nvalue: bye world\n---\n",
+ test.GetFileContent(t, snapPath),
+ )
+ test.Equal(t, 1, testEvents.items[updated])
+ })
+ })
+}
diff --git a/snaps/snapshot.go b/snaps/snapshot.go
index 6466d5a..c2dc810 100644
--- a/snaps/snapshot.go
+++ b/snaps/snapshot.go
@@ -41,9 +41,9 @@ func Update(u bool) func(*Config) {
}
}
-// Specify folder name where snapshots are stored
+// Specify snapshot file name
//
-// default: __snapshots__
+// default: test's filename
//
// this doesn't change the file extension see `snap.Ext`
func Filename(name string) func(*Config) {