Files
sm-journal-plugin/classes/image_alt_test.go
Jakub Zych 7662d54ab3 fix: image alt texts on markdown posts are written into the markdown
POST /_journal/api/v1/images/alt writes the alt of a markdown post into
its source: the inline image ![alt](src) outside code
(classes.SetMarkdownImageAlt) or a raw img outside code
(classes.SetImageAltOutsideCode), so the next content save keeps it. An
image the markdown does not hold that way is refused with 422
changes.N.src and nothing is written.
2026-11-04 23:20:02 +01:00

110 lines
5.2 KiB
Go

package classes
import (
"strings"
"testing"
)
// TestSetImageAlt pins the alt rewrite: only the matching img tag is
// rewritten from its token, the alt is escaped, a missing alt is added and
// every other byte is kept (T-33-04).
func TestSetImageAlt(t *testing.T) {
const doc = `<p class="a">x &amp; y</p><img src="https://cdn.example.test/a.jpg?x=1&amp;y=2"><img src="b.jpg" alt="old"/>`
out, n, old := SetImageAlt(doc, "https://cdn.example.test/a.jpg?x=1&y=2", `Lion's "mane" <b>`)
want := `<p class="a">x &amp; y</p><img src="https://cdn.example.test/a.jpg?x=1&amp;y=2" alt="Lion&#39;s &#34;mane&#34; &lt;b&gt;"><img src="b.jpg" alt="old"/>`
if out != want || n != 1 || old != nil {
t.Fatalf("SetImageAlt = %q, %d, %v; want %q, 1, nil", out, n, old, want)
}
out, n, old = SetImageAlt(doc, "b.jpg", "")
if out != `<p class="a">x &amp; y</p><img src="https://cdn.example.test/a.jpg?x=1&amp;y=2"><img src="b.jpg" alt=""/>` || n != 1 || old == nil || *old != "old" {
t.Fatalf("decorative = %q, %d, %v", out, n, old)
}
if out, n, _ := SetImageAlt(doc, "missing.jpg", "x"); out != doc || n != 0 {
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 := `<img src="a.jpg" alt="one"><p>t</p><img alt="x" src="a.jpg" alt="y"><!-- <img src="a.jpg"> --><script>"<img src=a.jpg>"</script>`
out, n, old := SetImageAlt(doc, "a.jpg", "new")
if n != 2 || old == nil || *old != "one" {
t.Fatalf("n=%d old=%v", n, old)
}
want := `<img src="a.jpg" alt="new"><p>t</p><img alt="new" src="a.jpg"><!-- <img src="a.jpg"> --><script>"<img src=a.jpg>"</script>`
if out != want {
t.Fatalf("out = %q\nwant %q", out, want)
}
if out, n, _ := SetImageAlt(`<img src="a.jpg?x=1&amp;y=2">`, "a.jpg?x=1&amp;y=2", "z"); n != 0 || out != `<img src="a.jpg?x=1&amp;y=2">` {
t.Fatalf("escaped src matched: %q %d", out, n)
}
for _, tc := range []struct{ html, src string }{{`<img src="a.jpg">`, ""}, {`<p>no images</p>`, "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)
}
}
// L1 WR-03: the bytes of an unfinished tag at the end are kept.
for _, in := range []string{"p a <img", "x<y", `<img src="b.jpg"><p>a <b`} {
if out, n, _ := SetImageAlt(in, "a.jpg", "z"); out != in || n != 0 {
t.Errorf("SetImageAlt(%q) = %q %d", in, out, n)
}
}
if out, n, _ := SetImageAlt(`<img src="a.jpg"><p>a <b`, "a.jpg", "z"); out != `<img src="a.jpg" alt="z"><p>a <b` || n != 1 {
t.Errorf("match with an unfinished tail = %q %d", out, n)
}
}
// TestSetMarkdownImageAlt pins P33-65 (L1 WR-05): inline images with or
// without a title get the alt; a destination written unescaped matches the
// src goldmark renders for it; an alt with ] and a line break is escaped
// and folded; the same image in a fence or a code span is left alone; a
// reference-style image is not matched.
func TestSetMarkdownImageAlt(t *testing.T) {
out, n, old := SetMarkdownImageAlt("A ![](/a.jpg) and ![old](/a.jpg \"t\") and ![x](/b.jpg).", "/a.jpg", "Lion's mane")
if n != 2 || old == nil || *old != "" || out != "A ![Lion's mane](/a.jpg) and ![Lion's mane](/a.jpg \"t\") and ![x](/b.jpg)." {
t.Fatalf("plain = %q %d %v", out, n, old)
}
html, err := FormatHTML("![x](</a b.jpg>)")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(html, `src="/a%20b.jpg"`) {
t.Fatalf("goldmark src = %s", html)
}
if out, n, _ := SetMarkdownImageAlt("![x](</a b.jpg>)", "/a%20b.jpg", "y"); n != 1 || out != "![y](</a b.jpg>)" {
t.Fatalf("escaped src = %q %d", out, n)
}
if out, n, _ := SetMarkdownImageAlt("![x](/ż.jpg)", "/%C5%BC.jpg", "y"); n != 1 || out != "![y](/ż.jpg)" {
t.Fatalf("non-ASCII src = %q %d", out, n)
}
if out, _, _ := SetMarkdownImageAlt("![x](/a.jpg)", "/a.jpg", "a ] b\nc [d] \\e"); out != `![a \] b c \[d\] \\e](/a.jpg)` {
t.Fatalf("escaped alt = %q", out)
}
code := "```\n![x](/a.jpg)\n```\n\nInline `![x](/a.jpg)` and\n\n ![x](/a.jpg)\n"
if out, n, _ := SetMarkdownImageAlt(code, "/a.jpg", "y"); n != 0 || out != code {
t.Fatalf("code = %q %d", out, n)
}
ref := "![x][a]\n\n[a]: /a.jpg\n"
if out, n, _ := SetMarkdownImageAlt(ref, "/a.jpg", "y"); n != 0 || out != ref {
t.Fatalf("reference = %q %d", out, n)
}
// The rendered alt follows the markdown.
out, _, _ = SetMarkdownImageAlt("Text\n\n![](/a.jpg)\n", "/a.jpg", "Grzyb [leśny]")
if html, _ := FormatHTML(out); !strings.Contains(html, `alt="Grzyb [leśny]"`) {
t.Fatalf("rendered = %s", html)
}
// SetImageAltOutsideCode: a raw img gets the alt, one in a code sample
// keeps its bytes.
raw := "<img src=\"/a.jpg\">\n\n```html\n<img src=\"/a.jpg\">\n```\n\nand `<img src=\"/a.jpg\">`\n"
got, n, _ := SetImageAltOutsideCode(raw, "/a.jpg", "z")
want := "<img src=\"/a.jpg\" alt=\"z\">\n\n```html\n<img src=\"/a.jpg\">\n```\n\nand `<img src=\"/a.jpg\">`\n"
if n != 1 || got != want {
t.Fatalf("outside code = %q %d", got, n)
}
}