From e7832a8d26c6a9ea59d5264e89bfaca4deaf3e63 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 19:44:41 +0100 Subject: [PATCH] test: unit tests for heading conversion, image alt, content facts and related settings - ConvertContentH1 and ConvertTranslationH1 for post bodies and category pillar text - SetImageAlt with several matches, repeated alt attributes and markup that only looks like an img - the content facts outline, images, decorative images and embeds - the related-posts mode and count --- classes/content_facts_test.go | 69 +++++++++++++++++++++++++++++++++++ classes/headings_test.go | 38 +++++++++++++++++++ classes/image_alt_test.go | 25 +++++++++++++ classes/related_test.go | 47 ++++++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 classes/content_facts_test.go create mode 100644 classes/related_test.go diff --git a/classes/content_facts_test.go b/classes/content_facts_test.go new file mode 100644 index 0000000..d03e320 --- /dev/null +++ b/classes/content_facts_test.go @@ -0,0 +1,69 @@ +package classes + +import ( + "testing" + + "git.golem15.com/golem15/summercms/modules/pact" +) + +// TestParseContentFacts pins what a body says about itself: images with +// alt, decorative (alt present and empty) and size attributes (negative +// and invalid sizes 0), an img without src skipped; YouTube embeds of the +// editor's figure; the heading outline with collapsed text, inline tags +// joined, block children spaced, a no-break space collapsed like any +// other, a heading inside a heading read as part +// of the outer one. +func TestParseContentFacts(t *testing.T) { + f := parseContentFacts(`

Lwia grzywa

+

 Lion's mane no src

+
+

Dawkowanie
i forma

Outer
inner
tail

`) + wantImages := []pact.ContentImage{ + {URL: "/m/a.jpg", Alt: "Lion's mane", Width: 800}, + {URL: "/m/line.png", Decorative: true}, + {URL: "/m/b.jpg"}, + } + if len(f.images) != len(wantImages) { + t.Fatalf("images = %+v", f.images) + } + for i, w := range wantImages { + if f.images[i] != w { + t.Errorf("image %d = %+v, want %+v", i, f.images[i], w) + } + } + if len(f.videos) != 1 || f.videos[0] != (pact.ContentVideo{Provider: "youtube", ID: "abc123"}) { + t.Fatalf("videos = %+v", f.videos) + } + wantHeadings := []pact.ContentHeading{{Level: 2, Text: "Lwia grzywa"}, {Level: 3, Text: "Dawkowanie i forma"}, {Level: 4, Text: "Outer inner tail"}, {Level: 2, Text: ""}} + if len(f.headings) != len(wantHeadings) { + t.Fatalf("headings = %+v", f.headings) + } + for i, w := range wantHeadings { + if f.headings[i] != w { + t.Errorf("heading %d = %+v, want %+v", i, f.headings[i], w) + } + } +} + +// TestParseContentFactsEmpty pins an empty body and the category pillar +// text read the same way (headings only). +func TestParseContentFactsEmpty(t *testing.T) { + if f := parseContentFacts(" "); f.images != nil || f.videos != nil || f.headings != nil { + t.Fatalf("blank = %+v", f) + } + pillar := parseContentFacts("

Intro

Gatunki

Skipped

") + if len(pillar.headings) != 2 || pillar.headings[1].Level != 4 { + t.Fatalf("pillar = %+v", pillar.headings) + } +} + +// TestTopHeading pins the record's highest heading level: 2 while the +// H1 setting turns body H1s into H2s, 0 (any) otherwise. +func TestTopHeading(t *testing.T) { + if topHeading(true) != 2 || topHeading(false) != 0 { + t.Fatal("topHeading is wrong") + } + if toolUpdatePost != "journal_update_post" || toolFixHeadings != "journal_fix_headings" || toolSetImageAlt != "journal_set_image_alt" { + t.Fatal("the descriptor's tool names changed") + } +} diff --git a/classes/headings_test.go b/classes/headings_test.go index 0669d6d..cb3f05e 100644 --- a/classes/headings_test.go +++ b/classes/headings_test.go @@ -28,3 +28,41 @@ func TestConvertH1(t *testing.T) { } } } + +// TestConvertContentH1 pins the body pair: content_html's count when its +// HTML had H1s, content's otherwise (a markdown body's H1 is only in its +// rendered HTML), both converted. +func TestConvertContentH1(t *testing.T) { + c, h, n := ConvertContentH1("# Title\n\ntext", "

Title

text

") + if c != "# Title\n\ntext" || h != "

Title

text

" || n != 1 { + t.Fatalf("markdown body = %q %q %d", c, h, n) + } + c, h, n = ConvertContentH1("

a

b

", "") + if c != "

a

b

" || h != "" || n != 2 { + t.Fatalf("HTML body without rendered HTML = %q %q %d", c, h, n) + } + if _, _, n := ConvertContentH1("

x

", "

x

"); n != 0 { + t.Fatal("no H1, nothing converted") + } + if CountH1(`

a

b

c

`) != 2 || CountH1("") != 0 { + t.Fatal("CountH1 is wrong") + } +} + +// TestConvertTranslationH1 pins a locale's translated attributes, +// changed in place: content and content_html converted, other attributes +// and values that are not strings left alone (a category's pillar text +// takes the same path as a post body). +func TestConvertTranslationH1(t *testing.T) { + attrs := map[string]any{"title": "

not content

", "content": "

Pillar

x

", "content_html": 5} + if n := ConvertTranslationH1(attrs); n != 1 || attrs["content"] != "

Pillar

x

" || attrs["title"] != "

not content

" || attrs["content_html"] != 5 { + t.Fatalf("attrs = %v n=%d", attrs, n) + } + both := map[string]any{"content": "# T", "content_html": "

T

"} + if n := ConvertTranslationH1(both); n != 1 || both["content_html"] != "

T

" || both["content"] != "# T" { + t.Fatalf("both = %v n=%d", both, n) + } + if ConvertTranslationH1(nil) != 0 || ConvertTranslationH1(map[string]any{"title": "x"}) != 0 { + t.Fatal("nothing to convert") + } +} diff --git a/classes/image_alt_test.go b/classes/image_alt_test.go index 88d50e1..6b61fb3 100644 --- a/classes/image_alt_test.go +++ b/classes/image_alt_test.go @@ -20,3 +20,28 @@ func TestSetImageAlt(t *testing.T) { t.Fatalf("missing = %q, %d", out, n) } } + +// TestSetImageAltEdges pins the edges: every matching img is rewritten +// and counted (the first one's old alt answered), repeated alt attributes +// collapse to one, a src given escaped does not match the unescaped one, +// an empty src and a body without img change nothing, and text that +// looks like an img inside a comment or script is not touched. +func TestSetImageAltEdges(t *testing.T) { + doc := `one

t

x` + out, n, old := SetImageAlt(doc, "a.jpg", "new") + if n != 2 || old == nil || *old != "one" { + t.Fatalf("n=%d old=%v", n, old) + } + want := `new

t

new` + if out != want { + t.Fatalf("out = %q\nwant %q", out, want) + } + if out, n, _ := SetImageAlt(``, "a.jpg?x=1&y=2", "z"); n != 0 || out != `` { + t.Fatalf("escaped src matched: %q %d", out, n) + } + for _, tc := range []struct{ html, src string }{{``, ""}, {`

no images

`, "a.jpg"}, {"", "a.jpg"}} { + if out, n, old := SetImageAlt(tc.html, tc.src, "x"); out != tc.html || n != 0 || old != nil { + t.Errorf("SetImageAlt(%q, %q) = %q %d %v", tc.html, tc.src, out, n, old) + } + } +} diff --git a/classes/related_test.go b/classes/related_test.go new file mode 100644 index 0000000..69a6acd --- /dev/null +++ b/classes/related_test.go @@ -0,0 +1,47 @@ +package classes + +import ( + "testing" + + "git.golem15.com/golem15/summercms/modules/compass" +) + +// TestReadRelatedSettings pins the related-posts config: the category mode +// and 3 by default; "scored" in any case turns scoring on; the count +// clamped to 3..6, 0 or a non-number falling back to 3. The scoring and +// its ties by the newest post are TestJournalAPIMatrix's on the database. +func TestReadRelatedSettings(t *testing.T) { + if s := ReadRelatedSettings(nil); s.Mode != RelatedModeCategory || s.Count != 3 || s.Scored() { + t.Fatalf("nil config = %+v", s) + } + for _, tc := range []struct { + mode string + count any + want RelatedSettings + }{ + {"", nil, RelatedSettings{RelatedModeCategory, 3}}, + {" Scored ", 5, RelatedSettings{RelatedModeScored, 5}}, + {"scored", 1, RelatedSettings{RelatedModeScored, 3}}, + {"scored", 99, RelatedSettings{RelatedModeScored, 6}}, + {"category", 4, RelatedSettings{RelatedModeCategory, 4}}, + {"tags", 0, RelatedSettings{RelatedModeCategory, 3}}, + {"scored", "many", RelatedSettings{RelatedModeScored, 3}}, + } { + cfg, err := compass.Open(compass.Options{Dir: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + if tc.mode != "" { + _ = cfg.Set(RelatedModeKey, tc.mode) + } + if tc.count != nil { + _ = cfg.Set(RelatedCountKey, tc.count) + } + if got := ReadRelatedSettings(cfg); got != tc.want { + t.Errorf("mode %q count %v = %+v, want %+v", tc.mode, tc.count, got, tc.want) + } + } + if relatedTagPoints != 2 || relatedCategoryPoints != 1 { + t.Fatal("a shared tag is 2 points, a shared category 1") + } +}