ZDDC/zddc/internal/handler/projecthandler_test.go
ZDDC 3fa2762c28 fix(zddc-server): project landing logo links to deployment root
The project landing page at /<project> had its own hand-rolled
header with <svg class="logo"> — not the canonical app-header__logo
class, and not loading shared/logo.js. So the logo on that page was
purely decorative while every other tool's logo (in the same beta
build) was wrapped by shared/logo.js into a clickable link to
/<project>. Inconsistent and surprising — clicking the logo from
mdedit/archive/etc. takes you to project landing, but clicking the
logo on project landing did nothing.

Inline the wrap directly in the template (the page is server-
rendered, so it can't lean on shared/logo.js the way bundled tools
do):

  <a class="app-header__logo-link" href="/" title="ZDDC home">
    <svg class="app-header__logo" ...>...</svg>
  </a>

href="/" because "next up" from the project landing is the
deployment root (the project picker / landing tool).

Also rename .logo → .app-header__logo for visual consistency, and
add the matching hover/focus styles inline. The test asserts both
the wrapping anchor and the canonical class name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 07:46:59 -05:00

125 lines
3.7 KiB
Go

package handler
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
)
func TestIsProjectRootURL(t *testing.T) {
cases := map[string]bool{
"/Project-1": true,
"/Project_2": true,
"/Project-1/": false, // trailing slash
"/Project-1/x": false, // deeper
"/": false, // deployment root
"": false,
}
for path, want := range cases {
if got := IsProjectRootURL(path); got != want {
t.Errorf("IsProjectRootURL(%q) = %v, want %v", path, got, want)
}
}
}
func TestServeProjectLanding(t *testing.T) {
root := t.TempDir()
// Need .zddc on disk for the resolver to be happy at the root.
if err := os.WriteFile(filepath.Join(root, ".zddc"),
[]byte("acl:\n permissions:\n \"*\": rwcda\n"), 0o644); err != nil {
t.Fatal(err)
}
// Project with two parties under archive/ (PascalCase to exercise
// the case-insensitive archive resolver) and one orphaned dot-prefixed
// dir that should be filtered out of the party list.
for _, sub := range []string{
"Project-1/Archive/PartyA",
"Project-1/Archive/PartyB",
"Project-1/Archive/.hidden",
} {
if err := os.MkdirAll(filepath.Join(root, sub), 0o755); err != nil {
t.Fatal(err)
}
}
cfg := config.Config{Root: root}
t.Run("renders project name + stage cards", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/Project-1", nil)
rec := httptest.NewRecorder()
ServeProjectLanding(cfg, rec, req, "Project-1")
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
// Page identifies the project and includes the four stages.
for _, want := range []string{
"Project-1",
"<h3>Archive</h3>",
"<h3>Working</h3>",
"<h3>Staging</h3>",
"<h3>Reviewing</h3>",
"Master Deliverables List",
`href="/Project-1/working"`,
`href="/Project-1/archive/"`,
// Logo wraps to the deployment root — same convention as
// shared/logo.js applies in tools (which would route here
// to /Project-1, the project landing). On the project
// landing itself, "next up" is the deployment root.
`<a class="app-header__logo-link" href="/"`,
`class="app-header__logo"`,
} {
if !strings.Contains(body, want) {
t.Errorf("body missing %q", want)
}
}
})
t.Run("lists existing parties as direct MDL links", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/Project-1", nil)
rec := httptest.NewRecorder()
ServeProjectLanding(cfg, rec, req, "Project-1")
body := rec.Body.String()
// Both parties surfaced; on-disk casing preserved in the URL.
if !strings.Contains(body, `href="/Project-1/Archive/PartyA/mdl/"`) {
t.Errorf("body missing PartyA MDL link")
}
if !strings.Contains(body, `href="/Project-1/Archive/PartyB/mdl/"`) {
t.Errorf("body missing PartyB MDL link")
}
// Dot-prefixed entries filtered out.
if strings.Contains(body, ".hidden") {
t.Errorf("body should not list .hidden directory")
}
})
t.Run("no parties yet → falls back to generic instruction", func(t *testing.T) {
bare := t.TempDir()
if err := os.WriteFile(filepath.Join(bare, ".zddc"),
[]byte("acl:\n permissions:\n \"*\": rwcda\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(bare, "Fresh"), 0o755); err != nil {
t.Fatal(err)
}
bareCfg := config.Config{Root: bare}
req := httptest.NewRequest(http.MethodGet, "/Fresh", nil)
rec := httptest.NewRecorder()
ServeProjectLanding(bareCfg, rec, req, "Fresh")
body := rec.Body.String()
// Falls through to the "no party folders yet" copy.
if !strings.Contains(body, "No party folders yet") {
t.Errorf("body missing fresh-project fallback copy")
}
})
}