diff options
| author | grm <grm@eyesin.space> | 2026-09-12 19:47:28 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-12 19:47:28 +0300 |
| commit | e974eb6f3feb06c8951f85c1d34161938905f807 (patch) | |
| tree | 3bba878b6764e9d18519efd393d4e780eaabd81a /internal/web/web_test.go | |
| parent | dd13e4b73a105ee416b6cfd8eb19b217622c0031 (diff) | |
| download | blogspace-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.go | 100 |
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) { |
