Files
warmbly/internal/notify/templates/templates_test.go
T
Matthew Meszaros f04bd26b44 feat: comprehensive unibox + WS latency + dashboard-style transactional emails
User: "inbox is really really bad. So I want all possible ways to
search for an email that we can do... realtime for everything and the
dashboard to show our latency... show how much unread emails." Plus a
follow-up: "I don't like how the emails looks like because they have
that blue gradient, I want dashboard style good one."

Inbox:
- Wired the backend search endpoint (GET /unibox with from/subject/
  unseen/since/until/cursor/limit) — was implemented server-side but
  the frontend was never calling it. Inbox now actually reflects
  server data.
- New UniboxSearchParams model + searchIncoming client + infinite
  useUniboxSearch hook that drops null rows defensively.
- ConversationList: SearchInput (subject substring) + quick-filter
  strip (All / Unread / Today / This week). Unread count surfaces
  in the SectionBar header AND on the Unread chip. Skeleton +
  explicit error block with retry; "Load more · N shown" when more
  pages are available.
- UniboxFilterSheet (advanced filters in the right-side panel):
  free-text query, sender substring, account picker pulled from
  the user's connected mailboxes, status toggle (Any/Unread/Read),
  since/until date pickers with toggle, newest/oldest sort. Draft
  state mirrors parent until Apply.

LivePanel telemetry (sidebar):
- Real WS roundtrip latency. SocketProvider stamps performance.now()
  per heartbeat ref; phoenix phx_reply with that ref computes the
  delta and publishes via setWsLatencyMs. LivePanel colour-codes
  the latency text: <100ms emerald, <300ms amber, ≥300ms red, "—"
  when disconnected.
- Unread count row reads from useAppStore.unseenCount.
- Status label: OFFLINE / CONNECTING / LIVE (with pulse) / IDLE,
  tied to connectionStatus + active mailbox count.

Transactional emails (no more blue gradient):
- base.go rewritten as dashboard chrome: cream #f5f6f8 background,
  white card with hairline #e2e8f0 border, 8px radius, slate-900
  text. Logo monogram in slate, no decorative haze, no gradients.
- login_code / registration_code: tiny uppercase eyebrow + 18px
  bold heading + neutral body + monospace code pill in a hairline-
  bordered box. No serif type.
- reset_password / welcome: same chrome. Slate-900 primary button
  replaces the sky-gradient one. Plaintext link below for accessible
  fallback.
- Template tests updated against the new markup; all green.
2026-05-23 10:07:11 +00:00

320 lines
9.0 KiB
Go

package templates
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ─── Base template (shared chrome) ───────────────────────────────
func TestBaseTemplate_Structure(t *testing.T) {
html, err := GenerateLoginCodeHTML("000000")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
checks := []string{
"<!DOCTYPE html>",
"<html lang=\"en\">",
// Inline SVG logo, dashboard slate fill.
"<svg",
"M222.805 644.772",
"fill=\"#0f172a\"",
// Brae chrome: cream wrapper + white card + hairline border.
"#f5f6f8",
"#ffffff",
"#e2e8f0",
"border-radius:8px",
}
for _, s := range checks {
if !strings.Contains(html, s) {
t.Errorf("base template: expected HTML to contain %q", s)
}
}
}
func TestBaseTemplate_BusinessDetails(t *testing.T) {
html, err := GenerateLoginCodeHTML("000000")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
checks := []string{
// Branding
CompanyName,
"warmbly.com",
"Privacy",
"Terms",
TermsURL,
PrivacyURL,
// Companies Act 2006 required details
LegalEntity,
CompanyNumber,
PlaceOfReg,
RegisteredAddr,
}
for _, s := range checks {
if !strings.Contains(html, s) {
t.Errorf("footer: expected HTML to contain %q", s)
}
}
}
// ─── Login Code ──────────────────────────────────────────────────
func TestGenerateLoginCodeHTML(t *testing.T) {
code := "123456"
html, err := GenerateLoginCodeHTML(code)
if err != nil {
t.Fatalf("GenerateLoginCodeHTML returned error: %v", err)
}
if html == "" {
t.Fatal("GenerateLoginCodeHTML returned empty string")
}
checks := []struct {
name string
substr string
}{
{"contains the verification code", code},
{"code monospace tracking", "letter-spacing:6px"},
{"heading", "Your login code"},
{"sign-in helper", "finish logging in"},
{"expiry notice", "Expires in 15 minutes"},
{"safety notice", "safely ignore"},
{"title tag", "<title>Your Login Code</title>"},
}
for _, c := range checks {
if !strings.Contains(html, c.substr) {
t.Errorf("%s: expected HTML to contain %q", c.name, c.substr)
}
}
}
func TestGenerateLoginCodeHTML_DifferentCodes(t *testing.T) {
codes := []string{"000000", "999999", "ABC123", "1"}
for _, code := range codes {
html, err := GenerateLoginCodeHTML(code)
if err != nil {
t.Fatalf("GenerateLoginCodeHTML(%q) returned error: %v", code, err)
}
if !strings.Contains(html, code) {
t.Errorf("GenerateLoginCodeHTML(%q): code not found in output", code)
}
}
}
// ─── Registration Code ──────────────────────────────────────────
func TestGenerateRegistrationCodeHTML(t *testing.T) {
code := "789012"
html, err := GenerateRegistrationCodeHTML(code)
if err != nil {
t.Fatalf("GenerateRegistrationCodeHTML returned error: %v", err)
}
if html == "" {
t.Fatal("GenerateRegistrationCodeHTML returned empty string")
}
checks := []struct {
name string
substr string
}{
{"contains the verification code", code},
{"welcome heading", "Welcome to Warmbly"},
{"registration prompt", "finish creating your account"},
{"expiry notice", "Expires in 15 minutes"},
{"title tag", "<title>Your Verification Code</title>"},
}
for _, c := range checks {
if !strings.Contains(html, c.substr) {
t.Errorf("%s: expected HTML to contain %q", c.name, c.substr)
}
}
}
func TestGenerateRegistrationCodeHTML_DifferentCodes(t *testing.T) {
codes := []string{"000000", "999999", "ABC123", "1"}
for _, code := range codes {
html, err := GenerateRegistrationCodeHTML(code)
if err != nil {
t.Fatalf("GenerateRegistrationCodeHTML(%q) returned error: %v", code, err)
}
if !strings.Contains(html, code) {
t.Errorf("GenerateRegistrationCodeHTML(%q): code not found in output", code)
}
}
}
// ─── Reset Password ─────────────────────────────────────────────
func TestGenerateResetPasswordHTML(t *testing.T) {
url := "https://app.warmbly.com/reset?token=abc123def456"
html, err := GenerateResetPasswordHTML("", url)
if err != nil {
t.Fatalf("GenerateResetPasswordHTML returned error: %v", err)
}
if html == "" {
t.Fatal("GenerateResetPasswordHTML returned empty string")
}
checks := []struct {
name string
substr string
}{
{"reset URL in button href", url},
{"button text", "Reset password</a>"},
{"reset prompt", "Reset your password"},
{"ignore notice", "safely ignore this email"},
{"expiry notice", "expires in 4 hours"},
{"title tag", "<title>Reset Your Password</title>"},
}
for _, c := range checks {
if !strings.Contains(html, c.substr) {
t.Errorf("%s: expected HTML to contain %q", c.name, c.substr)
}
}
// The reset URL should appear at least twice (button href + plaintext link)
count := strings.Count(html, url)
if count < 2 {
t.Errorf("expected reset URL to appear at least 2 times, got %d", count)
}
}
func TestGenerateResetPasswordHTML_URLEncoding(t *testing.T) {
url := "https://app.warmbly.com/reset?token=abc&user=test@example.com"
html, err := GenerateResetPasswordHTML("", url)
if err != nil {
t.Fatalf("GenerateResetPasswordHTML returned error: %v", err)
}
if !strings.Contains(html, "app.warmbly.com/reset") {
t.Error("expected HTML to contain the reset URL domain")
}
}
// ─── Cross-template checks ──────────────────────────────────────
func TestTemplatesProduceDistinctOutput(t *testing.T) {
loginHTML, err := GenerateLoginCodeHTML("111111")
if err != nil {
t.Fatal(err)
}
regHTML, err := GenerateRegistrationCodeHTML("111111")
if err != nil {
t.Fatal(err)
}
resetHTML, err := GenerateResetPasswordHTML("", "https://example.com/reset")
if err != nil {
t.Fatal(err)
}
if loginHTML == regHTML {
t.Error("login and registration templates should produce different output")
}
if loginHTML == resetHTML {
t.Error("login and reset templates should produce different output")
}
if regHTML == resetHTML {
t.Error("registration and reset templates should produce different output")
}
}
func TestLoginCodeDoesNotContainResetContent(t *testing.T) {
html, _ := GenerateLoginCodeHTML("123456")
if strings.Contains(html, "Reset Password</a>") {
t.Error("login code template should not contain reset password button")
}
if strings.Contains(html, "reset your password") {
t.Error("login code template should not contain reset password text")
}
}
func TestResetPasswordDoesNotContainCodeBlock(t *testing.T) {
html, _ := GenerateResetPasswordHTML("", "https://example.com/reset")
if strings.Contains(html, "letter-spacing:8px") {
t.Error("reset password template should not contain a code display block")
}
if strings.Contains(html, "verify your login") {
t.Error("reset password template should not contain login verify text")
}
}
// ─── Constants ──────────────────────────────────────────────────
// ─── Preview — writes HTML files to a temp dir and prints paths ──
func TestPreview(t *testing.T) {
dir := filepath.Join(os.TempDir(), "warmbly-email-preview")
os.MkdirAll(dir, 0755)
templates := []struct {
name string
gen func() (string, error)
}{
{"login-code.html", func() (string, error) { return GenerateLoginCodeHTML("123456") }},
{"registration-code.html", func() (string, error) { return GenerateRegistrationCodeHTML("789012") }},
{"reset-password.html", func() (string, error) {
return GenerateResetPasswordHTML("", "https://app.warmbly.com/reset?token=abc123def456")
}},
}
for _, tmpl := range templates {
html, err := tmpl.gen()
if err != nil {
t.Fatalf("%s: %v", tmpl.name, err)
}
path := filepath.Join(dir, tmpl.name)
if err := os.WriteFile(path, []byte(html), 0644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
t.Logf("Preview: %s", path)
}
t.Logf("Open all: open %s/*.html", dir)
}
// ─── Constants ──────────────────────────────────────────────────
func TestBusinessConstants(t *testing.T) {
if CompanyName == "" {
t.Error("CompanyName should not be empty")
}
if LegalEntity == "" {
t.Error("LegalEntity should not be empty")
}
if CompanyNumber == "" {
t.Error("CompanyNumber should not be empty")
}
if PlaceOfReg == "" {
t.Error("PlaceOfReg should not be empty")
}
if RegisteredAddr == "" {
t.Error("RegisteredAddr should not be empty")
}
if WebsiteURL == "" {
t.Error("WebsiteURL should not be empty")
}
if !strings.HasPrefix(WebsiteURL, "https://") {
t.Error("WebsiteURL should start with https://")
}
if !strings.HasPrefix(TermsURL, "https://") {
t.Error("TermsURL should start with https://")
}
if !strings.HasPrefix(PrivacyURL, "https://") {
t.Error("PrivacyURL should start with https://")
}
}