Merge pull request 'Fix rendering, create render tests' (#64) from frontend-fixes into main
Reviewed-on: https://git.ts.mattnite.net/mattnite/forgejo-tickets/pulls/64
This commit is contained in:
commit
e0b5fd8938
|
|
@ -0,0 +1,297 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/mattnite/forgejo-tickets/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAllTemplatesExecute parses every template and executes it with
|
||||||
|
// representative data wrapped in PageData, exactly as the real Renderer does.
|
||||||
|
// This catches errors like accessing a field on PageData that only exists in
|
||||||
|
// the Data map (e.g. $.ProductFilter instead of using a captured variable).
|
||||||
|
func TestAllTemplatesExecute(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
dueDate := now.Add(24 * time.Hour)
|
||||||
|
|
||||||
|
testUser := &models.User{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Role: "customer",
|
||||||
|
}
|
||||||
|
|
||||||
|
adminUser := &models.User{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Email: "admin@example.com",
|
||||||
|
Name: "Admin",
|
||||||
|
Role: "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := models.Repo{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Name: "Test Product",
|
||||||
|
Slug: "test-product",
|
||||||
|
ForgejoOwner: "owner",
|
||||||
|
ForgejoRepo: "repo",
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketID := uuid.New()
|
||||||
|
|
||||||
|
// Each entry maps a template name to data that exercises all template paths,
|
||||||
|
// including {{range}} loops that re-scope the dot.
|
||||||
|
cases := map[string]struct {
|
||||||
|
data interface{}
|
||||||
|
user *models.User
|
||||||
|
}{
|
||||||
|
"home": {data: nil, user: nil},
|
||||||
|
|
||||||
|
"login": {data: map[string]interface{}{
|
||||||
|
"GoogleEnabled": true,
|
||||||
|
"MicrosoftEnabled": false,
|
||||||
|
"AppleEnabled": false,
|
||||||
|
"Error": "",
|
||||||
|
"Email": "test@example.com",
|
||||||
|
}},
|
||||||
|
|
||||||
|
"register": {data: map[string]interface{}{
|
||||||
|
"Name": "Test",
|
||||||
|
"Email": "test@example.com",
|
||||||
|
"Error": "",
|
||||||
|
}},
|
||||||
|
|
||||||
|
"forgot-password": {data: nil},
|
||||||
|
|
||||||
|
"reset-password": {data: map[string]interface{}{
|
||||||
|
"Token": "abc123",
|
||||||
|
"Error": "",
|
||||||
|
}},
|
||||||
|
|
||||||
|
"change-password": {data: map[string]interface{}{
|
||||||
|
"HasPassword": true,
|
||||||
|
"Error": "",
|
||||||
|
}, user: testUser},
|
||||||
|
|
||||||
|
"tickets/list": {data: map[string]interface{}{
|
||||||
|
"Tickets": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"ID": ticketID,
|
||||||
|
"Title": "Test Ticket",
|
||||||
|
"Status": "open",
|
||||||
|
"Priority": "high",
|
||||||
|
"Pinned": false,
|
||||||
|
"RepoName": "Test Product",
|
||||||
|
"DueDate": &dueDate,
|
||||||
|
"CreatedAt": now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"StatusFilter": "open",
|
||||||
|
"PriorityFilter": "",
|
||||||
|
"ProductFilter": "Test Product",
|
||||||
|
"Products": []string{"Test Product", "Other Product"},
|
||||||
|
"Page": 1,
|
||||||
|
"TotalPages": 2,
|
||||||
|
}, user: testUser},
|
||||||
|
|
||||||
|
"tickets/new": {data: map[string]interface{}{
|
||||||
|
"Repos": []models.Repo{repo},
|
||||||
|
"Error": "",
|
||||||
|
"Title": "",
|
||||||
|
"Description": "",
|
||||||
|
"RepoID": "",
|
||||||
|
}, user: testUser},
|
||||||
|
|
||||||
|
"tickets/detail": {data: map[string]interface{}{
|
||||||
|
"Ticket": map[string]interface{}{
|
||||||
|
"ID": ticketID,
|
||||||
|
"Title": "Test Ticket",
|
||||||
|
"Description": "A test description",
|
||||||
|
"Status": "open",
|
||||||
|
"Priority": "high",
|
||||||
|
"Pinned": false,
|
||||||
|
"Assignees": "Dev User",
|
||||||
|
"DueDate": &dueDate,
|
||||||
|
"Attachments": []map[string]interface{}{
|
||||||
|
{"ID": int64(1), "Name": "file.txt", "Size": int64(1024)},
|
||||||
|
},
|
||||||
|
"CreatedAt": now,
|
||||||
|
},
|
||||||
|
"Repo": repo,
|
||||||
|
"Timeline": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"Type": "comment",
|
||||||
|
"Body": "A comment",
|
||||||
|
"AuthorName": "Dev",
|
||||||
|
"IsTeam": true,
|
||||||
|
"EventText": "",
|
||||||
|
"CreatedAt": now,
|
||||||
|
"Attachments": []map[string]interface{}{
|
||||||
|
{"ID": int64(2), "Name": "img.png"},
|
||||||
|
},
|
||||||
|
"CommentID": int64(100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": "status_change",
|
||||||
|
"Body": "",
|
||||||
|
"AuthorName": "Dev",
|
||||||
|
"IsTeam": true,
|
||||||
|
"EventText": "changed status to open",
|
||||||
|
"CreatedAt": now,
|
||||||
|
"Attachments": []map[string]interface{}{},
|
||||||
|
"CommentID": int64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"RelatedIssues": []map[string]interface{}{
|
||||||
|
{"Number": int64(5), "Title": "Related", "IsVisible": true, "DisplayText": "Related", "TicketID": uuid.New().String()},
|
||||||
|
},
|
||||||
|
"Mentions": map[string]string{"dev": "Dev User"},
|
||||||
|
}, user: testUser},
|
||||||
|
|
||||||
|
"admin/dashboard": {data: map[string]interface{}{
|
||||||
|
"UserCount": int64(10),
|
||||||
|
"TotalTickets": int64(50),
|
||||||
|
"OpenTickets": int64(20),
|
||||||
|
"InProgressTickets": int64(15),
|
||||||
|
"ClosedTickets": int64(15),
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/users/list": {data: map[string]interface{}{
|
||||||
|
"Users": []models.User{*adminUser},
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/users/new": {data: map[string]interface{}{
|
||||||
|
"Error": "",
|
||||||
|
"Name": "",
|
||||||
|
"Email": "",
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/users/pending": {data: map[string]interface{}{
|
||||||
|
"Users": []models.User{{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Email: "pending@example.com",
|
||||||
|
Name: "Pending User",
|
||||||
|
Role: "customer",
|
||||||
|
}},
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/users/detail": {data: map[string]interface{}{
|
||||||
|
"User": *testUser,
|
||||||
|
"Tickets": []map[string]interface{}{},
|
||||||
|
"AllRepos": []models.Repo{repo},
|
||||||
|
"AssignedRepoIDs": map[string]bool{
|
||||||
|
repo.ID.String(): true,
|
||||||
|
},
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/repos/list": {data: map[string]interface{}{
|
||||||
|
"Repos": []models.Repo{repo},
|
||||||
|
"BaseURL": "https://example.com",
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/repos/new": {data: map[string]interface{}{
|
||||||
|
"Error": "",
|
||||||
|
"Name": "",
|
||||||
|
"Slug": "",
|
||||||
|
"ForgejoOwner": "",
|
||||||
|
"ForgejoRepo": "",
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/repos/edit": {data: map[string]interface{}{
|
||||||
|
"Repo": repo,
|
||||||
|
"BaseURL": "https://example.com",
|
||||||
|
"ForgejoURL": "https://forgejo.example.com",
|
||||||
|
"RepoPermErr": "",
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/tickets/list": {data: map[string]interface{}{
|
||||||
|
"Tickets": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"ID": ticketID,
|
||||||
|
"Title": "Test Ticket",
|
||||||
|
"Status": "open",
|
||||||
|
"Priority": "high",
|
||||||
|
"Pinned": false,
|
||||||
|
"RepoName": "Test Product",
|
||||||
|
"UserName": "Test User",
|
||||||
|
"DueDate": &dueDate,
|
||||||
|
"CreatedAt": now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"StatusFilter": "",
|
||||||
|
"PriorityFilter": "",
|
||||||
|
"ProductFilter": "Test Product",
|
||||||
|
"ReporterFilter": "Test User",
|
||||||
|
"Products": []string{"Test Product", "Other Product"},
|
||||||
|
"Reporters": []string{"Test User", "Other User"},
|
||||||
|
"Page": 1,
|
||||||
|
"TotalPages": 1,
|
||||||
|
}, user: adminUser},
|
||||||
|
|
||||||
|
"admin/tickets/detail": {data: map[string]interface{}{
|
||||||
|
"Ticket": map[string]interface{}{
|
||||||
|
"ID": ticketID,
|
||||||
|
"Title": "Test Ticket",
|
||||||
|
"Description": "A test description",
|
||||||
|
"Status": "open",
|
||||||
|
"Priority": "high",
|
||||||
|
"Pinned": false,
|
||||||
|
"Assignees": "Dev User",
|
||||||
|
"DueDate": &dueDate,
|
||||||
|
"Attachments": []map[string]interface{}{},
|
||||||
|
"ForgejoIssueNumber": int64(1),
|
||||||
|
"CreatedAt": now,
|
||||||
|
},
|
||||||
|
"User": *testUser,
|
||||||
|
"Repo": repo,
|
||||||
|
"Timeline": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"Type": "comment",
|
||||||
|
"Body": "A comment",
|
||||||
|
"AuthorName": "Dev",
|
||||||
|
"IsTeam": true,
|
||||||
|
"EventText": "",
|
||||||
|
"CreatedAt": now,
|
||||||
|
"Attachments": []map[string]interface{}{},
|
||||||
|
"CommentID": int64(100),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"RelatedIssues": []map[string]interface{}{},
|
||||||
|
"Mentions": map[string]string{},
|
||||||
|
}, user: adminUser},
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenderer expects web/templates relative to cwd; chdir to project root.
|
||||||
|
if err := os.Chdir(filepath.Join("..", "..")); err != nil {
|
||||||
|
t.Fatalf("chdir to project root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := NewRenderer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRenderer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
pd := PageData{
|
||||||
|
User: tc.user,
|
||||||
|
CSRFToken: "test-csrf-token",
|
||||||
|
Data: tc.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, ok := r.templates[name]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("template %q not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, pd); err != nil {
|
||||||
|
t.Errorf("template %q execution failed: %v", name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
{{with .Data}}
|
{{with .Data}}
|
||||||
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter "reporter" .ReporterFilter}}
|
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter "reporter" .ReporterFilter}}
|
||||||
|
{{$productFilter := .ProductFilter}}
|
||||||
|
{{$reporterFilter := .ReporterFilter}}
|
||||||
<div class="mb-4 space-y-2">
|
<div class="mb-4 space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
||||||
<a href="{{filterURL "/admin/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
<a href="{{filterURL "/admin/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||||
{{range .Products}}
|
{{range .Products}}
|
||||||
<a href="{{filterURL "/admin/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ProductFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
<a href="{{filterURL "/admin/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $productFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -34,7 +36,7 @@
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Reporter</span>
|
<span class="text-xs font-medium text-gray-500 uppercase w-16">Reporter</span>
|
||||||
<a href="{{filterURL "/admin/tickets" $filters "reporter" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ReporterFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
<a href="{{filterURL "/admin/tickets" $filters "reporter" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ReporterFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||||
{{range .Reporters}}
|
{{range .Reporters}}
|
||||||
<a href="{{filterURL "/admin/tickets" $filters "reporter" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ReporterFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
<a href="{{filterURL "/admin/tickets" $filters "reporter" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $reporterFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
{{with .Data}}
|
{{with .Data}}
|
||||||
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter}}
|
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter}}
|
||||||
|
{{$productFilter := .ProductFilter}}
|
||||||
<div class="mb-4 space-y-2">
|
<div class="mb-4 space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
||||||
<a href="{{filterURL "/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
<a href="{{filterURL "/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||||
{{range .Products}}
|
{{range .Products}}
|
||||||
<a href="{{filterURL "/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ProductFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
<a href="{{filterURL "/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $productFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue