aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/web_test.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-12 19:47:28 +0300
committergrm <grm@eyesin.space>2026-09-12 19:47:28 +0300
commite974eb6f3feb06c8951f85c1d34161938905f807 (patch)
tree3bba878b6764e9d18519efd393d4e780eaabd81a /internal/web/web_test.go
parentdd13e4b73a105ee416b6cfd8eb19b217622c0031 (diff)
downloadblogspace-e974eb6f3feb06c8951f85c1d34161938905f807.tar.gz
blogspace-e974eb6f3feb06c8951f85c1d34161938905f807.tar.bz2
blogspace-e974eb6f3feb06c8951f85c1d34161938905f807.zip
Add a favicon and rework the Design page
Blogs had no icon at all, and browsers probing /favicon.ico ended up rendering the 404 page. Ship a default paper & ink icon (svg + png) and let each blog replace it from the Design page; /favicon.ico now serves the default or redirects to the blog's own image. ICO uploads accepted. The Design page gains colour schemes, reset to defaults, thumbnail image pickers, jump links and a sticky save bar, plus new options: heading font, link underline/hover, content box style and padding, header image height, post dates and format, footer RSS toggle, menu alignment/style/hover and sidebar width. The Menu card picks the position with illustrated tiles and shows only the options that apply to it, using CSS :checked rules rather than JavaScript. All new defaults reproduce the previous look, so existing blogs are unchanged until edited. Theme is jsonb, so no migration. 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.go100
1 files changed, 95 insertions, 5 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index 002f18a..9afa11a 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -6,6 +6,7 @@ import (
"net/url"
"strings"
"testing"
+ "time"
"github.com/google/uuid"
"github.com/gramanas/blogspace/internal/config"
@@ -59,7 +60,7 @@ func TestRootRoutePrecedence(t *testing.T) {
t.Errorf("%s: got %d, want %d", path, rec.Code, want)
}
}
- for _, slug := range []string{"webadmin", "admin", "b", "media", "feed.xml"} {
+ for _, slug := range []string{"webadmin", "admin", "b", "media", "feed.xml", "favicon.ico"} {
if !reservedPageSlugs[slug] {
t.Errorf("page slug %q should be reserved", slug)
}
@@ -67,19 +68,108 @@ func TestRootRoutePrecedence(t *testing.T) {
}
func TestThemeNormalizeAndCSS(t *testing.T) {
- th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","nav_position":"left-sidebar"}`))
- if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.NavPosition != "left-sidebar" {
+ th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","nav_position":"left-sidebar","link_hover":"blue","heading_font":"wingdings","favicon":"x"}`))
+ if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.NavPosition != "left-sidebar" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" {
t.Errorf("normalize: %+v", th)
}
- f := url.Values{"bg_color": {"#ABCDEF"}, "content_width": {"wide"}, "header_show_title": {"on"}, "bg_image": {"not-a-uuid"}}
+ // Themes saved before the new options existed keep today's look.
+ if !th.ShowDates || !th.FooterShowRSS || th.LinkUnderline != "always" || th.ContentStyle != "card" || th.DateFormat != "long" {
+ t.Errorf("old theme defaults: %+v", th)
+ }
+ f := url.Values{"bg_color": {"#ABCDEF"}, "content_width": {"wide"}, "header_show_title": {"on"}, "bg_image": {"not-a-uuid"},
+ "link_hover": {"#ff0000"}, "nav_hover": {"#00ff00"}, "nav_hover_custom": {"on"}, "nav_style": {"uppercase"}, "nav_align": {"center"},
+ "link_underline": {"hover"}, "heading_font": {"serif"}, "content_style": {"flat"}, "content_padding": {"roomy"}, "date_format": {"iso"}}
th = ThemeFromForm(DefaultTheme(), f)
if th.BgColor != "#abcdef" || th.ContentWidth != "wide" || !th.HeaderShowTitle || th.BgImage != "" {
t.Errorf("from form: %+v", th)
}
+ if th.LinkHover != "" || th.NavHover != "#00ff00" || th.ShowDates || th.DateFormat != "iso" {
+ t.Errorf("hover checkbox / unticked checkboxes: %+v", th)
+ }
css := th.CSS()
- if !strings.Contains(css, "background-color:#abcdef") || !strings.Contains(css, "max-width:1100px") || strings.Contains(css, "display:none") {
+ for _, want := range []string{"background-color:#abcdef", "max-width:1100px", "text-transform:uppercase", ".site-nav a:hover { color:#00ff00", "a:hover { text-decoration:underline",
+ "h1, h2, h3, h4, h5, h6 { font-family:Georgia", "text-align:center", "border-radius:0", "padding:2.2em 3em", "@media (max-width: 700px)"} {
+ if !strings.Contains(css, want) {
+ t.Errorf("css missing %q:\n%s", want, css)
+ }
+ }
+ if strings.Contains(css, "display:none") {
t.Errorf("css: %s", css)
}
+ // Default padding is left to blog.css so its phone override keeps working.
+ if css := DefaultTheme().CSS(); strings.Contains(css, "padding:") || strings.Contains(css, "a:hover") {
+ t.Errorf("default css should not set padding or hover: %s", css)
+ }
+}
+
+func TestSidebarWidthCSS(t *testing.T) {
+ th := ThemeFromForm(DefaultTheme(), url.Values{"nav_position": {"left-sidebar"}, "nav_sidebar_width": {"wide"}})
+ css := th.CSS()
+ if !strings.Contains(css, "width:240px") || !strings.Contains(css, "margin-left:260px") || !strings.Contains(css, "margin-left:0") {
+ t.Errorf("sidebar css: %s", css)
+ }
+ // The width only matters for the sidebar layout, and "normal" is blog.css's own value.
+ th.NavPosition = "top-bar"
+ if strings.Contains(th.CSS(), "width:240px") {
+ t.Error("sidebar width leaked into a horizontal menu")
+ }
+ th = ThemeFromForm(DefaultTheme(), url.Values{"nav_position": {"left-sidebar"}})
+ if strings.Contains(th.CSS(), ".nav-left-sidebar") {
+ t.Error("normal width should emit nothing")
+ }
+}
+
+func TestThemeImagePick(t *testing.T) {
+ id := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
+ cur := DefaultTheme()
+ cur.BgImage, cur.Favicon = id, id
+ th := ThemeFromForm(cur, url.Values{})
+ if th.BgImage != id || th.Favicon != id {
+ t.Errorf("missing radio should keep the image: %+v", th)
+ }
+ th = ThemeFromForm(cur, url.Values{"bg_image": {"none"}, "favicon": {"junk"}})
+ if th.BgImage != "" || th.Favicon != "" {
+ t.Errorf("none clears, junk is dropped: %+v", th)
+ }
+ th = ThemeFromForm(DefaultTheme(), url.Values{"header_image": {id}})
+ if th.HeaderImage != id {
+ t.Errorf("radio selects: %+v", th)
+ }
+}
+
+func TestPresets(t *testing.T) {
+ if len(Presets()) < 4 {
+ t.Fatal("expected several presets")
+ }
+ cur := DefaultTheme()
+ cur.BgImage, cur.NavPosition, cur.Font, cur.LinkHover = "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "top-bar", "mono", "#123456"
+ for _, p := range Presets() {
+ for _, c := range []string{p.Bg, p.ContentBg, p.Text, p.Link, p.HeaderBg, p.HeaderText, p.NavBg, p.NavText, p.FooterBg, p.FooterText} {
+ if !hexColor.MatchString(c) {
+ t.Errorf("preset %s: bad colour %q", p.Key, c)
+ }
+ }
+ th := cur.WithPreset(p)
+ if th.BgColor != p.Bg || th.FooterColor != p.FooterText || th.LinkHover != "" {
+ t.Errorf("preset %s not applied: %+v", p.Key, th)
+ }
+ if th.BgImage != cur.BgImage || th.NavPosition != "top-bar" || th.Font != "mono" {
+ t.Errorf("preset %s touched non-colour fields: %+v", p.Key, th)
+ }
+ }
+ if _, ok := PresetByKey("nope"); ok {
+ t.Error("unknown preset should not resolve")
+ }
+}
+
+func TestFormatDate(t *testing.T) {
+ at := time.Date(2026, 9, 3, 10, 0, 0, 0, time.UTC)
+ for format, want := range map[string]string{"long": "3 September 2026", "short": "3 Sep 2026", "iso": "2026-09-03"} {
+ th := Theme{DateFormat: format}
+ if got := th.FormatDate(at); got != want {
+ t.Errorf("%s: got %q, want %q", format, got, want)
+ }
+ }
}
func TestAllTemplatesParse(t *testing.T) {