Replace Trix with Lexxy

This commit is contained in:
Stanko K.R.
2026-07-20 15:10:54 +02:00
parent df5ed25e14
commit 797efa54fd
46 changed files with 678 additions and 495 deletions
+1
View File
@@ -6,4 +6,5 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
include SystemTestHelper
include RichTextEditorHelper
end
+22
View File
@@ -1,6 +1,7 @@
require "test_helper"
class ContentFiltersTest < ActionView::TestCase
include MessagesHelper, ERB::Util
test "entire message contains an unfurled URL" do
text = "https://basecamp.com/"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
@@ -10,6 +11,16 @@ class ContentFiltersTest < ActionView::TestCase
assert_match /<div><action-text-attachment/, filtered.to_html
end
test "entire message contains an unfurled URL in a lexxy body" do
text = "https://basecamp.com/"
body = "<p><a href=\"#{text}\">#{text}</a></p>#{unfurled_link_trix_attachment_for_basecamp}"
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_no_match %r{>\s*https://basecamp\.com/\s*</a>}, filtered.to_html
assert_match /<action-text-attachment/, filtered.to_html
end
test "message includes additional text besides an unfurled URL" do
text = "Hello https://basecamp.com/"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
@@ -53,6 +64,17 @@ class ContentFiltersTest < ActionView::TestCase
assert_match /<div><action-text-attachment/, filtered.to_html
end
test "message keeps strikethrough, underline and code block formatting" do
body = %(<p>Hello <s>struck</s> <u>under</u> <mark>marked</mark></p><pre data-language="ruby">def x<br>end</pre>)
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0016", creator: users(:jason)
html = message_presentation(message)
assert_match %r{<s>struck</s>}, html
assert_match %r{<u>under</u>}, html
assert_match %r{<mark>marked</mark>}, html
assert_match %r{<pre data-language="ruby">}, html
end
test "message contains a forbidden tag" do
exploit_image_tag = 'Hello <img src="https://ssecurityrise.com/tests/billionlaughs-cache.svg">World'
message = Message.create! room: rooms(:pets), body: exploit_image_tag, client_message_id: "0015", creator: users(:jason)
+136
View File
@@ -0,0 +1,136 @@
require "application_system_test_case"
class ComposerTest < ApplicationSystemTestCase
setup do
sign_in "jz@37signals.com"
join_room rooms(:designers)
end
test "enter sends the message when the toolbar is collapsed" do
type_in_composer "A quick reply"
press_in_composer :enter
assert_message_text "A quick reply"
assert_composer_empty
end
test "enter adds a newline in rich text mode and meta+enter sends" do
toggle_rich_text_toolbar
type_in_composer "line one"
press_in_composer :enter
type_in_composer "line two"
assert_no_message_text "line one"
press_in_composer [ :control, :enter ]
assert_message_text /line one\s*line two/
assert_composer_empty
end
test "markdown strikethrough survives sanitization" do
type_in_composer "Hello ~~Claude~~ World"
press_in_composer :enter
assert_selector last_message_selector("s"), text: "Claude"
assert_message_text "Hello Claude World"
end
test "mentioning a user with @ inserts a mention attachment" do
type_in_composer "Hey @Jas"
pick_mention "Jason"
click_send_button
assert_selector last_message_selector(".mention"), text: "Jason"
message = wait_for_persisted_message
assert_includes message.body.body.to_html, "application/vnd.campfire.mention"
assert_equal [ users(:jason) ], message.mentionees
end
test "editing a message with a mention keeps the mention" do
type_in_composer "Hey @Jas"
pick_mention "Jason"
click_send_button
assert_selector last_message_selector(".mention"), text: "Jason"
message = wait_for_persisted_message
within_message message do
reveal_message_actions
find(".message__edit-btn").click
assert_edit_editor_text "Jason"
click_on "Save changes"
end
assert_selector last_message_selector(".mention"), text: "Jason"
assert_equal [ users(:jason) ], message.reload.mentionees
end
test "replying quotes the original message with attribution" do
within_message messages(:third) do
reveal_message_actions
find("[aria-label='Reply']").click
end
assert_composer_text "Third time's a charm."
click_send_button
assert_selector last_message_selector("blockquote"), text: "Third time's a charm."
assert_selector last_message_selector("cite"), text: "JZ"
end
test "arrow up edits my last message when the composer is empty" do
composer_editor.click
press_in_composer :up
assert_selector ".message__body-content--editing"
assert_edit_editor_text "Third time's a charm."
end
test "pasting a URL unfurls an opengraph preview" do
metadata = Opengraph::Metadata.new(
title: "Example Site",
url: "https://example.com/article",
description: "An example article",
image: ""
)
Opengraph::Metadata.stubs(:from_url).returns(metadata)
paste_in_composer "https://example.com/article"
within "#composer" do
assert_selector ".og-embed__title", text: "Example Site", wait: 10
end
click_send_button
assert_selector last_message_selector(".og-embed__title"), text: "Example Site"
end
private
def click_send_button
find("#composer [data-action='composer#submit']").click
end
def last_message_selector(inner)
".message:last-of-type .message__body #{inner}"
end
def assert_no_message_text(text)
assert_no_selector ".message__body", text: text
end
# The message insert races with the form POST, so give the server a moment
def wait_for_persisted_message(room: rooms(:designers), since: 1.minute.ago)
message = nil
20.times do
message = room.messages.ordered.last
break if message.created_at > since
sleep 0.25
end
message
end
end
@@ -0,0 +1,56 @@
# Drives the rich text editor in the composer. Encapsulates all editor-specific
# selectors and interactions so tests describe behavior, not the editor's DOM.
module RichTextEditorHelper
# Rails' built-in helper drives Trix; this drives Lexxy
def fill_in_rich_text_area(id, with:)
find("lexxy-editor##{id}").execute_script("this.value = arguments[0]", with)
end
def composer_editor
find("#composer lexxy-editor .lexxy-editor__content")
end
def type_in_composer(text)
composer_editor.click
composer_editor.send_keys(text)
end
def press_in_composer(*keys)
composer_editor.send_keys(*keys)
end
# The rich text button is only displayed for fine-pointer devices, a media
# query headless Chrome doesn't satisfy, so click it directly.
def toggle_rich_text_toolbar
page.execute_script("document.querySelector('#composer .composer__rich-text-btn').click()")
end
# Waits for the suggestion to appear, then commits the selected one with Tab.
def pick_mention(name)
assert_selector ".lexxy-prompt-menu__item", text: name
composer_editor.send_keys :tab
end
def paste_in_composer(text)
composer_editor.click
page.execute_script(<<~JS, text)
const content = document.querySelector("#composer lexxy-editor .lexxy-editor__content")
const event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: new DataTransfer() })
event.clipboardData.setData("text/plain", arguments[0])
content.dispatchEvent(event)
JS
end
def assert_composer_text(text)
assert_selector "#composer lexxy-editor .lexxy-editor__content", text: text
end
def assert_composer_empty
assert_no_selector "#composer lexxy-editor .lexxy-editor__content", text: /./
end
def assert_edit_editor_text(text)
assert_selector ".message__body-content--editing lexxy-editor", text: text
end
end