mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-09-22 00:04:55 +09:00
Replace Trix with Lexxy
This commit is contained in:
@@ -6,4 +6,5 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
|
||||
|
||||
include SystemTestHelper
|
||||
include RichTextEditorHelper
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user