aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/web_test.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-18 19:34:35 +0300
committergrm <grm@eyesin.space>2026-09-18 19:34:35 +0300
commit96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f (patch)
treee7851ddadf2ed776ab24a1bad4d446b7c48567db /internal/web/web_test.go
parent63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d (diff)
downloadblogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.tar.gz
blogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.tar.bz2
blogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.zip
Show a live preview of the header on the Design tab's Menu card
The menu options had grown to a dozen switches with nothing to look at but the blog after a save. A new POST /b/{sub}/design/preview reads the form as a save would and renders the header alone — the real blog.css and theme CSS, the modules in their order, the menu as edited — into a sandboxed frame that refreshes shortly after every change, with a Wide screen / Phone toggle. Nothing is stored. The frame keeps the dashboard's origin (allow-same-origin, no scripts) so the blog's logo and fonts, linked through /b/<sub>/media since the root host's /media is the root blog's, get the session cookie; that is safe because no header module is owner HTML. The frame runs no scripts, so the page's own script folds the menu in it the way the blog does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/web_test.go')
-rw-r--r--internal/web/web_test.go37
1 files changed, 34 insertions, 3 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index 12e30f2..e9e38fb 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -216,7 +216,7 @@ func TestHeaderFold(t *testing.T) {
render := func(th Theme, mods []store.Module) string {
var b strings.Builder
l := buildLayout(th, mods)
- v := view{Blog: &store.Blog{Title: "Gizmo"}, Data: map[string]any{"theme": th, "layout": l, "menu": []store.MenuItem{{Label: "Home", URL: "/"}, {Label: "About", URL: "/about"}}}}
+ v := view{Blog: &store.Blog{Title: "Gizmo"}, Media: "/media", Data: map[string]any{"theme": th, "layout": l, "menu": []store.MenuItem{{Label: "Home", URL: "/"}, {Label: "About", URL: "/about"}}}}
if err := tpl.ExecuteTemplate(&b, "siteheader", v); err != nil {
t.Fatal(err)
}
@@ -244,6 +244,37 @@ func TestHeaderFold(t *testing.T) {
}
}
+// The design page's preview is the header alone as a page of its own, with
+// the blog's files linked through the dashboard's path for them.
+func TestDesignPreview(t *testing.T) {
+ tpl, err := newTemplates(false).get("en", "blog/preview.html")
+ if err != nil {
+ t.Fatal(err)
+ }
+ th := DefaultTheme()
+ th.Logo, th.BgImage = uuid.New().String(), uuid.New().String()
+ media := "/b/gizmo/media"
+ l := buildLayout(th, []store.Module{{Area: "header", Kind: "title"}, {Area: "header", Kind: "logo"}, {Area: "header", Kind: "menu"}, {Area: "left", Kind: "recent"}})
+ v := view{Blog: &store.Blog{Title: "Gizmo"}, Lang: "en", Media: media, Data: map[string]any{"theme": th, "css": th.CSSAt(media), "layout": l, "menu": []store.MenuItem{{Label: "About", URL: "/about"}}}}
+ var b strings.Builder
+ if err := tpl.ExecuteTemplate(&b, "preview", v); err != nil {
+ t.Fatal(err)
+ }
+ out := b.String()
+ for _, want := range []string{`<link rel="stylesheet" href="/static/blog.css">`, `<body class="nav-left nav-ul-hover nav-size-normal nav-gap-normal nav-active-underline nav-bold">`,
+ `<img class="logo" src="/b/gizmo/media/` + th.Logo, `background-image:url(/b/gizmo/media/` + th.BgImage, `<a href="/about">About</a>`} {
+ if !strings.Contains(out, want) {
+ t.Errorf("preview missing %q:\n%s", want, out)
+ }
+ }
+ if strings.Contains(out, "mod-recent") || strings.Contains(out, "url(/media/") {
+ t.Errorf("preview: %s", out)
+ }
+ if css := th.CSS(); !strings.Contains(css, "url(/media/"+th.BgImage) {
+ t.Errorf("blog css: %s", css)
+ }
+}
+
// Announcements on post pages are on by default (the look blogs had before
// the switch) and, like every checkbox, off when the box is unticked; stored
// themes from before the option get the default too.
@@ -318,7 +349,7 @@ func TestHeaderRule(t *testing.T) {
logo := uuid.New().String()
render := func(kind string, th Theme) string {
var b strings.Builder
- root := view{Blog: &store.Blog{Title: "Gizmo", Tagline: "a hacklab"}, Data: map[string]any{"theme": th, "layout": Layout{}}}
+ root := view{Blog: &store.Blog{Title: "Gizmo", Tagline: "a hacklab"}, Media: "/media", Data: map[string]any{"theme": th, "layout": Layout{}}}
if err := tpl.ExecuteTemplate(&b, "module", map[string]any{"m": store.Module{Area: "header", Kind: kind}, "root": root}); err != nil {
t.Fatal(err)
}
@@ -748,7 +779,7 @@ func TestAllTemplatesParse(t *testing.T) {
for _, name := range []string{"auth/login.html", "dashboard/overview.html", "dashboard/pages.html", "dashboard/page_form.html",
"dashboard/posts.html", "dashboard/post_form.html", "dashboard/design.html", "dashboard/files.html", "dashboard/settings.html",
"dashboard/password.html", "dashboard/confirm.html", "dashboard/sections.html", "dashboard/section_form.html", "admin/index.html", "admin/new_user.html", "admin/delete_user.html",
- "blog/page.html", "blog/post.html", "blog/tag.html", "blog/search.html", "blog/404.html"} {
+ "blog/page.html", "blog/post.html", "blog/tag.html", "blog/search.html", "blog/404.html", "blog/preview.html"} {
for _, l := range i18n.Languages() {
if _, err := tpl.get(l.Code, name); err != nil {
t.Errorf("%s (%s): %v", name, l.Code, err)