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>
125 lines
3.7 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|