discourse/plugins/chat/spec/system/chat_channel_spec.rb
Joffrey JAFFEUX 90efdd7f9d
PERF: cook message in background (#24227)
This commit starts from a simple observation: cooking messages on the hot path can be slow. Especially with a lot of mentions.

To move cooking from the hot path, this commit has made the following changes:

- updating cooked, inserting mentions and notifying user of new mentions has been moved inside the `process_message` job. It happens right after the `Chat::MessageProcessor` run, which is where the cooking happens.
- the similar existing code in `rebake!` has also been moved to rely on the `process_message`job only
- refactored `create_mentions` and `update_mentions` into one single `upsert_mentions` which can be called invariably
- allows services to decide if their job is ran inline or later. It avoids to need to know you have to use `Jobs.run_immediately!` in this case, in tests it will be inline per default
- made various frontend changes to make the chat-channel component lifecycle clearer. we had to handle `did-update @channel` which was super awkward and creating bugs with listeners which the changes of the PR made clear in failing specs
- adds a new `-processed` (and `-not-processed`) class on the chat message, this is made to have a good lifecyle hook in system specs
2023-11-06 15:45:30 +01:00

366 lines
11 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
RSpec.describe "Chat channel", type: :system do
fab!(:current_user) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
let(:side_panel_page) { PageObjects::Pages::ChatSidePanel.new }
before do
chat_system_bootstrap
channel_1.add(current_user)
sign_in(current_user)
end
context "when has unread threads" do
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel_1) }
before do
channel_1.update!(threading_enabled: true)
thread_1.add(current_user)
Fabricate(:chat_message, thread: thread_1, use_service: true)
end
context "when visiting channel" do
it "opens thread panel" do
chat_page.visit_channel(channel_1)
expect(side_panel_page).to have_open_thread_list
end
end
context "when visiting channel on mobile", mobile: true do
it "doesnt open thread panel" do
chat_page.visit_channel(channel_1)
expect(side_panel_page).to have_no_open_thread_list
end
end
context "when visiting thread" do
it "doesnt open thread panel" do
chat_page.visit_thread(thread_1)
expect(side_panel_page).to have_no_open_thread_list
end
end
context "when opening channel message" do
it "doesnt open thread panel" do
chat_page.visit_channel(channel_1, message_id: message_1.id)
expect(side_panel_page).to have_no_open_thread_list
end
end
end
context "when first batch of messages doesnt fill page" do
before { 30.times { Fabricate(:chat_message, user: current_user, chat_channel: channel_1) } }
it "autofills for more messages" do
chat_page.prefers_full_page
visit("/")
# cheap trick to ensure the messages don't fill the initial page
page.execute_script(
"document.head.insertAdjacentHTML('beforeend', `<style>.chat-message-text{font-size:3px;}</style>`)",
)
sidebar_page.open_channel(channel_1)
expect(channel_page.messages).to have_message(id: message_1.id)
end
end
context "when sending a message" do
context "with lots of messages" do
before { 50.times { Fabricate(:chat_message, chat_channel: channel_1) } }
it "loads most recent messages" do
unloaded_message = Fabricate(:chat_message, chat_channel: channel_1)
chat_page.visit_channel(channel_1, message_id: message_1.id)
expect(channel_page.messages).to have_no_message(id: unloaded_message.id)
channel_page.send_message
expect(channel_page.messages).to have_message(id: unloaded_message.id)
end
end
context "with two sessions opened on same channel" do
it "syncs the messages" do
Jobs.run_immediately!
using_session(:tab_1) do
sign_in(current_user)
chat_page.visit_channel(channel_1)
end
using_session(:tab_2) do
sign_in(current_user)
chat_page.visit_channel(channel_1)
end
using_session(:tab_1) do |session|
channel_page.send_message("test_message")
session.quit
end
using_session(:tab_2) do |session|
expect(channel_page.messages).to have_message(text: "test_message")
session.quit
end
end
end
it "allows to edit this message once persisted" do
chat_page.visit_channel(channel_1)
channel_page.send_message("aaaaaa")
expect(channel_page.messages).to have_message(persisted: true, text: "aaaaaa")
last_message = find(".chat-message-container:last-child")
last_message.hover
expect(channel_page).to have_css(
".chat-message-actions-container[data-id='#{last_message["data-id"]}']",
)
end
end
context "when clicking the arrow button" do
before { 50.times { Fabricate(:chat_message, chat_channel: channel_1) } }
it "jumps to the bottom of the channel" do
unloaded_message = Fabricate(:chat_message, chat_channel: channel_1)
visit("/chat/message/#{message_1.id}")
expect(channel_page).to have_no_loading_skeleton
expect(page).to have_no_css("[data-id='#{unloaded_message.id}']")
find(".chat-scroll-to-bottom__button.visible").click
expect(channel_page).to have_no_loading_skeleton
expect(page).to have_css("[data-id='#{unloaded_message.id}']")
end
end
context "when returning to a channel where last read is not last message" do
it "jumps to the bottom of the channel" do
channel_1.membership_for(current_user).update!(last_read_message: message_1)
messages = 50.times.map { Fabricate(:chat_message, chat_channel: channel_1) }
chat_page.visit_channel(channel_1)
expect(page).to have_css("[data-id='#{messages.first.id}']")
expect(page).to have_no_css("[data-id='#{messages.last.id}']")
end
end
context "when a new message is created" do
fab!(:other_user) { Fabricate(:user) }
before do
channel_1.add(other_user)
50.times { Fabricate(:chat_message, chat_channel: channel_1) }
end
xit "doesnt scroll the pane" do
visit("/chat/message/#{message_1.id}")
new_message = Fabricate(:chat_message, chat_channel: channel_1)
expect(page).to have_no_content(new_message.message)
end
end
context "when a message contains mentions" do
fab!(:other_user) { Fabricate(:user) }
fab!(:message) do
Fabricate(
:chat_message,
chat_channel: channel_1,
message: "hello @here @all @#{current_user.username} @#{other_user.username} @unexisting",
user: other_user,
)
end
before { channel_1.add(other_user) }
it "highlights the mentions" do
chat_page.visit_channel(channel_1)
expect(page).to have_selector(".mention.highlighted.valid-mention", text: "@here")
expect(page).to have_selector(".mention.highlighted.valid-mention", text: "@all")
expect(page).to have_selector(
".mention.highlighted.valid-mention",
text: "@#{current_user.username}",
)
expect(page).to have_selector(".mention", text: "@#{other_user.username}")
expect(page).to have_selector(".mention", text: "@unexisting")
end
it "renders user status on mentions" do
SiteSetting.enable_user_status = true
current_user.set_status!("off to dentist", "tooth")
other_user.set_status!("surfing", "surfing_man")
Fabricate(:chat_mention, user: current_user, chat_message: message)
Fabricate(:chat_mention, user: other_user, chat_message: message)
chat_page.visit_channel(channel_1)
expect(page).to have_selector(
".mention .user-status-message img[alt='#{current_user.user_status.emoji}']",
)
expect(page).to have_selector(
".mention .user-status-message img[alt='#{other_user.user_status.emoji}']",
)
end
end
context "when reply is right under" do
fab!(:other_user) { Fabricate(:user) }
before do
Fabricate(:chat_message, in_reply_to: message_1, user: other_user, chat_channel: channel_1)
channel_1.add(other_user)
end
it "doesnt show the reply-to line" do
chat_page.visit_channel(channel_1)
expect(page).to have_no_selector(".chat-reply__excerpt")
end
end
context "when reply is not directly connected" do
fab!(:other_user) { Fabricate(:user) }
before do
Fabricate(:chat_message, user: other_user, chat_channel: channel_1)
Fabricate(:chat_message, in_reply_to: message_1, user: other_user, chat_channel: channel_1)
channel_1.add(other_user)
end
it "shows the reply-to line" do
chat_page.visit_channel(channel_1)
expect(page).to have_selector(".chat-reply__excerpt")
end
end
context "when replying to message that has HTML tags" do
fab!(:other_user) { Fabricate(:user) }
fab!(:message_2) do
Fabricate(
:chat_message,
user: other_user,
chat_channel: channel_1,
message: "<mark>not marked</mark>",
)
end
before do
Fabricate(:chat_message, user: other_user, chat_channel: channel_1)
Fabricate(:chat_message, in_reply_to: message_2, user: current_user, chat_channel: channel_1)
channel_1.add(other_user)
end
it "renders text in the reply-to" do
chat_page.visit_channel(channel_1)
expect(find(".chat-reply .chat-reply__excerpt")["innerHTML"].strip).to eq(
"&lt;mark&gt;not marked&lt;/mark&gt;",
)
end
it "renders safe HTML like mentions (which are just links) in the reply-to" do
update_message(
message_2,
user: other_user,
text: "@#{other_user.username} <mark>not marked</mark>",
)
chat_page.visit_channel(channel_1)
expect(find(".chat-reply .chat-reply__excerpt")["innerHTML"].strip).to eq(
"@#{other_user.username} &lt;mark&gt;not marked&lt;/mark&gt;",
)
end
end
context "when messages are separated by a day" do
before { Fabricate(:chat_message, chat_channel: channel_1, created_at: 2.days.ago) }
it "shows a date separator" do
chat_page.visit_channel(channel_1)
expect(page).to have_selector(".chat-message-separator__text", text: "Today")
end
end
context "when a message contains code fence" do
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1, message: <<~MESSAGE) }
Here's a message with code highlighting
\`\`\`ruby
Widget.triangulate(arg: "test")
\`\`\`
MESSAGE
it "adds the correct lang" do
chat_page.visit_channel(channel_1)
expect(page).to have_selector("code.lang-ruby")
end
end
context "when scrolling" do
before { 50.times { Fabricate(:chat_message, chat_channel: channel_1) } }
it "resets the active message" do
chat_page.visit_channel(channel_1)
last_message = find(".chat-message-container:last-child")
last_message.hover
expect(page).to have_css(
".chat-message-actions-container[data-id='#{last_message["data-id"]}']",
)
find(".chat-messages-scroll").scroll_to(0, -1000)
expect(page).to have_no_css(
".chat-message-actions-container[data-id='#{last_message["data-id"]}']",
)
end
end
context "when opening message secondary options" do
it "doesnt hide dropdown on mouseleave" do
chat_page.visit_channel(channel_1)
last_message = find(".chat-message-container:last-child")
last_message.hover
expect(page).to have_css(
".chat-message-actions-container[data-id='#{last_message["data-id"]}']",
)
find(".chat-message-actions-container .secondary-actions").click
expect(page).to have_css(
".chat-message-actions-container .secondary-actions .select-kit-body",
)
find("#site-logo").hover
expect(page).to have_css(
".chat-message-actions-container .secondary-actions .select-kit-body",
)
find("#site-logo").click
expect(page).to have_no_css(
".chat-message-actions-container .secondary-actions .select-kit-body",
)
end
end
end