diff --git a/go.mod b/go.mod index 5b960e6..a745978 100644 --- a/go.mod +++ b/go.mod @@ -264,4 +264,4 @@ require ( gorm.io/plugin/dbresolver v1.6.2 // indirect ) -replace github.com/goravel/framework => github.com/goravel/framework v1.18.1-0.20260925125002-93850001fe6f +replace github.com/goravel/framework => github.com/darakanoit/framework v1.18.1-0.20260927233735-13001ebed1a1 diff --git a/go.sum b/go.sum index 57c4521..b939c31 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,8 @@ github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/darakanoit/framework v1.18.1-0.20260927233735-13001ebed1a1 h1:ABBjKcQq9jbYjePRdmgO28fEhsJXaKNafazhr+vTjAk= +github.com/darakanoit/framework v1.18.1-0.20260927233735-13001ebed1a1/go.mod h1:7Phix1gVUYzA1muoX1o92AJszuDgBUgO31ICCCJ7+Xw= github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8= github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= @@ -317,8 +319,6 @@ github.com/goravel/example-proto v0.0.1 h1:ZxETeKREQWjuJ49bX/Hqj1NLR5Vyj489Ks6dR github.com/goravel/example-proto v0.0.1/go.mod h1:I8IPsHr4Ndf7KxmdsRpBR2LQ0Geo48+pjv9IIWf3mZg= github.com/goravel/fiber v1.18.1-0.20260901101548-72e06e6c5635 h1:iqzZ0P+KC8uBFNQ7PISL81U8CUc9myzS14NRYfftOB4= github.com/goravel/fiber v1.18.1-0.20260901101548-72e06e6c5635/go.mod h1:fk0YaivBle83SlSsPziy5+TRMavqlAr/QeVNo6pRmJA= -github.com/goravel/framework v1.18.1-0.20260925125002-93850001fe6f h1:/hWkvKBhtOPb6k1awR5scXcaLTHS+r1Rh3pS9O0DVIg= -github.com/goravel/framework v1.18.1-0.20260925125002-93850001fe6f/go.mod h1:7Phix1gVUYzA1muoX1o92AJszuDgBUgO31ICCCJ7+Xw= github.com/goravel/gemini v1.18.0 h1:Tv6IJ3KvBvH5QD7kAM3XFQR0g8IUfkqMbRGi7dtlWUo= github.com/goravel/gemini v1.18.0/go.mod h1:b4FueB0O0Qkz71eVWLiSaKghp+Eb/1hNNICkk7vMgrQ= github.com/goravel/gin v1.18.1-0.20260926005436-467f03c48058 h1:EId8wBSolUZQ00QQz4fyPrRWN+Tz/TZAw4u65XwSINY= diff --git a/resources/views/define_only.tmpl b/resources/views/define_only.tmpl new file mode 100644 index 0000000..3f30f14 --- /dev/null +++ b/resources/views/define_only.tmpl @@ -0,0 +1 @@ +{{ define "elsewhere.tmpl" }}x{{ end }} diff --git a/resources/views/greeting.tmpl b/resources/views/greeting.tmpl new file mode 100644 index 0000000..c80f5cb --- /dev/null +++ b/resources/views/greeting.tmpl @@ -0,0 +1 @@ +{{ define "greeting.tmpl" }}

Hello, {{ .Name }}

{{ end }} diff --git a/tests/feature/view_test.go b/tests/feature/view_test.go index 3bc4a04..2138f16 100644 --- a/tests/feature/view_test.go +++ b/tests/feature/view_test.go @@ -3,6 +3,7 @@ package feature import ( "testing" + frameworkerrors "github.com/goravel/framework/errors" "github.com/goravel/framework/support/path" "github.com/stretchr/testify/suite" @@ -61,3 +62,155 @@ func (s *ViewTestSuite) TestRender_SharedViewOverride() { s.Require().NoError(err) s.Contains(content, "

app-shared: Goravel

") } + +// TestMake_RendersAppView verifies a view renders to a string outside a request. +func (s *ViewTestSuite) TestMake_RendersAppView() { + html, err := facades.View().Make("shared.tmpl", map[string]any{"name": "Goravel"}).Render() + s.Require().NoError(err) + s.Equal("

app-shared: Goravel

", html) +} + +// TestMake_RendersPackageView verifies package views registered with +// LoadViewsFrom are reachable through Make. +func (s *ViewTestSuite) TestMake_RendersPackageView() { + html, err := facades.View().Make("package_only.tmpl", map[string]any{"name": "Goravel"}).Render() + s.Require().NoError(err) + s.Equal("

package-only: Goravel

", html) +} + +// TestMake_RendersNestedView verifies partials/layouts resolve outside a request. +// csrf_token is request-bound and absent here; it renders as an empty string. +func (s *ViewTestSuite) TestMake_RendersNestedView() { + html, err := facades.View().Make("admin/index.tmpl", map[string]any{"name": "Goravel"}).Render() + s.Require().NoError(err) + s.Equal("\n \n\n \n

I'm the header

\n\n

Hello, Goravel

\n

CSRF Token:

\n \n

I'm the footer

\n \n\n\n", html) +} + +// TestMake_MatchesHttpResponse verifies Render produces exactly what the driver +// sends for the same view. +func (s *ViewTestSuite) TestMake_MatchesHttpResponse() { + resp, err := s.Http(s.T()).Get("/shared") + s.Require().NoError(err) + resp.AssertSuccessful() + + content, err := resp.Content() + s.Require().NoError(err) + + html, err := facades.View().Make("shared.tmpl", map[string]any{"name": "Goravel"}).Render() + s.Require().NoError(err) + s.Equal(content, html) +} + +// TestMake_StructData verifies struct and pointer-to-struct data render. +func (s *ViewTestSuite) TestMake_StructData() { + type greeting struct { + Name string + } + + tests := []struct { + name string + data any + }{ + {name: "struct value", data: greeting{Name: "Goravel"}}, + {name: "pointer to struct", data: &greeting{Name: "Goravel"}}, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + html, err := facades.View().Make("greeting.tmpl", tt.data).Render() + s.Require().NoError(err) + s.Equal("

Hello, Goravel

", html) + }) + } +} + +// TestMake_WithOverridesDataAndShared verifies With overrides passed data and +// that shared data is merged at render time. +func (s *ViewTestSuite) TestMake_WithOverridesDataAndShared() { + facades.View().Share("Name", "Shared") + s.T().Cleanup(func() { + facades.View().Share("Name", nil) + }) + + template := facades.View().Make("greeting.tmpl", map[string]any{"Name": "data"}). + With("Name", "With") + + s.Equal("greeting.tmpl", template.Name()) + s.Equal(map[string]any{"Name": "With"}, template.Data()) + + html, err := template.Render() + s.Require().NoError(err) + s.Equal("

Hello, With

", html) + + // Shared data is used when the template supplies none of its own. + html, err = facades.View().Make("greeting.tmpl").Render() + s.Require().NoError(err) + s.Equal("

Hello, Shared

", html) +} + +// TestMake_MissingView verifies an unknown view fails at Render. +func (s *ViewTestSuite) TestMake_MissingView() { + _, err := facades.View().Make("missing.tmpl").Render() + s.ErrorIs(err, frameworkerrors.ViewTemplateNotExist) +} + +// TestMake_InvalidData verifies unsupported data is reported at Render. +func (s *ViewTestSuite) TestMake_InvalidData() { + _, err := facades.View().Make("shared.tmpl", 1).Render() + s.ErrorIs(err, frameworkerrors.ViewInvalidData) + + _, err = facades.View().Make("shared.tmpl", map[int]string{1: "a"}).Render() + s.ErrorIs(err, frameworkerrors.ViewInvalidData) +} + +// TestFirst verifies First picks the first renderable candidate. +func (s *ViewTestSuite) TestFirst() { + data := map[string]any{"name": "Goravel"} + + // define_only.tmpl exists but holds only a define block, so it is not + // renderable under its own name and must be skipped for a later candidate. + s.True(facades.View().Exists("define_only.tmpl")) + + tests := []struct { + name string + candidates []string + wantName string + wantHTML string + }{ + { + name: "skips missing views and returns the first existing candidate", + candidates: []string{"missing.tmpl", "package_only.tmpl", "shared.tmpl"}, + wantName: "package_only.tmpl", + wantHTML: "

package-only: Goravel

", + }, + { + name: "skips a view that exists but is not renderable under its own name", + candidates: []string{"define_only.tmpl", "package_only.tmpl"}, + wantName: "package_only.tmpl", + wantHTML: "

package-only: Goravel

", + }, + { + name: "returns the first candidate when it is already renderable", + candidates: []string{"package_only.tmpl", "shared.tmpl"}, + wantName: "package_only.tmpl", + wantHTML: "

package-only: Goravel

", + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + template := facades.View().First(tt.candidates, data) + s.Equal(tt.wantName, template.Name()) + + html, err := template.Render() + s.Require().NoError(err) + s.Equal(tt.wantHTML, html) + }) + } +} + +// TestFirst_NoneExist verifies First reports when no candidate exists. +func (s *ViewTestSuite) TestFirst_NoneExist() { + _, err := facades.View().First([]string{"missing.tmpl", "other.tmpl"}).Render() + s.ErrorIs(err, frameworkerrors.ViewNoneExist) +}