Group notification emails by board (#2506)

* Group notification emails by board

Notification bundle emails now group items by board instead of
showing the board name redundantly for each card. Each board
section has a linked header and an <hr> separator.

* Adjust board title styles and remove rem units

* Sort board groups alphabetically

Sort notification email board sections alphabetically by name
(case-insensitive).

Also, rewrite mailer tests to use Nokogiri::HTML5 for precise DOM
assertions instead of regex matching.

---------

Co-authored-by: Andy Smith <andy@37signals.com>
This commit is contained in:
Mike Dalessio
2026-02-09 11:29:11 -05:00
committed by GitHub
parent 692fd73512
commit 8c5b47d774
4 changed files with 94 additions and 29 deletions
+59 -10
View File
@@ -14,11 +14,12 @@ class Notification::BundleMailerTest < ActionMailer::TestCase
test "renders avatar with initials in span when avatar is not attached" do
create_notification(@user)
email = Notification::BundleMailer.notification(@bundle)
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
assert_match /<span[^>]*class="avatar"[^>]*>/, email.html_part.body.to_s
assert_match /#{@user.initials}/, email.html_part.body.to_s
assert_match /style="background-color: #[A-F0-9]{6};?"/, email.html_part.body.to_s
avatar = html.at_css("span.avatar")
assert avatar, "Expected a span.avatar element"
assert_equal @user.initials, avatar.text.strip
assert_match /background-color: #[A-F0-9]{6}/, avatar["style"]
end
test "renders avatar with external image URL when avatar is attached" do
@@ -30,15 +31,63 @@ class Notification::BundleMailerTest < ActionMailer::TestCase
create_notification(@user)
email = Notification::BundleMailer.notification(@bundle)
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
assert_match /<img[^>]*class="avatar"[^>]*>/, email.html_part.body.to_s
assert_match /<img[^>]*class="avatar"[^>]*src="[^"]*"/, email.html_part.body.to_s
assert_match /alt="#{@user.name}"/, email.html_part.body.to_s
avatar = html.at_css("img.avatar")
assert avatar, "Expected an img.avatar element"
assert avatar["src"].present?
assert_equal @user.name, avatar["alt"]
end
test "groups notifications by board, sorted alphabetically" do
private_board = boards(:private)
private_card = Current.with(user: @user) do
private_board.cards.create!(
title: "Private card", creator: @user, status: :published, account: @user.account
)
end
private_event = Event.create!(
creator: @user, board: private_board, eventable: private_card,
action: :card_published, account: @user.account
)
create_notification(@user, source: events(:logo_published))
create_notification(@user, source: private_event)
create_notification(@user, source: events(:layout_published))
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
board_headers = html.css(".notification__board")
assert_equal 2, board_headers.size, "Should have exactly two board headers"
assert_equal [ "Private board", "Writebook" ], board_headers.map(&:text)
end
test "board header links to the board" do
create_notification(@user, source: events(:logo_published))
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
board = boards(:writebook)
link = html.at_css(".notification__board a")
assert_equal "Writebook", link.text
assert_match %r{boards/#{board.id}}, link["href"]
end
test "shows multiple cards under same board header" do
create_notification(@user, source: events(:logo_published))
create_notification(@user, source: events(:layout_published))
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
assert_equal 1, html.css(".notification__board").size, "Same board should only have one header"
card_titles = html.css(".card__title").map(&:text)
assert_includes card_titles, "#1 The logo isn't big enough"
assert_includes card_titles, "#2 Layout is broken"
end
private
def create_notification(user)
Notification.create!(user: user, creator: user, source: events(:logo_published), created_at: 30.minutes.ago)
def create_notification(user, source: events(:logo_published))
Notification.create!(user: user, creator: user, source: source, created_at: 30.minutes.ago)
end
end