diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs index 8139d3972b..14d3da398c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs @@ -10,7 +10,7 @@ {{/if}} - + {{#if this.showChatQuoteSuccess}} diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js index ee754a1e75..ad47bb3ac0 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js @@ -85,6 +85,8 @@ export default { I18n.t("dates.long_no_year") ); } + + dateTimeEl.dataset.dateFormatted = true; }); }, { id: "chat-transcript-datetime" } diff --git a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb index adbe0756a4..17092b642b 100644 --- a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb +++ b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb @@ -23,6 +23,10 @@ module PageObjects has_no_css?(".chat-skeleton") end + def has_selection_management? + has_css?(".chat-selection-management") + end + def has_message?(text: nil, id: nil) if text has_css?(".chat-message-text", text: text) diff --git a/plugins/chat/spec/system/transcript_spec.rb b/plugins/chat/spec/system/transcript_spec.rb new file mode 100644 index 0000000000..90f2ecd9f2 --- /dev/null +++ b/plugins/chat/spec/system/transcript_spec.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +RSpec.describe "Quoting chat message transcripts", type: :system, js: true do + fab!(:current_user) { Fabricate(:user) } + fab!(:chat_channel_1) { Fabricate(:chat_channel) } + fab!(:chat_message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) } + fab!(:chat_message_2) { Fabricate(:chat_message, chat_channel: chat_channel_1) } + fab!(:chat_message_3) { Fabricate(:chat_message, chat_channel: chat_channel_1) } + fab!(:chat_message_4) { Fabricate(:chat_message, chat_channel: chat_channel_1) } + fab!(:chat_message_5) { Fabricate(:chat_message, chat_channel: chat_channel_1) } + fab!(:topic) { Fabricate(:with_posts_topic) } + + let(:chat_page) { PageObjects::Pages::Chat.new } + let(:chat_channel_page) { PageObjects::Pages::ChatChannel.new } + let(:topic_page) { PageObjects::Pages::Topic.new } + + before do + chat_system_bootstrap(current_user, [chat_channel_1]) + sign_in(current_user) + end + + def select_message(message, mobile: false) + if page.has_css?(".chat-message-container.selecting-messages") + chat_channel_page.message_by_id(message.id).find(".chat-message-selector").click + else + # we long press instead of hover on mobile + if mobile + chat_channel_page.message_by_id(message.id).click(delay: 0.5) + else + chat_channel_page.message_by_id(message.id).hover + end + + # we also have a different actions menu on mobile + if mobile + find(".chat-message-action-item[data-id=\"selectMessage\"]").click + else + expect(page).to have_css(".chat-message-actions .more-buttons") + find(".chat-message-actions .more-buttons").click + find(".select-kit-row[data-value=\"selectMessage\"]").click + end + end + end + + def cdp_allow_clipboard_access! + cdp_params = { + origin: page.server_url, + permission: { + name: "clipboard-read", + }, + setting: "granted", + } + page.driver.browser.execute_cdp("Browser.setPermission", **cdp_params) + + cdp_params = { + origin: page.server_url, + permission: { + name: "clipboard-write", + }, + setting: "granted", + } + page.driver.browser.execute_cdp("Browser.setPermission", **cdp_params) + end + + def read_clipboard + page.evaluate_async_script("navigator.clipboard.readText().then(arguments[0])") + end + + def click_selection_button(button) + selector = + case button + when "quote" + "#chat-quote-btn" + when "copy" + "#chat-copy-btn" + when "cancel" + "#chat-cancel-selection-btn" + when "move" + "#chat-move-to-channel-btn" + end + within(".chat-selection-management-buttons") { find(selector).click } + end + + def copy_messages_to_clipboard(messages) + messages = Array.wrap(messages) + messages.each { |message| select_message(message) } + expect(chat_channel_page).to have_selection_management + click_selection_button("copy") + expect(page).to have_content("Chat quote copied to clipboard") + clip_text = read_clipboard + expect(clip_text.chomp).to eq(generate_transcript(messages, current_user)) + clip_text + end + + def generate_transcript(messages, acting_user) + messages = Array.wrap(messages) + ChatTranscriptService + .new(messages.first.chat_channel, acting_user, messages_or_ids: messages.map(&:id)) + .generate_markdown + .chomp + end + + describe "copying quote transcripts with the clipboard" do + before { cdp_allow_clipboard_access! } + + it "quotes a single chat message into a topic " do + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + clip_text = copy_messages_to_clipboard(chat_message_5) + + # post transcript in topic + topic_page.visit_topic_and_open_composer(topic) + topic_page.fill_in_composer("This is a new post!\n\n" + clip_text) + expect(page).to have_css(".d-editor-preview .chat-transcript") + topic_page.send_reply + expect(page).to have_content("This is a new post!") + within topic_page.post_by_number(topic.posts.reload.last.post_number) do + expect(page).to have_css(".chat-transcript") + end + end + + it "quotes multiple chat messages into a topic" do + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + messages = [chat_message_5, chat_message_4, chat_message_3, chat_message_2] + clip_text = copy_messages_to_clipboard(messages) + + # post transcript in topic + topic_page.visit_topic_and_open_composer(topic) + topic_page.fill_in_composer("This is a new post!\n\n" + clip_text) + expect(page).to have_css(".d-editor-preview .chat-transcript", count: 4) + expect(page).to have_content("Originally sent in #{chat_channel_1.name}") + topic_page.send_reply + expect(page).to have_content("This is a new post!") + within topic_page.post_by_number(topic.posts.reload.last.post_number) do + expect(page).to have_css(".chat-transcript", count: 4) + end + end + + it "does not error in preview when quoting a chat message with a onebox" do + Oneboxer.stubs(:preview).returns( + "", + ) + chat_message_3.update!(message: "http://www.example.com/has-title.html") + chat_message_3.rebake! + + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + clip_text = copy_messages_to_clipboard(chat_message_3) + + # post transcript in topic + topic_page.visit_topic_and_open_composer(topic) + topic_page.fill_in_composer(clip_text) + + within ".chat-transcript-messages" do + expect(page).to have_content("An interesting article") + end + end + + it "quotes a single chat message into another chat message " do + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + # select message + copy to clipboard + clip_text = copy_messages_to_clipboard(chat_message_5) + click_selection_button("cancel") + + # send transcript message in chat + chat_channel_page.fill_composer(clip_text) + chat_channel_page.click_send_message + message = nil + try_until_success do + message = ChatMessage.find_by(user: current_user, message: clip_text.chomp) + expect(message).not_to eq(nil) + end + expect(chat_channel_page).to have_message(id: message.id) + within chat_channel_page.message_by_id(message.id) do + expect(page).to have_css(".chat-transcript") + end + end + end + + describe "quoting into a topic directly" do + it "opens the topic composer with the quote prefilled and the channel category preselected" do + topic.update!(category: chat_channel_1.chatable) + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + # select message + prefill in composer + select_message(chat_message_5) + click_selection_button("quote") + expect(topic_page).to have_expanded_composer + expect(topic_page).to have_composer_content(generate_transcript(chat_message_5, current_user)) + expect(page).to have_css( + ".category-input .select-kit-header[data-value='#{chat_channel_1.chatable.id}']", + ) + expect(page).not_to have_current_path(chat_channel_1.chatable.url) + + # create the topic with the transcript as the OP + topic_page.fill_in_composer_title("Some topic title for testing") + topic_page.send_reply + expect(page).to have_content("Some topic title for testing") + topic = Topic.where(user: current_user).last + expect(page).to have_current_path(topic.url) + within topic_page.post_by_number(1) do + expect(page).to have_css(".chat-transcript") + end + + # ensure the transcript date is formatted + expect(page).to have_css(".chat-transcript-datetime a[data-date-formatted=\"true\"]") + end + + context "when on mobile" do + it "first navigates to the channel's category before opening the topic composer with the quote prefilled", + mobile: true do + topic.update!(category: chat_channel_1.chatable) + chat_page.visit_channel(chat_channel_1) + expect(chat_channel_page).to have_no_loading_skeleton + expect(chat_channel_page).to have_content(chat_message_5.message) + + # select message + prefill in composer + select_message(chat_message_5, mobile: true) + click_selection_button("quote") + + expect(topic_page).to have_expanded_composer + expect(topic_page).to have_composer_content( + generate_transcript(chat_message_5, current_user), + ) + expect(page).to have_current_path(chat_channel_1.chatable.url) + expect(page).to have_css( + ".category-input .select-kit-header[data-value='#{chat_channel_1.chatable.id}']", + ) + end + end + end +end diff --git a/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js b/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js deleted file mode 100644 index 63569a12ad..0000000000 --- a/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js +++ /dev/null @@ -1,183 +0,0 @@ -import { skip, test } from "qunit"; -import { - click, - currentURL, - tap, - triggerEvent, - visit, -} from "@ember/test-helpers"; -import { - acceptance, - exists, - loggedInUser, - query, - visible, -} from "discourse/tests/helpers/qunit-helpers"; -import { - chatChannels, - generateChatView, -} from "discourse/plugins/chat/chat-fixtures"; -import selectKit from "discourse/tests/helpers/select-kit-helper"; - -const quoteResponse = { - markdown: `[chat quote="martin-chat;3875498;2022-02-04T01:12:15Z" channel="The Beam Discussions" channelId="1234"] - an extremely insightful response :) - [/chat]`, -}; - -function setupPretenders(server, helper) { - server.get("/chat/chat_channels.json", () => helper.response(chatChannels)); - server.post(`/chat/4/quote.json`, () => helper.response(quoteResponse)); - server.post(`/chat/7/quote.json`, () => helper.response(quoteResponse)); - server.get("/chat/:chat_channel_id/messages.json", () => - helper.response(generateChatView(loggedInUser())) - ); - server.post("/uploads/lookup-urls", () => { - return helper.response([]); - }); -} - -acceptance("Discourse Chat | Copying messages", function (needs) { - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: true, - has_chat_enabled: true, - }); - - needs.settings({ - chat_enabled: true, - }); - - needs.pretender((server, helper) => { - setupPretenders(server, helper); - }); - - test("it copies the quote and shows a message", async function (assert) { - await visit("/chat/channel/7/Bug"); - assert.ok(exists(".chat-message-container")); - - const firstMessage = query(".chat-message-container"); - await triggerEvent(firstMessage, "mouseenter"); - const dropdown = selectKit( - `.chat-message-actions-container[data-id="${firstMessage.dataset.id}"] .more-buttons` - ); - await dropdown.expand(); - await dropdown.selectRowByValue("selectMessage"); - assert.ok(firstMessage.classList.contains("selecting-messages")); - - const copyButton = query(".chat-live-pane #chat-copy-btn"); - assert.equal( - copyButton.disabled, - false, - "button is enabled as a message is selected" - ); - - await click(firstMessage.querySelector("input[type='checkbox']")); - assert.equal( - copyButton.disabled, - true, - "button is disabled when no messages are selected" - ); - - await click(firstMessage.querySelector("input[type='checkbox']")); - await click("#chat-copy-btn"); - assert.ok(exists(".chat-selection-message"), "shows the message"); - }); -}); - -acceptance("Discourse Chat | Quoting in composer", async function (needs) { - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: true, - has_chat_enabled: true, - }); - - needs.settings({ - chat_enabled: true, - }); - - needs.pretender((server, helper) => { - setupPretenders(server, helper); - }); - - skip("it opens the composer for the topic and pastes in the quote", async function (assert) { - await visit("/t/internationalization-localization/280"); - - await click(".header-dropdown-toggle.open-chat"); - assert.ok(visible(".chat-drawer-container"), "chat drawer is open"); - assert.ok(exists(".chat-message-container")); - - const firstMessage = query(".chat-message-container"); - await triggerEvent(firstMessage, "mouseenter"); - const dropdown = selectKit(".chat-message-container .more-buttons"); - await dropdown.expand(); - await dropdown.selectRowByValue("selectMessage"); - assert.ok(firstMessage.classList.contains("selecting-messages")); - - await click("#chat-quote-btn"); - assert.ok(exists("#reply-control.composer-action-reply")); - assert.strictEqual( - query(".composer-action-title .action-title").innerText, - "Internationalization / localization" - ); - assert.strictEqual( - query("textarea.d-editor-input").value, - quoteResponse.markdown - ); - }); -}); - -acceptance("Discourse Chat | Quoting on mobile", async function (needs) { - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: true, - has_chat_enabled: true, - }); - - needs.settings({ - chat_enabled: true, - }); - - needs.pretender((server, helper) => { - setupPretenders(server, helper); - }); - - needs.mobileView(); - - skip("it opens the chatable, opens the composer, and pastes the markdown in", async function (assert) { - await visit("/chat/channel/7/Bug"); - assert.ok(exists(".chat-message-container")); - - const firstMessage = query(".chat-message-container"); - await tap(firstMessage); - await click(".chat-message-action-item[data-id='selectMessage'] button"); - assert.ok(firstMessage.classList.contains("selecting-messages")); - - await click("#chat-quote-btn"); - - assert.equal(currentURL(), "/c/bug/1", "navigates to the chatable url"); - assert.ok( - exists("#reply-control.composer-action-createTopic"), - "the composer opens" - ); - assert.strictEqual( - query("textarea.d-editor-input").value, - quoteResponse.markdown, - "the composer has the markdown" - ); - assert.strictEqual( - selectKit(".category-chooser").header().value(), - "1", - "it fills category selector with the right category" - ); - }); -}); diff --git a/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js b/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js deleted file mode 100644 index 98ff2a6b8b..0000000000 --- a/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js +++ /dev/null @@ -1,597 +0,0 @@ -import PrettyText, { buildOptions } from "pretty-text/pretty-text"; -import { emojiUnescape } from "discourse/lib/text"; -import I18n from "I18n"; -import topicFixtures from "discourse/tests/fixtures/topic"; -import { cloneJSON, deepMerge } from "discourse-common/lib/object"; -import QUnit, { test } from "qunit"; - -import { click, fillIn, visit } from "@ember/test-helpers"; -import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; - -const rawOpts = { - siteSettings: { - enable_emoji: true, - enable_emoji_shortcuts: true, - enable_mentions: true, - emoji_set: "twitter", - external_emoji_url: "", - highlighted_languages: "json|ruby|javascript", - default_code_lang: "auto", - enable_markdown_linkify: true, - markdown_linkify_tlds: "com", - chat_enabled: true, - }, - getURL: (url) => url, -}; - -function cookMarkdown(input, opts) { - const merged = deepMerge({}, rawOpts, opts); - return new PrettyText(buildOptions(merged)).cook(input); -} - -QUnit.assert.cookedChatTranscript = function (input, opts, expected, message) { - const actual = cookMarkdown(input, opts); - this.pushResult({ - result: actual === expected, - actual, - expected, - message, - }); -}; - -function generateTranscriptHTML(messageContent, opts) { - const channelDataAttr = opts.channel - ? ` data-channel-name=\"${opts.channel}\"` - : ""; - const channelIdDataAttr = opts.channelId - ? ` data-channel-id=\"${opts.channelId}\"` - : ""; - const reactDataAttr = opts.reactions - ? ` data-reactions=\"${opts.reactionsAttr}\"` - : ""; - - let tabIndexHTML = opts.linkTabIndex ? ' tabindex="-1"' : ""; - - let transcriptClasses = ["chat-transcript"]; - if (opts.chained) { - transcriptClasses.push("chat-transcript-chained"); - } - - const transcript = []; - transcript.push( - `
` - ); - - if (opts.channel && opts.multiQuote) { - let originallySent = I18n.t("chat.quote.original_channel", { - channel: opts.channel, - channelLink: `/chat/channel/${opts.channelId}/-`, - }); - if (opts.linkTabIndex) { - originallySent = originallySent.replace(">", tabIndexHTML + ">"); - } - transcript.push(`
-${originallySent}
`); - } - - const dateTimeText = opts.showDateTimeText - ? moment - .tz(opts.datetime, opts.timezone) - .format(I18n.t("dates.long_no_year")) - : ""; - - const innerDatetimeEl = - opts.noLink || !opts.channelId - ? `${dateTimeText}` - : `${dateTimeText}`; - transcript.push(`
-
-
-${opts.username}
-
-${innerDatetimeEl}
`); - - if (opts.channel && !opts.multiQuote) { - transcript.push( - ` -#${opts.channel}
` - ); - } else { - transcript.push("
"); - } - - let messageHtml = `
\n${messageContent}`; - - if (opts.reactions) { - let reactionsHtml = [`
\n`]; - opts.reactions.forEach((react) => { - reactionsHtml.push( - `
\n${emojiUnescape( - `:${react.emoji}:`, - { lazy: true } - ).replace(/'/g, '"')} ${react.usernames.length}
\n` - ); - }); - reactionsHtml.push(`
\n`); - messageHtml += reactionsHtml.join(""); - } - transcript.push(`${messageHtml}
`); - transcript.push(""); - return transcript.join("\n"); -} - -// these are both set by the plugin with Site.markdown_additional_options which we can't really -// modify the response for here, source of truth are consts in ChatMessage::MARKDOWN_FEATURES -// and ChatMessage::MARKDOWN_IT_RULES -function buildAdditionalOptions() { - return { - chat: { - limited_pretty_text_features: [ - "anchor", - "bbcode-block", - "bbcode-inline", - "code", - "category-hashtag", - "censored", - "discourse-local-dates", - "emoji", - "emojiShortcuts", - "inlineEmoji", - "html-img", - "mentions", - "onebox", - "text-post-process", - "upload-protocol", - "watched-words", - "table", - "spoiler-alert", - ], - limited_pretty_text_markdown_rules: [ - "autolink", - "list", - "backticks", - "newline", - "code", - "fence", - "table", - "linkify", - "link", - "strikethrough", - "blockquote", - "emphasis", - ], - hashtag_configurations: { - "chat-composer": ["channel", "category", "tag"], - }, - }, - }; -} - -acceptance("Discourse Chat | chat-transcript", function (needs) { - let additionalOptions = buildAdditionalOptions(); - - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: false, - has_chat_enabled: false, - timezone: "Australia/Brisbane", - }); - - needs.settings({ - emoji_set: "twitter", - }); - - test("works with a minimal quote bbcode block", function (assert) { - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - }), - "renders the chat message with the required CSS classes and attributes" - ); - }); - - test("renders the channel name if provided with multiQuote", function (assert) { - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - multiQuote: true, - timezone: "Australia/Brisbane", - }), - "renders the chat transcript with the channel name included above the user and datetime" - ); - }); - - test("renders the channel name if provided without multiQuote", function (assert) { - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - timezone: "Australia/Brisbane", - }), - "renders the chat transcript with the channel name included next to the datetime" - ); - }); - - test("renders with the chained attribute for more compact quotes", function (assert) { - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" chained="true"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - multiQuote: true, - chained: true, - timezone: "Australia/Brisbane", - }), - "renders with the chained attribute" - ); - }); - - test("renders with the noLink attribute to remove the links to the individual messages from the datetimes", function (assert) { - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" noLink="true"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - multiQuote: true, - noLink: true, - timezone: "Australia/Brisbane", - }), - "renders with the noLink attribute" - ); - }); - - test("renders with the reactions attribute", function (assert) { - const reactionsAttr = "+1:martin;heart:martin,eviltrout"; - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" reactions="${reactionsAttr}"]\nThis is a chat message.\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML("

This is a chat message.

", { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - timezone: "Australia/Brisbane", - reactionsAttr, - reactions: [ - { emoji: "+1", usernames: ["martin"] }, - { emoji: "heart", usernames: ["martin", "eviltrout"] }, - ], - }), - "renders with the reaction data attribute and HTML" - ); - }); - - test("renders with minimal markdown rules inside the quote bbcode block, same as server-side chat messages", function (assert) { - assert.cookedChatTranscript( - `[chat quote="johnsmith;450;2021-04-25T05:40:39Z"] -[quote="martin, post:3, topic:6215"] -another cool reply -[/quote] -[/chat]`, - { additionalOptions }, - generateTranscriptHTML( - `

[quote="martin, post:3, topic:6215"]
-another cool reply
-[/quote]

`, - { - messageId: "450", - username: "johnsmith", - datetime: "2021-04-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "does not render the markdown feature that has been excluded" - ); - - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis ~~does work~~ with removed _rules_.\n\n* list item 1\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML( - `

This does work with removed rules.

-
    -
  • list item 1
  • -
`, - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "renders correctly when the rule has not been excluded" - ); - - additionalOptions.chat.limited_pretty_text_markdown_rules = [ - "autolink", - // "list", - "backticks", - "newline", - "code", - "fence", - "table", - "linkify", - "link", - // "strikethrough", - "blockquote", - // "emphasis", - ]; - - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis ~~does work~~ with removed _rules_.\n\n* list item 1\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML( - `

This ~~does work~~ with removed _rules_.

-

* list item 1

`, - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "renders correctly with some obvious rules excluded (list/strikethrough/emphasis)" - ); - - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nhere is a message :P with category hashtag #test\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML( - `

here is a message \":stuck_out_tongue:\" with category hashtag #test

`, - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "renders correctly when the feature has not been excluded" - ); - - additionalOptions.chat.limited_pretty_text_features = [ - "anchor", - "bbcode-block", - "bbcode-inline", - "code", - // "category-hashtag", - "censored", - "discourse-local-dates", - "emoji", - // "emojiShortcuts", - "inlineEmoji", - "html-img", - "mentions", - "onebox", - "text-post-process", - "upload-protocolrouter.location.setURL", - "watched-words", - "table", - "spoiler-alert", - ]; - - assert.cookedChatTranscript( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nhere is a message :P with category hashtag #test\n[/chat]`, - { additionalOptions }, - generateTranscriptHTML( - `

here is a message :P with category hashtag #test

`, - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "renders correctly with some obvious features excluded (category-hashtag, emojiShortcuts)" - ); - - assert.cookedChatTranscript( - `This ~~does work~~ with removed _rules_. - -* list item 1 - -here is a message :P with category hashtag #test - -[chat quote="martin;2321;2022-01-25T05:40:39Z"] -This ~~does work~~ with removed _rules_. - -* list item 1 - -here is a message :P with category hashtag #test -[/chat]`, - { additionalOptions }, - `

This does work with removed rules.

-
    -
  • list item 1
  • -
-

here is a message \":stuck_out_tongue:\" with category hashtag #test

\n` + - generateTranscriptHTML( - `

This ~~does work~~ with removed _rules_.

-

* list item 1

-

here is a message :P with category hashtag #test

`, - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - timezone: "Australia/Brisbane", - } - ), - "the rule changes do not apply outside the BBCode [chat] block" - ); - }); -}); - -acceptance( - "Discourse Chat | chat-transcript date decoration", - function (needs) { - let additionalOptions = buildAdditionalOptions(); - - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: true, - has_chat_enabled: true, - timezone: "Australia/Brisbane", - }); - needs.settings({ - chat_enabled: true, - }); - - needs.pretender((server, helper) => { - server.get("/chat/chat_channels.json", () => - helper.response({ - public_channels: [], - direct_message_channels: [], - message_bus_last_ids: { - channel_metadata: 0, - channel_edits: 0, - channel_status: 0, - new_channel: 0, - user_tracking_state: 0, - }, - }) - ); - - const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]); - const firstPost = topicResponse.post_stream.posts[0]; - const postCooked = cookMarkdown( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`, - { additionalOptions } - ); - firstPost.cooked += postCooked; - - server.get("/t/280.json", () => helper.response(topicResponse)); - }); - - test("chat transcript datetimes are formatted into the link with decorateCookedElement", async function (assert) { - await visit("/t/-/280"); - - assert.strictEqual( - query(".chat-transcript-datetime span").innerText.trim(), - moment - .tz("2022-01-25T05:40:39Z", "Australia/Brisbane") - .format(I18n.t("dates.long_no_year")), - "it decorates the chat transcript datetime link with a formatted date" - ); - }); - } -); - -acceptance( - "Discourse Chat - chat-transcript - Composer Oneboxes ", - function (needs) { - let additionalOptions = buildAdditionalOptions(); - needs.user({ - admin: false, - moderator: false, - username: "eviltrout", - id: 1, - can_chat: true, - has_chat_enabled: true, - timezone: "Australia/Brisbane", - }); - needs.settings({ - chat_enabled: true, - enable_markdown_linkify: true, - max_oneboxes_per_post: 2, - }); - needs.pretender((server, helper) => { - server.get("/chat/chat_channels.json", () => - helper.response({ - public_channels: [], - direct_message_channels: [], - message_bus_last_ids: { - channel_metadata: 0, - channel_edits: 0, - channel_status: 0, - new_channel: 0, - user_tracking_state: 0, - }, - }) - ); - - const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]); - const firstPost = topicResponse.post_stream.posts[0]; - const postCooked = cookMarkdown( - `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`, - { additionalOptions } - ); - firstPost.cooked += postCooked; - - server.get("/t/280.json", () => helper.response(topicResponse)); - }); - - test("Preview should not error for oneboxes within [chat] bbcode", async function (assert) { - await visit("/t/internationalization-localization/280"); - await click("#topic-footer-buttons .btn.create"); - - await fillIn( - ".d-editor-input", - ` -[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"] -http://www.example.com/has-title.html -[/chat]` - ); - - const rendered = generateTranscriptHTML( - '

', - { - messageId: "2321", - username: "martin", - datetime: "2022-01-25T05:40:39Z", - channel: "Cool Cats Club", - channelId: "1234", - multiQuote: true, - linkTabIndex: true, - showDateTimeText: true, - timezone: "Australia/Brisbane", - } - ); - - assert.strictEqual( - query(".d-editor-preview").innerHTML.trim(), - rendered.trim(), - "it renders correctly with the onebox inside the [chat] bbcode" - ); - - const textarea = query("#reply-control .d-editor-input"); - await fillIn(".d-editor-input", textarea.value + "\nA"); - assert.ok( - query(".d-editor-preview").innerHTML.trim().includes("\n

A

"), - "it does not error with a opts.discourse.hoisted error in the markdown pipeline when typing more text" - ); - }); - } -); diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb index b65cd48d7f..3254d71dab 100644 --- a/spec/fabricators/topic_fabricator.rb +++ b/spec/fabricators/topic_fabricator.rb @@ -20,6 +20,14 @@ Fabricator(:banner_topic, from: :topic) do archetype Archetype.banner end +Fabricator(:with_posts_topic, from: :topic) do + after_create do |topic| + 3.times do + Fabricate(:post, topic: topic) + end + end +end + Fabricator(:private_message_topic, from: :topic) do transient :recipient category_id { nil } diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb index 5815fb760c..dd8670a5cc 100644 --- a/spec/system/page_objects/pages/topic.rb +++ b/spec/system/page_objects/pages/topic.rb @@ -80,18 +80,34 @@ module PageObjects has_css?("#reply-control.open") end + def find_composer + find("#reply-control .d-editor .d-editor-input") + end + def type_in_composer(input) - find("#reply-control .d-editor .d-editor-input").send_keys(input) + find_composer.send_keys(input) + end + + def fill_in_composer(input) + find_composer.set(input) end def clear_composer - find("#reply-control .d-editor .d-editor-input").set("") + fill_in_composer("") + end + + def has_composer_content?(content) + find_composer.value == content end def send_reply within("#reply-control") { find(".save-or-cancel .create").click } end + def fill_in_composer_title(title) + find("#reply-title").set(title) + end + private def topic_footer_button_id(button)