Repository: gavv/httpexpect
Pull request #336 (2023-03-14 02:56:09)

Implement colorization for formatted diffs
This addresses issue #335.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/57820924/badge)](https://coveralls.io/builds/57820924)

Coverage: 94.818% (-0.2%) from 95.034% when pulling **c9581849eaca8c8d542a6850ea697f131805c07a on gpjservais:334-colorize-diffs** into **14a00a3ae462b88b4ea58791c67111dc7b5bfe80 on gavv:master**.


Patch:
diff --git a/expect.go b/expect.go
index 44d73b9..7687786 100644
--- a/expect.go
+++ b/expect.go
@@ -285,6 +285,27 @@ type RequestFactory interface {
 	NewRequest(method, url string, body io.Reader) (*http.Request, error)
 }
 
+// RequestFactoryFunc is an adapter that allows a function
+// to be used as the RequestFactory
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		RequestFactory: httpextect.RequestFactoryFunc(
+//			func(method string, url string, body io.Reader) (*http.Request, error) {
+//				// factory code here
+//			}),
+//	})
+type RequestFactoryFunc func(
+	method string, url string, body io.Reader,
+) (*http.Request, error)
+
+func (f RequestFactoryFunc) NewRequest(
+	method string, url string, body io.Reader,
+) (*http.Request, error) {
+	return f(method, url, body)
+}
+
 // Client is used to send http.Request and receive http.Response.
 // http.Client implements this interface.
 //
@@ -303,6 +324,22 @@ type Client interface {
 	Do(*http.Request) (*http.Response, error)
 }
 
+// ClientFunc is an adapter that allows a function to be used as the Client
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Client: httpextect.ClientFunc(
+//			func(req *http.Request) (*http.Response, error) {
+//				// client code here
+//			}),
+//	})
+type ClientFunc func(req *http.Request) (*http.Response, error)
+
+func (f ClientFunc) Do(req *http.Request) (*http.Response, error) {
+	return f(req)
+}
+
 // WebsocketDialer is used to establish websocket.Conn and receive http.Response
 // of handshake result.
 // websocket.Dialer implements this interface.
@@ -313,8 +350,7 @@ type Client interface {
 // Example:
 //
 //	e := httpexpect.WithConfig(httpexpect.Config{
-//	  BaseURL:         "http://example.com",
-//	  WebsocketDialer: httpexpect.NewWebsocketDialer(myHandler),
+//	    WebsocketDialer: httpexpect.NewWebsocketDialer(myHandler),
 //	})
 type WebsocketDialer interface {
 	// Dial establishes new Websocket connection and returns response
@@ -322,6 +358,27 @@ type WebsocketDialer interface {
 	Dial(url string, reqH http.Header) (*websocket.Conn, *http.Response, error)
 }
 
+// WebsocketDialerFunc is an adapter that allows a function
+// to be used as the WebsocketDialer
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		WebsocketDialer: httpextect.WebsocketDialerFunc(
+//			func(url string, reqH http.Header) (*websocket.Conn, *http.Response, error) {
+//				// dialer code here
+//			}),
+//	})
+type WebsocketDialerFunc func(
+	url string, reqH http.Header,
+) (*websocket.Conn, *http.Response, error)
+
+func (f WebsocketDialerFunc) Dial(
+	url string, reqH http.Header,
+) (*websocket.Conn, *http.Response, error) {
+	return f(url, reqH)
+}
+
 // Reporter is used to report failures.
 // *testing.T, FatalReporter, AssertReporter, RequireReporter implement it.
 type Reporter interface {
@@ -330,6 +387,22 @@ type Reporter interface {
 	Errorf(message string, args ...interface{})
 }
 
+// ReporterFunc is an adapter that allows a function to be used as the Reporter
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Reporter: httpextect.ReporterFunc(
+//			func(message string, args ...interface{}) {
+//				// reporter code here
+//			}),
+//	})
+type ReporterFunc func(message string, args ...interface{})
+
+func (f ReporterFunc) Errorf(message string, args ...interface{}) {
+	f(message, args)
+}
+
 // Logger is used as output backend for Printer.
 // *testing.T implements this interface.
 type Logger interface {
@@ -337,6 +410,25 @@ type Logger interface {
 	Logf(fmt string, args ...interface{})
 }
 
+// LoggerFunc is an adapter that allows a function to be used as the Logger
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Printers: []httpexpect.Printer{
+//			httpexpect.NewCompactPrinter(
+//				httpextect.LoggerFunc(
+//					func(fmt string, args ...interface{}) {
+//						// logger code here
+//					})),
+//		},
+//	})
+type LoggerFunc func(fmt string, args ...interface{})
+
+func (f LoggerFunc) Logf(fmt string, args ...interface{}) {
+	f(fmt, args)
+}
+
 // TestingTB is a subset of testing.TB interface used by httpexpect.
 // You can use *testing.T or pass custom implementation.
 type TestingTB interface {
diff --git a/formatter.go b/formatter.go
index 4b5c7ce..2cf3986 100644
--- a/formatter.go
+++ b/formatter.go
@@ -760,6 +760,42 @@ var defaultTemplateFuncs = template.FuncMap{
 		}
 		return color.New(colorAttr).Sprint(s)
 	},
+	"colordiff": func(enable bool, s string) string {
+		if !enable {
+			return s
+		}
+
+		prefixColor := []struct {
+			prefix string
+			color  color.Attribute
+		}{
+			{"---", color.FgWhite},
+			{"+++", color.FgWhite},
+			{"-", color.FgRed},
+			{"+", color.FgGreen},
+		}
+
+		lineColor := func(s string) color.Attribute {
+			for _, pc := range prefixColor {
+				if strings.HasPrefix(s, pc.prefix) {
+					return pc.color
+				}
+			}
+
+			return color.Reset
+		}
+
+		var sb strings.Builder
+		for _, line := range strings.Split(s, "\n") {
+			if sb.Len() != 0 {
+				sb.WriteString("\n")
+			}
+
+			sb.WriteString(color.New(lineColor(line)).Sprint(line))
+		}
+
+		return sb.String()
+	},
 }
 
 var defaultSuccessTemplate = `[OK] {{ join .AssertPath .LineWidth }}`
@@ -813,6 +849,6 @@ allowed delta:
 {{- if .HaveDiff }}
 
 diff:
-{{ .Diff | indent }}
+{{ .Diff | colordiff .EnableColors | indent }}
 {{- end -}}
 `


Test patch:
diff --git a/expect_test.go b/expect_test.go
index 859440e..b5186ba 100644
--- a/expect_test.go
+++ b/expect_test.go
@@ -1,8 +1,12 @@
 package httpexpect
 
 import (
+	"errors"
+	"io"
+	"net/http"
 	"testing"
 
+	"github.com/gorilla/websocket"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -529,3 +533,109 @@ func TestExpect_Config(t *testing.T) {
 		})
 	})
 }
+
+func TestExpect_Adapters(t *testing.T) {
+	t.Run("RequestFactoryFunc", func(t *testing.T) {
+		called := false
+		factory := RequestFactoryFunc(func(
+			_ string, _ string, _ io.Reader,
+		) (*http.Request, error) {
+			called = true
+			return nil, nil
+		})
+
+		e := WithConfig(Config{
+			RequestFactory: factory,
+			Reporter:       newMockReporter(t),
+		})
+
+		e.Request("GET", "/")
+
+		assert.True(t, called)
+	})
+
+	t.Run("ClientFunc", func(t *testing.T) {
+		called := false
+		client := ClientFunc(func(_ *http.Request) (*http.Response, error) {
+			called = true
+			return &http.Response{
+				Status:     "Test Status",
+				StatusCode: 504,
+			}, nil
+		})
+
+		e := WithConfig(Config{
+			Client:   client,
+			Reporter: newMockReporter(t),
+		})
+
+		req := e.GET("/")
+		resp := req.Expect()
+
+		assert.True(t, called)
+		assert.Equal(t, resp.httpResp.StatusCode, 504)
+		assert.Equal(t, resp.httpResp.Status, "Test Status")
+	})
+
+	t.Run("WebsocketDialerFunc", func(t *testing.T) {
+		called := false
+		dialer := WebsocketDialerFunc(func(
+			_ string, _ http.Header,
+		) (*websocket.Conn, *http.Response, error) {
+			called = true
+			return &websocket.Conn{}, &http.Response{}, nil
+		})
+
+		e := WithConfig(Config{
+			WebsocketDialer: dialer,
+			Reporter:        newMockReporter(t),
+		})
+
+		e.GET("/path").WithWebsocketUpgrade().Expect().Websocket()
+
+		assert.True(t, called)
+	})
+
+	t.Run("ReporterFunc", func(t *testing.T) {
+		called := false
+		message := ""
+		client := ClientFunc(func(r *http.Request) (*http.Response, error) {
+			return nil, errors.New("")
+		})
+		reporter := ReporterFunc(func(_ string, _ ...interface{}) {
+			called = true
+			message = "test reporter called"
+		})
+
+		e := WithConfig(Config{
+			Reporter: reporter,
+			Client:   client,
+		})
+
+		e.GET("/").Expect()
+
+		assert.True(t, called)
+		assert.Contains(t, message, "test reporter called")
+	})
+
+	t.Run("LoggerFunc", func(t *testing.T) {
+		called := false
+		message := ""
+		logger := LoggerFunc(func(_ string, _ ...interface{}) {
+			called = true
+			message = "test logger called"
+		})
+
+		e := WithConfig(Config{
+			Reporter: newMockReporter(t),
+			Printers: []Printer{
+				NewCompactPrinter(logger),
+			},
+		})
+
+		e.GET("").Expect()
+
+		assert.True(t, called)
+		assert.Contains(t, message, "test logger called")
+	})
+}