import { withPluginApi } from "discourse/lib/plugin-api"; import { acceptance, exists, loggedInUser, publishToMessageBus, query, queryAll, updateCurrentUser, visible, } from "discourse/tests/helpers/qunit-helpers"; import { click, currentURL, fillIn, focus, settled, triggerEvent, triggerKeyEvent, visit, } from "@ember/test-helpers"; import { skip, test } from "qunit"; import { chatChannels, messageContents, } from "discourse/plugins/chat/chat-fixtures"; import Session from "discourse/models/session"; import { cloneJSON } from "discourse-common/lib/object"; import { joinChannel, leaveChannel, presentUserIds, } from "discourse/tests/helpers/presence-pretender"; import User from "discourse/models/user"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; import * as ajaxModule from "discourse/lib/ajax"; import I18n from "I18n"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import fabricators from "../helpers/fabricators"; import { baseChatPretenders, chatChannelPretender, directMessageChannelPretender, } from "../helpers/chat-pretenders"; acceptance("Discourse Chat - anonymouse 🐭 user", function (needs) { needs.settings({ chat_enabled: true, }); test("doesn't error for anonymous users", async function (assert) { await visit(""); assert.ok(true, "no errors on homepage"); }); }); acceptance("Discourse Chat - without unread", function (needs) { needs.user({ admin: false, moderator: false, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ enable_sidebar: false, chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper); const hawkAsJson = { username: "hawk", id: 2, name: "hawk", avatar_template: "/letter_avatar_proxy/v4/letter/t/41988e/{size}.png", }; server.get("/u/search/users", () => { return helper.response({ users: [hawkAsJson], }); }); server.get("/chat/emojis.json", () => helper.response({ favorites: [{ name: "grinning" }] }) ); server.put("/chat/:chat_channel_id/react/:messageId.json", helper.response); server.put("/chat/:chat_channel_id/invite", helper.response); server.post("/chat/direct_messages/create.json", () => { return helper.response({ chat_channel: { chat_channels: [], chatable: { users: [hawkAsJson] }, chatable_id: 16, chatable_type: "DirectMessage", chatable_url: null, id: 75, title: "@hawk", last_message_sent_at: "2021-11-08T21:26:05.710Z", current_user_membership: { last_read_message_id: null, unread_count: 0, unread_mentions: 0, }, }, }); }); server.post("/chat/chat_channels/:chatChannelId/unfollow.json", () => { return helper.response({ current_user_membership: { following: false } }); }); server.get("/chat/direct_messages.json", () => { return helper.response({ chat_channel: { id: 75, title: "hawk", chatable_type: "DirectMessage", last_message_sent_at: "2021-07-20T08:14:16.950Z", chatable: { users: [{ username: "hawk" }], }, }, }); }); server.get("/u/hawk/card.json", () => { return helper.response({}); }); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); Object.defineProperty(this, "appEvents", { get: () => this.container.lookup("service:appEvents"), }); Session.current().highlightJsPath = "/assets/highlightjs/highlight-test-bundle.min.js"; }); // TODO: needs a future change to how we handle URLS to be possible skip("Clicking mention notification from outside chat opens the float", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.current-user"); await click("#quick-access-notifications .chat-mention"); assert.ok(visible(".chat-drawer-container"), "drawer is open"); assert.ok(query(".chat-drawer-container").classList.contains("channel-9")); }); test("notifications for current user and here/all are highlighted", async function (assert) { updateCurrentUser({ username: "osama" }); await visit("/chat/channel/11/another-category"); // 177 is message id from fixture const highlighted = []; const notHighlighted = []; query(".chat-message-container[data-id='177']") .querySelectorAll(".mention.highlighted") .forEach((node) => { highlighted.push(node.textContent.trim()); }); query(".chat-message-container[data-id='177']") .querySelectorAll(".mention:not(.highlighted)") .forEach((node) => { notHighlighted.push(node.textContent.trim()); }); assert.equal(highlighted.length, 2, "2 mentions are highlighted"); assert.equal(notHighlighted.length, 1, "1 mention is regular mention"); assert.ok(highlighted.includes("@here"), "@here mention is highlighted"); assert.ok(highlighted.includes("@osama"), "@osama mention is highlighted"); assert.ok( notHighlighted.includes("@mark"), "@mark mention is not highlighted" ); }); test("Chat messages are populated when a channel is entered and images are rendered", async function (assert) { await visit("/chat/channel/11/another-category"); const messages = queryAll(".chat-message .chat-message-text"); assert.equal(messages[0].innerText.trim(), messageContents[0]); assert.ok(messages[1].querySelector("a.chat-other-upload")); assert.equal( messages[2].innerText.trim().split("\n")[0], messageContents[2] ); assert.ok(messages[2].querySelector("img.chat-img-upload")); }); test("Reply-to line is hidden when reply-to message is directly above", async function (assert) { await visit("/chat/channel/11/another-category"); const messages = queryAll(".chat-message-container"); assert.notOk(messages[1].querySelector(".chat-reply__excerpt")); }); test("Reply-to line is present when reply-to message is not directly above", async function (assert) { await visit("/chat/channel/11/another-category"); const messages = queryAll(".chat-message-container"); const replyTo = messages[2].querySelector(".chat-reply__excerpt"); assert.ok(replyTo); assert.equal(replyTo.innerText.trim(), messageContents[0]); }); test("Unfollowing a direct message channel transitions to another channel", async function (assert) { await visit("/chat/channel/75/@hawk"); await click( ".chat-channel-row.chat-channel-75 .toggle-channel-membership-button.-leave" ); assert.ok(/^\/chat\/channel\/4/.test(currentURL())); }); test("Admin only controls are present", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); const currentUserDropdown = selectKit( ".chat-message-actions-container[data-id='174'] .more-buttons" ); await currentUserDropdown.expand(); assert.notOk( currentUserDropdown.rowByValue("rebakeMessage").exists(), "it doesn’t show the rebake button for non staff" ); updateCurrentUser({ admin: true, moderator: true }); await visit("/chat"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); await currentUserDropdown.expand(); assert.ok( currentUserDropdown.rowByValue("rebakeMessage").exists(), "it shows the rebake button" ); assert.notOk( currentUserDropdown.rowByValue("silence").exists(), "it hides the silence button" ); const notCurrentUserDropdown = selectKit( ".chat-message-actions-container[data-id='175'] .more-buttons" ); await triggerEvent(".chat-message-container[data-id='175']", "mouseenter"); await notCurrentUserDropdown.expand(); assert.ok( notCurrentUserDropdown.rowByValue("silence").exists(), "it shows the silence button" ); }); test("Message controls are present and correct for permissions", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); // User created this message assert.ok( ".chat-message-actions-container[data-id='174'] .reply-btn", "it shows the reply button" ); const currentUserDropdown = selectKit( ".chat-message-actions-container[data-id='174'] .more-buttons" ); await currentUserDropdown.expand(); assert.ok( currentUserDropdown.rowByValue("copyLinkToMessage").exists(), "it shows the link to button" ); assert.notOk( currentUserDropdown.rowByValue("rebakeMessage").exists(), "it doesn’t show the rebake button to a regular user" ); assert.ok( currentUserDropdown.rowByValue("edit").exists(), "it shows the edit button" ); assert.notOk( currentUserDropdown.rowByValue("flag").exists(), "it hides the flag button" ); assert.notOk( currentUserDropdown.rowByValue("silence").exists(), "it hides the silence button" ); assert.ok( currentUserDropdown.rowByValue("deleteMessage").exists(), "it shows the delete button" ); // User _didn't_ create this message await triggerEvent(".chat-message-container[data-id='175']", "mouseenter"); assert.ok( ".chat-message-actions-container[data-id='175'] .reply-btn", "it shows the reply button" ); const notCurrentUserDropdown = selectKit( ".chat-message-actions-container[data-id='175'] .more-buttons" ); await notCurrentUserDropdown.expand(); assert.ok( notCurrentUserDropdown.rowByValue("copyLinkToMessage").exists(), "it shows the link to button" ); assert.notOk( notCurrentUserDropdown.rowByValue("edit").exists(), "it hides the edit button" ); assert.notOk( notCurrentUserDropdown.rowByValue("deleteMessage").exists(), "it hides the delete button" ); }); test("pressing the reply button adds the indicator to the composer", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); await click(".reply-btn"); assert.ok( exists(".chat-composer-message-details .d-icon-reply"), "Reply icon is present" ); assert.equal( query( ".chat-composer-message-details .chat-reply__username" ).innerText.trim(), "markvanlan" ); }); test("pressing the edit button fills the composer and indicates edit", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); const dropdown = selectKit(".more-buttons"); await dropdown.expand(); await dropdown.selectRowByValue("edit"); assert.ok( exists(".chat-composer-message-details .d-icon-pencil-alt"), "Edit icon is present" ); assert.equal( query( ".chat-composer-message-details .chat-reply__username" ).innerText.trim(), "markvanlan" ); assert.equal( query(".chat-composer-input").value.trim(), messageContents[0] ); }); test("Reply-to is stored in draft", async function (assert) { await visit("/latest"); await click(".header-dropdown-toggle.open-chat"); await click(".chat-channel-row"); await settled(); await click(".chat-drawer-header__return-to-channels-btn"); await click(".chat-channel-row.chat-channel-9"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); await click(".chat-message-actions-container[data-id='174'] .reply-btn"); // Reply-to line is present assert.ok(exists(".chat-composer-message-details .chat-reply")); await click(".chat-drawer-header__return-to-channels-btn"); await click(".chat-channel-row.chat-channel-11"); // Reply-to line is gone since switching channels assert.notOk(exists(".chat-composer-message-details .chat-reply")); // Now click on reply btn and cancel it on channel 7 await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); await click(".chat-message-actions-container[data-id='174'] .reply-btn"); await click(".cancel-message-action"); // Go back to channel 9 and check that reply-to is present await click(".chat-drawer-header__return-to-channels-btn"); await click(".chat-channel-row.chat-channel-9"); // Now reply-to should be back and loaded from draft assert.ok(exists(".chat-composer-message-details .chat-reply")); // Go back one for time to channel 7 and make sure reply-to is gone await click(".chat-drawer-header__return-to-channels-btn"); await click(".chat-channel-row.chat-channel-11"); assert.notOk(exists(".chat-composer-message-details .chat-reply")); }); test("Sending a message", async function (assert) { await visit("/chat/channel/11/another-category"); const messageContent = "Here's a message"; const composerInput = query(".chat-composer-input"); assert.deepEqual( presentUserIds("/chat-reply/11"), [], "is not present before typing" ); await fillIn(composerInput, messageContent); assert.deepEqual( presentUserIds("/chat-reply/11"), [User.current().id], "is present after typing" ); await focus(composerInput); await triggerKeyEvent(composerInput, "keydown", "Enter"); assert.equal(document.activeElement, composerInput); assert.equal(composerInput.innerText.trim(), "", "composer input cleared"); assert.deepEqual( presentUserIds("/chat-reply/11"), [], "stops being present after sending message" ); let messages = queryAll(".chat-message"); let lastMessage = messages[messages.length - 1]; // Message is staged, without an ID assert.ok(lastMessage.classList.contains("chat-message-staged")); // Last message was from a different user; full meta data is shown assert.ok( lastMessage.querySelector(".chat-user-avatar"), "Avatar is present" ); assert.ok( lastMessage.querySelector(".chat-message-info__username__name"), "Username is present" ); assert.equal( lastMessage.querySelector(".chat-message-text").innerText.trim(), this.siteSettings.enable_markdown_typographer ? "Here’s a message" : messageContent ); await publishToMessageBus("/chat/11", { type: "sent", stagedId: 1, chat_message: { id: 202, user: { id: 1, }, cooked: messageContent + " some extra cooked stuff", }, }); assert.equal( lastMessage.closest(".chat-message-container").dataset.id, 202 ); assert.notOk(lastMessage.classList.contains("chat-message-staged")); assert.equal( lastMessage.querySelector(".chat-message-text").innerText.trim(), messageContent + " some extra cooked stuff", "last message is updated with the cooked content of the message" ); const nextMessageContent = "What up what up!"; await fillIn(composerInput, nextMessageContent); await focus(composerInput); await triggerKeyEvent(composerInput, "keydown", "Enter"); messages = queryAll(".chat-message"); lastMessage = messages[messages.length - 1]; // We just sent a message so avatar/username will not be present for the last message assert.notOk( lastMessage.querySelector(".chat-user-avatar"), "Avatar is not shown" ); assert.notOk( lastMessage.querySelector(".full-name"), "Username is not shown" ); assert.equal( lastMessage.querySelector(".chat-message-text").innerText.trim(), nextMessageContent ); }); test("cooked processing messages are handled properly", async function (assert) { await visit("/chat/channel/11/another-category"); const cooked = "

hello there

"; await publishToMessageBus(`/chat/11`, { type: "processed", chat_message: { cooked, id: 175, }, }); assert.ok( query( ".chat-message-container[data-id='175'] .chat-message-text" ).innerHTML.includes(cooked) ); }); test("Code highlighting in a message", async function (assert) { await visit("/chat/channel/11/another-category"); const messageContent = `Here's a message with code highlighting \`\`\`ruby Widget.triangulate(arg: "test") \`\`\``; const composerInput = query(".chat-composer-input"); await fillIn(composerInput, messageContent); await focus(composerInput); await triggerKeyEvent(composerInput, "keydown", "Enter"); await publishToMessageBus("/chat/11", { type: "sent", stagedId: 1, chat_message: { id: 202, cooked: `
Widget.triangulate(arg: "test")
      
`, user: { id: 1, }, }, }); const messages = queryAll(".chat-message"); const lastMessage = messages[messages.length - 1]; assert.equal( lastMessage.closest(".chat-message-container").dataset.id, 202 ); assert.ok( exists( ".chat-message-container[data-id='202'] .chat-message-text code.lang-ruby.hljs" ), "chat message code block has been highlighted as ruby code" ); }); test("Drafts are saved and reloaded", async function (assert) { await visit("/chat/channel/11/another-category"); await fillIn(".chat-composer-input", "Hi people"); await visit("/chat/channel/75/@hawk"); assert.equal(query(".chat-composer-input").value.trim(), ""); await fillIn(".chat-composer-input", "What up what up"); await visit("/chat/channel/11/another-category"); assert.equal(query(".chat-composer-input").value.trim(), "Hi people"); await fillIn(".chat-composer-input", ""); await visit("/chat/channel/75/@hawk"); assert.equal(query(".chat-composer-input").value.trim(), "What up what up"); // Send a message const composerTextarea = query(".chat-composer-input"); await focus(composerTextarea); await triggerKeyEvent(composerTextarea, "keydown", "Enter"); assert.equal(query(".chat-composer-input").value.trim(), ""); // Navigate away and back to make sure input didn't re-fill await visit("/chat/channel/11/another-category"); await visit("/chat/channel/75/@hawk"); assert.equal(query(".chat-composer-input").value.trim(), ""); }); test("Pressing escape cancels editing", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container[data-id='174']", "mouseenter"); const dropdown = selectKit(".more-buttons"); await dropdown.expand(); await dropdown.selectRowByValue("edit"); assert.ok(exists(".chat-composer-message-details")); await triggerKeyEvent(".chat-composer", "keydown", "Escape"); // chat-composer-message-details will be gone as no message is being edited assert.notOk(exists(".chat-composer .chat-composer-message-details")); }); test("Unread indicator increments for public channels when messages come in", async function (assert) { await visit("/t/internationalization-localization/280"); assert.notOk( exists(".header-dropdown-toggle.open-chat .chat-channel-unread-indicator") ); await publishToMessageBus("/chat/9/new-messages", { message_id: 201, user_id: 2, }); assert.ok( exists(".header-dropdown-toggle.open-chat .chat-channel-unread-indicator") ); }); test("Unread count increments for direct message channels when messages come in", async function (assert) { await visit("/t/internationalization-localization/280"); assert.notOk( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number" ) ); await publishToMessageBus("/chat/75/new-messages", { message_id: 201, user_id: 2, }); assert.ok( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number" ) ); assert.equal( query( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number" ).innerText.trim(), 1 ); }); test("Unread DM count overrides the public unread indicator", async function (assert) { await visit("/t/internationalization-localization/280"); await publishToMessageBus("/chat/9/new-messages", { message_id: 201, user_id: 2, }); await publishToMessageBus("/chat/75/new-messages", { message_id: 202, user_id: 2, }); assert.ok( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number" ) ); assert.notOk( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator:not(.urgent)" ) ); }); test("Mentions in public channels show the unread urgent indicator", async function (assert) { await visit("/t/internationalization-localization/280"); await publishToMessageBus("/chat/9/new-mentions", { message_id: 201, }); assert.ok( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number" ) ); assert.notOk( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator:not(.urgent)" ) ); }); test("message selection and live pane buttons for regular user", async function (assert) { updateCurrentUser({ admin: false, moderator: false }); await visit("/chat/channel/11/another-category"); 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")); assert.ok(exists("#chat-quote-btn")); }); test("message selection is not present for regular user", async function (assert) { updateCurrentUser({ admin: false, moderator: false }); await visit("/chat/channel/11/another-category"); assert.notOk( exists( ".chat-message-container .chat-message-actions-container .select-btn" ) ); }); test("creating a new direct message channel works", async function (assert) { await visit("/chat/channel/11/another-category"); await click(".open-draft-channel-page-btn"); await fillIn(".filter-usernames", "hawk"); await click("li.user[data-username='hawk']"); assert.notOk( query(".join-channel-btn"), "Join channel button is not present" ); const enabledComposer = document.querySelector(".chat-composer-input"); assert.ok(!enabledComposer.disabled); assert.equal( enabledComposer.placeholder, I18n.t("chat.placeholder_start_conversation", { usernames: "hawk" }) ); }); test("creating a new direct message channel from popup chat works", async function (assert) { await visit("/chat"); await click(".open-draft-channel-page-btn"); await fillIn(".filter-usernames", "hawk"); await click('.chat-user-avatar-container[data-user-card="hawk"]'); assert.ok(query(".selected-user").innerText, "hawk"); }); test("Reacting works with no existing reactions", async function (assert) { await visit("/chat/channel/11/another-category"); const message = query(".chat-message-container"); await triggerEvent(message, "mouseenter"); assert.notOk(message.querySelector(".chat-message-reaction-list")); await click(".chat-message-actions .react-btn"); await click(`.chat-emoji-picker .emoji[alt="grinning"]`); assert.ok(message.querySelector(".chat-message-reaction-list")); const reaction = message.querySelector( ".chat-message-reaction-list .chat-message-reaction.reacted" ); assert.ok(reaction); assert.equal(reaction.querySelector(".count").innerText.trim(), 1); }); test("Reacting works with existing reactions", async function (assert) { await visit("/chat/channel/11/another-category"); const messages = queryAll(".chat-message-container"); // First 2 messages have no reactions; make sure the list isn't rendered assert.notOk(messages[0].querySelector(".chat-message-reaction-list")); assert.notOk(messages[1].querySelector(".chat-message-reaction-list")); const lastMessage = messages[2]; assert.ok(lastMessage.querySelector(".chat-message-reaction-list")); assert.equal( lastMessage.querySelectorAll(".chat-message-reaction.reacted").length, 2 ); assert.equal( lastMessage.querySelectorAll(".chat-message-reaction:not(.reacted)") .length, 1 ); // React with a heart and make sure the count increments and class is added const heartReaction = lastMessage.querySelector( `.chat-message-reaction[data-emoji-name="heart"]` ); assert.equal(heartReaction.innerText.trim(), "1"); await click(heartReaction); assert.equal(heartReaction.innerText.trim(), "2"); assert.ok(heartReaction.classList.contains("reacted")); await publishToMessageBus("/chat/11", { action: "add", user: { id: 1, username: "eviltrout" }, emoji: "heart", type: "reaction", chat_message_id: 176, }); // Click again make sure count goes down await click(heartReaction); assert.equal(heartReaction.innerText.trim(), "1"); assert.notOk(heartReaction.classList.contains("reacted")); // Message from another user coming in! await publishToMessageBus("/chat/11", { action: "add", user: { id: 77, username: "rando" }, emoji: "sneezing_face", type: "reaction", chat_message_id: 176, }); const sneezingFaceReaction = lastMessage.querySelector( `.chat-message-reaction[data-emoji-name="sneezing_face"]` ); assert.ok(sneezingFaceReaction); assert.equal(sneezingFaceReaction.innerText.trim(), "1"); assert.notOk(sneezingFaceReaction.classList.contains("reacted")); await click(sneezingFaceReaction); assert.equal(sneezingFaceReaction.innerText.trim(), "2"); assert.ok(sneezingFaceReaction.classList.contains("reacted")); }); test("Reacting and unreacting works on newly created chat messages", async function (assert) { await visit("/chat/channel/11/another-category"); const composerInput = query(".chat-composer-input"); await fillIn(composerInput, "hellloooo"); await focus(composerInput); await triggerKeyEvent(composerInput, "keydown", "Enter"); const messages = queryAll(".chat-message-container"); const lastMessage = messages[messages.length - 1]; await publishToMessageBus("/chat/11", { type: "sent", stagedId: 1, chat_message: { id: 202, user: { id: 1, }, cooked: "

hellloooo

", }, }); assert.deepEqual(lastMessage.dataset.id, "202"); await triggerEvent(lastMessage, "mouseenter"); await click( `.chat-message-actions-container[data-id="${lastMessage.dataset.id}"] .react-btn` ); await click(`.emoji[alt="grinning"]`); const reaction = lastMessage.querySelector( `.chat-message-reaction.reacted[data-emoji-name="grinning"]` ); await publishToMessageBus("/chat/11", { action: "add", user: { id: 1, username: "eviltrout" }, emoji: "grinning", type: "reaction", chat_message_id: 202, }); await click(reaction); assert.notOk( lastMessage.querySelector( `.chat-message-reaction.reacted[data-emoji-name="grinning"]` ) ); }); test("mention warning is rendered", async function (assert) { await visit("/chat/channel/11/another-category"); await publishToMessageBus("/chat/11", { type: "mention_warning", cannot_see: [{ id: 75, username: "hawk" }], without_membership: [ { id: 76, username: "eviltrout" }, { id: 77, username: "sam" }, ], chat_message_id: 176, }); assert.ok( exists( ".chat-message-container[data-id='176'] .chat-message-mention-warning" ) ); assert.ok( query( ".chat-message-container[data-id='176'] .chat-message-mention-warning .cannot-see" ).innerText.includes("hawk") ); const withoutMembershipText = query( ".chat-message-container[data-id='176'] .chat-message-mention-warning .without-membership" ).innerText; assert.ok(withoutMembershipText.includes("eviltrout")); assert.ok(withoutMembershipText.includes("sam")); await click( ".chat-message-container[data-id='176'] .chat-message-mention-warning .invite-link" ); assert.notOk( exists( ".chat-message-container[data-id='176'] .chat-message-mention-warning" ) ); }); test("It displays a separator between days", async function (assert) { await visit("/chat/channel/11/another-category"); assert.equal( query(".first-daily-message").innerText.trim(), "July 22, 2021" ); }); test("pressing keys focuses composer in full page chat", async function (assert) { await visit("/chat/channel/11/another-category"); document.activeElement.blur(); await triggerKeyEvent(document.body, "keydown", 65); // 65 is `a` keycode let composer = query(".chat-composer-input"); assert.equal(composer.value, "a"); assert.equal(document.activeElement, composer); document.activeElement.blur(); await triggerKeyEvent(document.body, "keydown", 65); assert.equal(composer.value, "aa"); assert.equal(document.activeElement, composer); document.activeElement.blur(); await triggerKeyEvent(document.body, "keydown", 191); // 191 is `?` assert.notEqual( document.activeElement, composer, "? is a special case and should not focus" ); document.activeElement.blur(); await triggerKeyEvent(document.body, "keydown", "Enter"); assert.notEqual( document.activeElement, composer, "enter is a special case and should not focus" ); }); test("changing channel resets message selection", async function (assert) { await visit("/chat/channel/11/another-category"); await triggerEvent(".chat-message-container", "mouseenter"); const dropdown = selectKit(".chat-message-actions .more-buttons"); await dropdown.expand(); await dropdown.selectRowByValue("selectMessage"); await click("#chat-copy-btn"); await click("#chat-channel-row-9"); assert.notOk(exists("#chat-copy-btn")); }); }); acceptance( "Discourse Chat - Acceptance Test with unread public channel 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) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper, [ { id: 11, unread_count: 2, muted: false }, ]); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); }); test("Expand button takes you to full page chat on the correct channel", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); await click(".chat-drawer-header__full-screen-btn"); assert.equal(currentURL(), `/chat/channel/11/another-category`); }); test("Unread header indicator is present", async function (assert) { await visit("/t/internationalization-localization/280"); assert.ok( exists( ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator" ), "Unread indicator present in header" ); }); } ); acceptance( "Discourse Chat - Acceptance Test show/hide close fullscreen chat button", 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) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper, [ { id: 9, unread_count: 2, muted: false }, ]); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); }); test("Close fullscreen chat button present", async function (assert) { await visit("/chat/channel/11/another-category"); assert.ok(exists(".open-drawer-btn")); }); } ); acceptance( "Discourse Chat - Expand and collapse drawer (chat-drawer)", 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) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper, [ { id: 9, unread_count: 2, muted: false }, ]); server.get("/chat/api/chat_channels/:id/memberships.json", () => { return helper.response([]); }); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); }); test("drawer can be collapsed and expanded", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); assert.ok( visible(".chat-drawer-header__top-line--expanded"), "drawer is expanded" ); await click(".chat-drawer-header__expand-btn"); assert.ok( visible(".chat-drawer-header__top-line--collapsed"), "drawer is collapsed" ); await click(".chat-drawer-header__expand-btn"); assert.ok( visible(".chat-drawer-header__top-line--expanded"), "drawer is expanded" ); }); test("drawer title links to channel info when expanded", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); assert.ok( visible(".chat-drawer-header__top-line--expanded"), "drawer is expanded" ); await click("#chat-channel-row-9"); await click(".chat-drawer-header__title"); assert.equal(currentURL(), `/chat/channel/9/site/info/members`); }); test("drawer title expands the drawer when collapsed", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); await click(".chat-channel-row"); assert.ok( visible(".chat-drawer-header__top-line--expanded"), "drawer is expanded" ); await click(".chat-drawer-header__expand-btn"); assert.ok( visible(".chat-drawer-header__top-line--collapsed"), "drawer is collapsed" ); await click(".chat-drawer-header__title"); assert.ok( visible(".chat-drawer-header__top-line--expanded"), "drawer is expanded" ); }); } ); acceptance( "Discourse Chat - Acceptance Test with unread DMs and public channel 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) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); // chat channel with ID 75 is direct message channel. chatChannelPretender(server, helper, [ { id: 9, unread_count: 2, muted: false }, { id: 75, unread_count: 2, muted: false }, ]); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); }); test("Unread indicator doesn't show when user is in do not disturb", async function (assert) { let now = new Date(); let later = new Date(); later.setTime(now.getTime() + 600000); updateCurrentUser({ do_not_disturb_until: later.toUTCString() }); await visit("/t/internationalization-localization/280"); assert.notOk( exists( ".header-dropdown-toggle.open-chat .chat-unread-urgent-indicator" ) ); }); test("drawer open to DM channel with unread messages with sidebar off", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); await click("#chat-channel-row-75"); const chatContainer = query(".chat-drawer"); assert.strictEqual(chatContainer.dataset.chatChannelId, "75"); }); test("Chat full page open to DM channel with unread messages with sidebar on", async function (assert) { this.chatService.set("sidebarActive", true); this.owner.lookup("service:chat-state-manager").prefersFullPage(); await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); assert.equal(currentURL(), `/chat/channel/75/hawk`); }); } ); acceptance( "Discourse Chat - chat channel settings and creation", function (needs) { needs.user({ admin: true, moderator: true, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); const channel = { chatable: {}, chatable_id: 88, chatable_type: "Category", chatable_url: null, id: 88, title: "Something", last_message_sent_at: "2021-11-08T21:26:05.710Z", current_user_membership: { last_read_message_id: null, unread_count: 0, unread_mentions: 0, }, }; server.get("/chat/api/chat_channels.json", () => { return helper.response([fabricators.chatChannel()]); }); server.get("/chat/chat_channels/:id.json", () => { return helper.response(channel); }); server.put("/chat/chat_channels", () => { return helper.response({ chat_channel: channel, }); }); }); test("Create channel modal", async function (assert) { await visit("/chat/browse"); await click(".new-channel-btn"); assert.strictEqual(currentURL(), "/chat/browse/open"); let categories = selectKit(".create-channel-modal .category-chooser"); await categories.expand(); await categories.selectRowByValue("6"); // Category 6 is "support" assert.strictEqual( query(".create-channel-modal .create-channel-name-input").value.trim(), "support" ); assert.notOk(query(".create-channel-modal .btn.create").disabled); await click(".create-channel-modal .btn.create"); assert.strictEqual(currentURL(), "/chat/channel/88/something"); }); } ); acceptance("Discourse Chat - chat preferences", 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) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper); }); needs.hooks.beforeEach(function () { Object.defineProperty(this, "chatService", { get: () => this.container.lookup("service:chat"), }); }); test("Chat preferences route takes user to homepage when can_chat is false", async function (assert) { updateCurrentUser({ can_chat: false }); await visit("/u/eviltrout/preferences/chat"); assert.equal(currentURL(), "/latest"); }); test("There are all 5 settings shown", async function (assert) { this.chatService.set("sidebarActive", true); await visit("/u/eviltrout/preferences/chat"); assert.equal(currentURL(), "/u/eviltrout/preferences/chat"); assert.equal(queryAll(".chat-setting").length, 5); }); test("The user can save the settings", async function (assert) { updateCurrentUser({ has_chat_enabled: false }); const spy = sinon.spy(ajaxModule, "ajax"); await visit("/u/eviltrout/preferences/chat"); await click("#user_chat_enabled"); await click("#user_chat_only_push_notifications"); await click("#user_chat_ignore_channel_wide_mention"); await selectKit("#user_chat_sounds").expand(); await selectKit("#user_chat_sounds").selectRowByValue("bell"); await selectKit("#user_chat_email_frequency").expand(); await selectKit("#user_chat_email_frequency").selectRowByValue("never"); await click(".save-changes"); assert.ok( spy.calledWithMatch("/u/eviltrout.json", { data: { chat_enabled: true, chat_sound: "bell", only_chat_push_notifications: true, ignore_channel_wide_mention: true, chat_email_frequency: "never", }, type: "PUT", }), "is able to save the chat preferences for the user" ); }); }); acceptance("Discourse Chat - plugin API", 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) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper); }); test("defines a decorateChatMessage plugin API", async function (assert) { withPluginApi("1.1.0", (api) => { api.decorateChatMessage((message) => { message.innerText = "test"; }); }); await visit("/chat/channel/75/@hawk"); assert.equal( document.querySelector('.chat-message-container[data-id="177"]') .innerText, "test" ); }); }); acceptance("Discourse Chat - image uploads", function (needs) { needs.user({ admin: false, moderator: false, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, chat_allow_uploads: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper); server.post( "/uploads.json", () => { return helper.response({ extension: "jpeg", filesize: 126177, height: 800, human_filesize: "123 KB", id: 202, original_filename: "avatar.PNG.jpg", retain_hours: null, short_path: "/images/avatar.png", short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", thumbnail_height: 320, thumbnail_width: 690, url: "/images/avatar.png", width: 1920, }); }, 500 // this delay is important to slow down the uploads a bit so we can click elements in the UI like the cancel button ); }); test("uploading files in chat works", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".header-dropdown-toggle.open-chat"); await click(".chat-channel-row"); assert.ok(exists(".chat-drawer.is-expanded"), "drawer is open"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on( "upload-mixin:chat-composer-uploader:all-uploads-complete", async () => { await settled(); assert.ok( exists(".preview .preview-img"), "the chat upload preview should show" ); assert.notOk( exists(".bottom-data .uploading"), "the chat upload preview should no longer say it is uploading" ); assert.strictEqual( queryAll(".chat-composer-input").val(), "", "the chat composer does not get the upload markdown when the upload is complete" ); done(); } ); appEvents.on( "upload-mixin:chat-composer-uploader:upload-started", async () => { await settled(); assert.ok( exists(".chat-upload"), "the chat upload preview should show" ); assert.ok( exists(".bottom-data .uploading"), "the chat upload preview should say it is uploading" ); assert.strictEqual( queryAll(".chat-composer-input").val(), "", "the chat composer does not get an uploading... placeholder" ); } ); const image = createFile("avatar.png"); appEvents.trigger("upload-mixin:chat-composer-uploader:add-files", image); }); test("uploading files in composer does not insert placeholder text into chat composer", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); assert.ok(exists(".d-editor-input"), "the composer input is visible"); this.container.lookup("service:chat").set("sidebarActive", false); await click(".header-dropdown-toggle.open-chat"); await click(".chat-channel-row"); assert.ok(visible(".chat-drawer-container"), "drawer is open"); const appEvents = loggedInUser().appEvents; const done = assert.async(); await fillIn(".d-editor-input", "The image:\n"); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n", "the topic composer gets the completed image markdown" ); assert.strictEqual( query(".chat-composer-input").value, "", "the chat composer does not get the completed image markdown" ); done(); }); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n", "the topic composer gets the placeholder image markdown" ); assert.strictEqual( query(".chat-composer-input").value, "", "the chat composer does not get the placeholder image markdown" ); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); }); acceptance( "Discourse Chat - image uploads - uploads not allowed", function (needs) { needs.user({ admin: false, moderator: false, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, chat_allow_uploads: false, discourse_local_dates_enabled: false, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); directMessageChannelPretender(server, helper); chatChannelPretender(server, helper); }); test("uploads are not allowed in public channels", async function (assert) { await visit("/chat/channel/4/public-category"); await click(".chat-composer-dropdown__trigger-btn"); assert.notOk( exists(".chat-composer-dropdown__item.chat-upload-btn"), "composer dropdown should not be visible because uploads are not enabled and no other buttons are rendered" ); }); test("uploads are not allowed in direct message channels", async function (assert) { await visit("/chat/channel/75/@hawk"); await click(".chat-composer-dropdown__trigger-btn"); assert.notOk( exists(".chat-composer-dropdown__item.chat-upload-btn"), "composer dropdown should not be visible because uploads are not enabled and no other buttons are rendered" ); }); } ); acceptance("Discourse Chat - Insert Date", function (needs) { needs.user({ username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, discourse_local_dates_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); }); test("can use local date modal", async function (assert) { await visit("/chat/channel/4/public-category"); await click(".chat-composer-dropdown__trigger-btn"); await click(".chat-composer-dropdown__action-btn.local-dates"); assert.ok(exists(".discourse-local-dates-create-modal")); await click(".modal-footer .btn-primary"); assert.ok( query(".chat-composer-input").value.startsWith("[date"), "inserts date in composer input" ); }); }); acceptance( "Discourse Chat - Channel Status - Read only channel", function (needs) { needs.user({ admin: true, moderator: true, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); server.get("/chat/chat_channels.json", () => { const cloned = cloneJSON(chatChannels); cloned.public_channels.find((chan) => chan.id === 7).status = CHANNEL_STATUSES.readOnly; return helper.response(cloned); }); }); test("read only channel composer is disabled", async function (assert) { await visit("/chat/channel/5/public-category"); assert.strictEqual(query(".chat-composer-input").disabled, true); }); test("read only channel header status shows correct information", async function (assert) { await visit("/chat/channel/5/public-category"); assert.strictEqual( query(".chat-channel-status").innerText.trim(), I18n.t("chat.channel_status.read_only_header") ); }); test("read only channels do not show the reply, react, delete, edit, restore, or rebuild options for messages", async function (assert) { await visit("/chat/channel/5/public-category"); await triggerEvent(".chat-message-container", "mouseenter"); const dropdown = selectKit(".chat-message-actions .more-buttons"); await dropdown.expand(); assert.notOk(exists(".select-kit-row[data-value='edit']")); assert.notOk(exists(".select-kit-row[data-value='deleteMessage']")); assert.notOk(exists(".select-kit-row[data-value='rebakeMessage']")); assert.notOk(exists(".reply-btn")); assert.notOk(exists(".react-btn")); }); } ); acceptance( "Discourse Chat - Channel Status - Closed channel (regular user)", 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) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); server.get("/chat/chat_channels.json", () => { const cloned = cloneJSON(chatChannels); cloned.public_channels.find((chan) => chan.id === 4).status = CHANNEL_STATUSES.closed; return helper.response(cloned); }); }); test("closed channel composer is disabled", async function (assert) { await visit("/chat/channel/4/public-category"); assert.strictEqual(query(".chat-composer-input").disabled, true); }); test("closed channel header status shows correct information", async function (assert) { await visit("/chat/channel/4/public-category"); assert.strictEqual( query(".chat-channel-status").innerText.trim(), I18n.t("chat.channel_status.closed_header") ); }); test("closed channels do not show the reply, react, delete, edit, restore, or rebuild options for messages", async function (assert) { await visit("/chat/channel/4/public-category"); await triggerEvent(".chat-message-container", "mouseenter"); const dropdown = selectKit(".chat-message-actions .more-buttons"); await dropdown.expand(); assert.notOk(exists(".select-kit-row[data-value='edit']")); assert.notOk(exists(".select-kit-row[data-value='deleteMessage']")); assert.notOk(exists(".select-kit-row[data-value='rebakeMessage']")); assert.notOk(exists(".reply-btn")); assert.notOk(exists(".react-btn")); }); } ); acceptance( "Discourse Chat - Channel Status - Closed channel (staff user)", function (needs) { needs.user({ admin: true, moderator: true, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); server.get("/chat/chat_channels.json", () => { const cloned = cloneJSON(chatChannels); cloned.public_channels.find((chan) => chan.id === 7).status = CHANNEL_STATUSES.closed; return helper.response(cloned); }); }); test("closed channel composer is enabled", async function (assert) { await visit("/chat/channel/4/public-category"); assert.strictEqual(query(".chat-composer-input").disabled, false); }); test("closed channels show the reply, react, delete, edit, restore, or rebuild options for messages", async function (assert) { await visit("/chat/channel/4/public-category"); await triggerEvent(".chat-message-container", "mouseenter"); const dropdown = selectKit(".chat-message-actions .more-buttons"); await dropdown.expand(); assert.ok( exists(".select-kit-row[data-value='edit']"), "the edit message button is shown" ); assert.ok( exists(".select-kit-row[data-value='deleteMessage']"), "the delete message button is shown" ); assert.ok( exists(".select-kit-row[data-value='rebakeMessage']"), "the rebake message button is shown" ); assert.ok(exists(".reply-btn", "the reply button is shown")); assert.ok(exists(".react-btn"), "the react button is shown"); }); } ); acceptance("Discourse Chat - Channel Replying Indicator", function (needs) { needs.user({ admin: true, moderator: true, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); server.get("/chat/chat_channels.json", () => { const cloned = cloneJSON(chatChannels); cloned.public_channels.find((chan) => chan.id === 7).status = CHANNEL_STATUSES.closed; return helper.response(cloned); }); }); test("indicator content when replying/not replying", async function (assert) { const user = { id: 8, username: "bob" }; await visit("/chat/channel/4/public-category"); await joinChannel("/chat-reply/4", user); assert.equal( query(".chat-replying-indicator__text").innerText, I18n.t("chat.replying_indicator.single_user", { username: user.username, }) ); await leaveChannel("/chat-reply/4", user); assert.notOk(exists(".chat-replying-indicator__text")); }); }); acceptance("Discourse Chat - Direct Message Creator", function (needs) { needs.user({ admin: true, moderator: true, username: "eviltrout", id: 1, can_chat: true, has_chat_enabled: true, }); needs.settings({ chat_enabled: true, }); needs.pretender((server, helper) => { baseChatPretenders(server, helper); chatChannelPretender(server, helper); server.get("/u/search/users", () => { return helper.response([]); }); }); test("Create a direct message", async function (assert) { await visit("/latest"); await click(".header-dropdown-toggle.open-chat"); assert.ok( !exists(".open-draft-channel-page-btn.btn-floating"), "mobile floating button should not exist on desktop" ); await click(".btn.open-draft-channel-page-btn"); assert.ok(exists(".chat-draft"), "view changes to draft channel screen"); }); }); function createFile(name, type = "image/png") { // the blob content doesn't matter at all, just want it to be random-ish const file = new Blob([(Math.random() + 1).toString(36).substring(2)], { type, }); file.name = name; return file; }