package mailhtml import ( "strings" "testing" "unicode/utf8" ) // A designed email: a stylesheet with classes, a media query, a :hover rule, // a hidden preheader and a table layout. This is the shape issue #393 reports. const designed = ` Newsletter
Quick question about your pipeline

Hi Ana

We help teams like Acme book more meetings.

  • Deliverability first
  • Real warmup

Book a demo

` func TestInlineCSSMovesRulesOntoElements(t *testing.T) { out := InlineCSS(designed) for _, want := range []string{ `style="margin: 0; background-color: #f4f4f4`, // body `width: 600px; margin: 0 auto`, // .wrap on the table `style="padding: 16px"`, // td.cell } { if !strings.Contains(out, want) { t.Errorf("expected inlined %q in:\n%s", want, out) } } } func TestInlineCSSHonoursTheCascade(t *testing.T) { out := InlineCSS(designed) // .btn's color is !important, so it beats the anchor's own style attribute. if !strings.Contains(out, "color: #ffffff !important") { t.Errorf("!important sheet rule should beat an inline one:\n%s", out) } if strings.Contains(out, "#eeeeee") { t.Errorf("the losing inline color should not survive:\n%s", out) } // Without !important the author's own inline value wins. plain := InlineCSS(`

x

`) if !strings.Contains(plain, "color: blue") || strings.Contains(plain, "red") { t.Errorf("inline style should beat a sheet rule: %q", plain) } } func TestInlineCSSKeepsWhatItCannotInline(t *testing.T) { out := InlineCSS(designed) if !strings.Contains(out, "@media") { t.Errorf("media queries must stay in the

t

`) if !strings.Contains(out, "color: blue") { t.Errorf("the id selector should win: %q", out) } } func TestInlineCSSShorthandPrecedesLonghand(t *testing.T) { out := InlineCSS(`
c
`) m, mt := strings.Index(out, "margin: 0"), strings.Index(out, "margin-top: 8px") if m < 0 || mt < 0 || m > mt { t.Errorf("shorthand must be written before the longhand that narrows it: %q", out) } } func TestInlineCSSLeavesAFragmentAFragment(t *testing.T) { out := InlineCSS(`

hi

`) if strings.Contains(out, " into the head; dropping the head on // the way out would lose the media query with it. out := InlineCSS(`

hi

`) if !strings.Contains(out, "@media") { t.Errorf("a fragment's media query must survive: %q", out) } } func TestInlineCSSIsANoOpWithoutAStylesheet(t *testing.T) { body := `

Hi {{.FirstName}}, link

` if got := InlineCSS(body); got != body { t.Errorf("a body with no

hi

` out := InlineCSS(body) if strings.Contains(out, `style="color`) { t.Errorf("an opted-out stylesheet must not be inlined: %q", out) } if !strings.Contains(out, ".x{color:red}") { t.Errorf("an opted-out stylesheet must survive verbatim: %q", out) } } func TestInlineCSSSkipsAPrintStylesheet(t *testing.T) { out := InlineCSS(`

hi

`) if strings.Contains(out, `style="color`) { t.Errorf("a print stylesheet is not what the reader sees: %q", out) } } func TestInlineCSSSurvivesBrokenCSS(t *testing.T) { // Unterminated blocks, a comment inside a value, a url() holding braces // and semicolons: none of it may panic or lose the document. for _, css := range []string{ `.a{color:red`, `.a{background:url(data:image/png;base64,AA{BB)}`, `.a{color:/* c */red}`, `@media{`, `.a{content:"}"}`, } { out := InlineCSS(`

hi

`) if !strings.Contains(out, "hi") { t.Errorf("CSS %q lost the body: %q", css, out) } } } func TestInsertBeforeBodyEndIgnoresAConditionalComment(t *testing.T) { // Outlook conditional comments carry their own . Inserting there // put the signature and the opt-out footer inside a comment. body := `

a

` out := InsertBeforeBodyEnd(body, "[F]") if !strings.Contains(out, "[F]") { t.Errorf("insert landed in the conditional comment: %q", out) } } func TestInsertBeforeBodyEndVariants(t *testing.T) { cases := map[string]string{ "

a

": "

a

[F]", "x": "x[F]", "

x

": "

x

[F]", "xy": "xy[F]", "

a

": "

a

[F]", } for in, want := range cases { if got := InsertBeforeBodyEnd(in, "[F]"); got != want { t.Errorf("InsertBeforeBodyEnd(%q):\n got %q\nwant %q", in, got, want) } } } func TestToPlainTextKeepsStructureAndDropsCSS(t *testing.T) { got := ToPlainText(designed) if strings.Contains(got, "font-family") || strings.Contains(got, "@media") || strings.Contains(got, "{") { t.Errorf("the stylesheet must not reach the text part:\n%s", got) } if strings.Contains(got, "Quick question about your pipeline") { t.Errorf("a hidden preheader is written to be read once:\n%s", got) } if !strings.Contains(got, "We help teams like Acme book more meetings.") { t.Errorf("inline elements must not run their words together:\n%s", got) } if !strings.Contains(got, "- Deliverability first\n- Real warmup") { t.Errorf("list items should be bulleted lines:\n%s", got) } if !strings.Contains(got, "Book a demo (https://example.com/demo)") { t.Errorf("a link should keep its destination:\n%s", got) } if strings.Contains(got, "\n\n\n") { t.Errorf("no more than one blank line between blocks:\n%s", got) } } func TestToPlainTextLists(t *testing.T) { got := ToPlainText(`
  1. one
  2. two
`) if !strings.Contains(got, "1. one") || !strings.Contains(got, "2. two") { t.Errorf("ordered items should be numbered: %q", got) } if !strings.Contains(got, "- a") { t.Errorf("unordered items should be dashed: %q", got) } // A