DEV: various improvements to devex on chat (#21612)

- Improves styleguide support
- Adds toggle color scheme to styleguide
- Adds properties mutators to styleguide
- Attempts to quit a session as soon as done with it in system specs, this should at least free resources faster
- Refactors fabricators to simplify them
- Adds more fabricators (uploads for example)
- Starts implementing components pattern in system specs
- Uses Chat::Message creator to create messages in system specs, this should help to have more real specs as the side effects should now happen
This commit is contained in:
Joffrey JAFFEUX 2023-05-17 17:49:52 +02:00 committed by GitHub
parent 4d0b997559
commit 60c67afba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1002 additions and 260 deletions

View File

@ -41,7 +41,6 @@
{{#each @channel.messages key="id" as |message|}}
<ChatMessage
@message={{message}}
@channel={{@channel}}
@resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}

View File

@ -113,11 +113,10 @@ export default class ChatLivePane extends Component {
if (this._loadedChannelId !== this.args.channel?.id) {
this.unsubscribeToUpdates(this._loadedChannelId);
this.chatChannelPane.selectingMessages = false;
this.chatChannelComposer.message =
this.args.channel.draft ||
ChatMessage.createDraftMessage(this.args.channel, {
user: this.currentUser,
});
if (this.args.channel.draft) {
this.chatChannelComposer.message = this.args.channel.draft;
}
this._loadedChannelId = this.args.channel?.id;
}

View File

@ -1,6 +1,6 @@
<div class="chat-composer-message-details">
<div class="chat-composer-message-details" data-id={{@message.id}}>
<div class="chat-reply">
{{d-icon @icon}}
{{d-icon (if @message.editing "pencil-alt" "reply")}}
<ChatUserAvatar @user={{@message.user}} />
<span class="chat-reply__username">{{@message.user.username}}</span>
<span class="chat-reply__excerpt">

View File

@ -9,7 +9,6 @@
this.currentMessage
this.currentMessage.inReplyTo
}}
@icon={{if this.currentMessage.editing "pencil-alt" "reply"}}
@cancelAction={{this.onCancel}}
/>
{{/if}}
@ -26,7 +25,7 @@
}}
{{did-update this.didUpdateMessage this.currentMessage}}
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
{{did-insert this.setupAppEvents}}
{{did-insert this.setup}}
{{will-destroy this.teardown}}
{{will-destroy this.cancelPersistDraft}}
>
@ -71,7 +70,7 @@
{{on "click" this.onSend}}
@icon="paper-plane"
class="chat-composer__send-btn"
title="chat.composer.send"
title={{i18n "chat.composer.send"}}
disabled={{or this.disabled (not this.sendEnabled)}}
tabindex={{if this.sendEnabled 0 -1}}
{{on "focus" (fn this.computeIsFocused true)}}

View File

@ -141,7 +141,10 @@ export default class ChatComposer extends Component {
}
@action
setupAppEvents() {
setup() {
this.composer.message = ChatMessage.createDraftMessage(this.args.channel, {
user: this.currentUser,
});
this.appEvents.on("chat:modify-selection", this, "modifySelection");
this.appEvents.on(
"chat:open-insert-link-modal",

View File

@ -54,6 +54,10 @@ export default class ChatMessageActionsDesktop extends Component {
this.context
);
if (!messageContainer) {
return;
}
const viewport = messageContainer.closest(".popper-viewport");
this.size =
viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL;

View File

@ -130,7 +130,7 @@ export default class ChatMessage extends Component {
}
_chatMessageDecorators.forEach((decorator) => {
decorator.call(this, this.messageContainer, this.args.channel);
decorator.call(this, this.messageContainer, this.args.message.channel);
});
});
}
@ -147,7 +147,7 @@ export default class ChatMessage extends Component {
!this.args.message?.deletedAt ||
this.currentUser.id === this.args.message?.user?.id ||
this.currentUser.staff ||
this.args.channel?.canModerate
this.args.message?.channel?.canModerate
);
}
@ -316,7 +316,10 @@ export default class ChatMessage extends Component {
}
get threadingEnabled() {
return this.args.channel?.threadingEnabled && !!this.args.message?.thread;
return (
this.args.message?.channel?.threadingEnabled &&
!!this.args.message?.thread
);
}
get showThreadIndicator() {

View File

@ -34,7 +34,6 @@
{{#each this.thread.messages key="id" as |message|}}
<ChatMessage
@message={{message}}
@channel={{this.channel}}
@resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}

View File

@ -20,10 +20,7 @@
/>
</div>
</div>
<Chat::Thread::OriginalMessage
@thread={{@thread}}
@message={{@thread.originalMessage}}
/>
<Chat::Thread::OriginalMessage @message={{@thread.originalMessage}} />
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
<StyleguideExample @title="<ChatComposerMessageDetails>">
<Styleguide::Component>
<ChatComposerMessageDetails @message={{this.message}} />
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Mode">
{{#if this.message.editing}}
<DButton @action={{this.toggleMode}} @translatedLabel="Reply" />
{{else}}
<DButton @action={{this.toggleMode}} @translatedLabel="Editing" />
{{/if}}
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import { cached } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
export default class ChatStyleguideChatComposerMessageDetails extends Component {
@service site;
@service session;
@service keyValueStore;
@cached
get message() {
return fabricators.message();
}
@action
toggleMode() {
if (this.message.editing) {
this.message.editing = false;
this.message.inReplyTo = fabricators.message();
} else {
this.message.editing = true;
this.message.inReplyTo = null;
}
}
}

View File

@ -0,0 +1,23 @@
<StyleguideExample @title="<ChatComposer>">
<Styleguide::Component>
<Chat::Composer::Channel
@channel={{this.channel}}
@onSendMessage={{this.onSendMessage}}
/>
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Disabled">
<DToggleSwitch
@state={{this.channel.isReadOnly}}
{{on "click" this.toggleDisabled}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Sending">
<DToggleSwitch
@state={{this.chatChannelPane.sending}}
{{on "click" this.toggleSending}}
/>
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,30 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
export default class ChatStyleguideChatComposer extends Component {
@service chatChannelComposer;
@service chatChannelPane;
channel = fabricators.channel();
@action
toggleDisabled() {
if (this.channel.status === CHANNEL_STATUSES.open) {
this.channel.status = CHANNEL_STATUSES.readOnly;
} else {
this.channel.status = CHANNEL_STATUSES.open;
}
}
@action
toggleSending() {
this.chatChannelPane.sending = !this.chatChannelPane.sending;
}
@action
onSendMessage() {
this.chatChannelComposer.reset();
}
}

View File

@ -0,0 +1,54 @@
<StyleguideExample @title="<ChatMessage>">
<Styleguide::Component>
<ChatMessage
@message={{this.message}}
@context="channel"
@messageDidEnterViewport={{(noop)}}
@messageDidLeaveViewport={{(noop)}}
/>
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Last Visit">
<DToggleSwitch
@state={{this.message.newest}}
{{on "click" this.toggleLastVisit}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Deleted">
<DToggleSwitch
@state={{not (not this.message.deletedAt)}}
{{on "click" this.toggleDeleted}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Bookmark">
<DToggleSwitch
@state={{not (not this.message.bookmark)}}
{{on "click" this.toggleBookmarked}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Thread">
<DToggleSwitch
@state={{not (not this.message.thread)}}
{{on "click" this.toggleThread}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Reactions">
<DToggleSwitch
@state={{not (not this.message.reactions)}}
{{on "click" this.toggleReaction}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Upload">
<DToggleSwitch
@state={{not (not this.message.uploads)}}
{{on "click" this.toggleUpload}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Message">
<textarea
{{on "input" this.updateMessage}}
>{{this.message.message}}</textarea>
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,86 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { getOwner } from "discourse-common/lib/get-owner";
export default class ChatStyleguideChatMessage extends Component {
manager = new ChatMessagesManager(getOwner(this));
message = fabricators.message();
@action
toggleDeleted() {
if (this.message.deletedAt) {
this.message.deletedAt = null;
} else {
this.message.deletedAt = moment();
}
}
@action
toggleBookmarked() {
if (this.message.bookmark) {
this.message.bookmark = null;
} else {
this.message.bookmark = fabricators.bookmark();
}
}
@action
toggleHighlighted() {
this.message.highlighted = !this.message.highlighted;
}
@action
toggleEdited() {
this.message.edited = !this.message.edited;
}
@action
toggleLastVisit() {
this.message.newest = !this.message.newest;
}
@action
toggleThread() {
if (this.message.thread) {
this.message.channel.threadingEnabled = false;
this.message.thread = null;
this.message.threadReplyCount = 0;
} else {
this.message.thread = fabricators.thread({
channel: this.message.channel,
});
this.message.threadReplyCount = 1;
this.message.channel.threadingEnabled = true;
}
}
@action
updateMessage(event) {
this.message.message = event.target.value;
this.message.cook();
}
@action
toggleReaction() {
if (this.message.reactions?.length) {
this.message.reactions = [];
} else {
this.message.reactions = [
fabricators.reaction({ emoji: "heart" }),
fabricators.reaction({ emoji: "rocket", reacted: true }),
];
}
}
@action
toggleUpload() {
if (this.message.uploads?.length) {
this.message.uploads = [];
} else {
this.message.uploads = [fabricators.upload(), fabricators.upload()];
}
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<ChatThreadOriginalMessage>">
<Styleguide::Component>
<Chat::Thread::OriginalMessage @message={{this.message}} />
</Styleguide::Component>
</StyleguideExample>

View File

@ -0,0 +1,6 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
export default class ChatStyleguideChatThreadOriginalMessage extends Component {
message = fabricators.message();
}

View File

@ -0,0 +1,170 @@
/*
Fabricators are used to create fake data for testing purposes.
The following fabricators are available in lib folder to allow
styleguide to use them, and eventually to generate dummy data
in a placeholder component. It should not be used for any other case.
*/
import ChatChannel, {
CHANNEL_STATUSES,
CHATABLE_TYPES,
} from "discourse/plugins/chat/discourse/models/chat-channel";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
import ChatDirectMessage from "discourse/plugins/chat/discourse/models/chat-direct-message";
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
import User from "discourse/models/user";
import Bookmark from "discourse/models/bookmark";
import Category from "discourse/models/category";
let sequence = 0;
function messageFabricator(args = {}) {
const channel = args.channel || channelFabricator();
const message = ChatMessage.create(
channel,
Object.assign(
{
id: args.id || sequence++,
user: args.user || userFabricator(),
message:
args.message ||
"@discobot **abc**defghijklmnopqrstuvwxyz [discourse](discourse.org) :rocket: ",
created_at: args.created_at || moment(),
},
args
)
);
const excerptLength = 50;
const text = message.message.toString();
if (text.length <= excerptLength) {
message.excerpt = text;
} else {
message.excerpt = text.slice(0, excerptLength) + "...";
}
message.cook();
return message;
}
function channelFabricator(args = {}) {
const id = args.id || sequence++;
return ChatChannel.create(
Object.assign(
{
id,
chatable_type:
args.chatable?.type ||
args.chatable_type ||
CHATABLE_TYPES.categoryChannel,
last_message_sent_at: args.last_message_sent_at,
chatable_id: args.chatable?.id || args.chatable_id,
title: args.title || "General",
description: args.description,
chatable: args.chatable || categoryFabricator(),
status: CHANNEL_STATUSES.open,
},
args
)
);
}
function categoryFabricator(args = {}) {
return Category.create({
id: args.id || sequence++,
color: args.color || "D56353",
read_restricted: false,
name: args.name || "General",
slug: args.slug || "general",
});
}
function directMessageFabricator(args = {}) {
return ChatDirectMessage.create({
id: args.id || sequence++,
users: args.users || [userFabricator(), userFabricator()],
});
}
function directMessageChannelFabricator(args = {}) {
const directMessage =
args.chatable ||
directMessageFabricator({
id: args.chatable_id || sequence++,
});
return channelFabricator(
Object.assign(args, {
chatable_type: CHATABLE_TYPES.directMessageChannel,
chatable_id: directMessage.id,
chatable: directMessage,
})
);
}
function userFabricator(args = {}) {
return User.create({
id: args.id || sequence++,
username: args.username || "hawk",
name: args.name,
avatar_template: "/letter_avatar_proxy/v3/letter/t/41988e/{size}.png",
});
}
function bookmarkFabricator(args = {}) {
return Bookmark.create({
id: args.id || sequence++,
});
}
function threadFabricator(args = {}) {
const channel = args.channel || channelFabricator();
return ChatThread.create(channel, {
id: args.id || sequence++,
original_message: args.original_message || messageFabricator({ channel }),
});
}
function reactionFabricator(args = {}) {
return ChatMessageReaction.create({
count: args.count || 1,
users: args.users || [userFabricator()],
emoji: args.emoji || "heart",
reacted: args.reacted || false,
});
}
function uploadFabricator() {
return {
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,
};
}
export default {
bookmark: bookmarkFabricator,
user: userFabricator,
channel: channelFabricator,
directMessageChannel: directMessageChannelFabricator,
message: messageFabricator,
thread: threadFabricator,
reaction: reactionFabricator,
upload: uploadFabricator,
category: categoryFabricator,
directMessage: directMessageFabricator,
};

View File

@ -89,6 +89,7 @@ export default class ChatChannel {
@tracked membershipsCount = 0;
@tracked archive;
@tracked tracking;
@tracked threadingEnabled = false;
threadsManager = new ChatThreadsManager(getOwner(this));
messagesManager = new ChatMessagesManager(getOwner(this));
@ -114,7 +115,10 @@ export default class ChatChannel {
this.autoJoinUsers = args.auto_join_users;
this.allowChannelWideMentions = args.allow_channel_wide_mentions;
this.chatable = this.isDirectMessageChannel
? ChatDirectMessage.create(args)
? ChatDirectMessage.create({
id: args.chatable?.id,
users: args.chatable?.users,
})
: Category.create(args.chatable);
this.currentUserMembership = UserChatChannelMembership.create(
args.current_user_membership

View File

@ -1,5 +1,6 @@
import User from "discourse/models/user";
import { tracked } from "@glimmer/tracking";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
export default class ChatDirectMessage {
static create(args = {}) {
@ -9,9 +10,11 @@ export default class ChatDirectMessage {
@tracked id;
@tracked users = null;
type = CHATABLE_TYPES.drectMessageChannel;
constructor(args = {}) {
this.id = args.chatable.id;
this.users = this.#initUsers(args.chatable.users || []);
this.id = args.id;
this.users = this.#initUsers(args.users || []);
}
#initUsers(users) {

View File

@ -12,9 +12,9 @@ export default class ChatMessageReaction {
@tracked count = 0;
@tracked reacted = false;
@tracked users = [];
@tracked emoji;
constructor(args = {}) {
this.messageId = args.messageId;
this.count = args.count;
this.emoji = args.emoji;
this.users = this.#initUsersModels(args.users);

View File

@ -7,7 +7,7 @@ import I18n from "I18n";
import { generateCookFunction } from "discourse/lib/text";
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
import { getOwner } from "discourse-common/lib/get-owner";
import { next } from "@ember/runloop";
export default class ChatMessage {
static cookFunction = null;
@ -38,9 +38,10 @@ export default class ChatMessage {
@tracked expanded;
@tracked bookmark;
@tracked userFlagStatus;
@tracked hidden;
@tracked hidden = false;
@tracked version = 0;
@tracked edited;
@tracked edited = false;
@tracked editing = false;
@tracked chatWebhookEvent = new TrackedObject();
@tracked mentionWarning;
@tracked availableFlags;
@ -62,6 +63,7 @@ export default class ChatMessage {
this.firstOfResults = args.firstOfResults;
this.staged = args.staged;
this.edited = args.edited;
this.editing = args.editing;
this.availableFlags = args.availableFlags || args.available_flags;
this.hidden = args.hidden;
this.threadReplyCount = args.threadReplyCount || args.thread_reply_count;
@ -82,10 +84,7 @@ export default class ChatMessage {
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
: null);
this.channel = channel;
this.reactions = this.#initChatMessageReactionModel(
args.id,
args.reactions
);
this.reactions = this.#initChatMessageReactionModel(args.reactions);
this.uploads = new TrackedArray(args.uploads || []);
this.user = this.#initUserModel(args.user);
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
@ -138,6 +137,7 @@ export default class ChatMessage {
}
cook() {
next(() => {
const site = getOwner(this).lookup("service:site");
const markdownOptions = {
@ -165,6 +165,7 @@ export default class ChatMessage {
this.cooked = ChatMessage.cookFunction(this.message);
});
}
});
}
get read() {
@ -306,10 +307,8 @@ export default class ChatMessage {
}
}
#initChatMessageReactionModel(messageId, reactions = []) {
return reactions.map((reaction) =>
ChatMessageReaction.create(Object.assign({ messageId }, reaction))
);
#initChatMessageReactionModel(reactions = []) {
return reactions.map((reaction) => ChatMessageReaction.create(reaction));
}
#initUserModel(user) {

View File

@ -13,6 +13,10 @@ export const THREAD_STATUSES = {
};
export default class ChatThread {
static create(channel, args = {}) {
return new ChatThread(channel, args);
}
@tracked id;
@tracked title;
@tracked status;

View File

@ -4,7 +4,6 @@ import ChatComposer from "./chat-composer";
export default class ChatChannelComposer extends ChatComposer {
@service chat;
@service chatChannelThreadComposer;
@service router;
@action

View File

@ -0,0 +1,4 @@
<Styleguide::ChatMessage />
<Styleguide::ChatComposer />
<Styleguide::ChatThreadOriginalMessage />
<Styleguide::ChatComposerMessageDetails />

View File

@ -641,3 +641,8 @@ en:
chat_notifications_with_unread:
one: "Chat notifications - %{count} unread notification"
other: "Chat notifications - %{count} unread notifications"
styleguide:
sections:
chat:
title: Chat

View File

@ -49,13 +49,29 @@ Fabricator(:direct_message_channel, from: :chat_channel) do
end
end
Fabricator(:chat_message, class_name: "Chat::Message") do
chat_channel
user
message "Beep boop"
cooked { |attrs| Chat::Message.cook(attrs[:message]) }
cooked_version Chat::Message::BAKED_VERSION
in_reply_to nil
Fabricator(:chat_message, class_name: "Chat::MessageCreator") do
transient :chat_channel
transient :user
transient :message
transient :in_reply_to
transient :thread
transient :upload_ids
initialize_with do |transients|
user = transients[:user] || Fabricate(:user)
channel =
transients[:chat_channel] || transients[:thread]&.channel ||
transients[:in_reply_to]&.chat_channel || Fabricate(:chat_channel)
resolved_class.create(
chat_channel: channel,
user: user,
content: transients[:message] || Faker::Lorem.paragraph,
thread_id: transients[:thread]&.id,
in_reply_to_id: transients[:in_reply_to]&.id,
upload_ids: transients[:upload_ids],
).chat_message
end
end
Fabricator(:chat_mention, class_name: "Chat::Mention") do

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
RSpec.describe "Chat | composer | shortcuts | thread", type: :system, js: true do
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
fab!(:current_user) { Fabricate(:user) }
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:thread_page) { PageObjects::Pages::ChatThread.new }
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
chat_system_bootstrap
channel_1.add(current_user)
sign_in(current_user)
end
describe "ArrowUp" do
let(:thread_1) { message_1.reload.thread }
context "when there are editable messages" do
let(:last_thread_message) { thread_1.replies.last }
before do
thread_message_1 = Fabricate(:chat_message, user: current_user, in_reply_to: message_1)
Fabricate(:chat_message, user: current_user, thread: thread_message_1.reload.thread)
end
it "starts editing the last editable message" do
chat_page.visit_thread(thread_1)
thread_page.composer.edit_last_message_shortcut
expect(thread_page.composer_message_details).to have_message(last_thread_message)
expect(thread_page.composer.value).to eq(last_thread_message.message)
end
end
context "when there are no editable messages" do
before { Fabricate(:chat_message, in_reply_to: message_1) }
it "does nothing" do
chat_page.visit_thread(thread_1)
thread_page.composer.edit_last_message_shortcut
expect(thread_page.composer_message_details).to have_no_message
expect(thread_page.composer.value).to be_blank
end
end
end
end

View File

@ -45,9 +45,15 @@ RSpec.describe "Chat channel", type: :system, js: true do
chat.visit_channel(channel_1)
end
using_session(:tab_1) { channel.send_message("test_message") }
using_session(:tab_1) do |session|
channel.send_message("test_message")
session.quit
end
using_session(:tab_2) { expect(channel).to have_message(text: "test_message") }
using_session(:tab_2) do |session|
expect(channel).to have_message(text: "test_message")
session.quit
end
end
end

View File

@ -23,11 +23,12 @@ RSpec.describe "Edited message", type: :system, js: true do
it "shows as edited for all users" do
chat_page.visit_channel(channel_1)
using_session(:user_1) do
using_session(:user_1) do |session|
sign_in(editing_user)
chat_page.visit_channel(channel_1)
channel_page.edit_message(message_1, "a different message")
expect(page).to have_content(I18n.t("js.chat.edited"))
session.quit
end
expect(page).to have_content(I18n.t("js.chat.edited"))

View File

@ -35,7 +35,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(
@ -62,7 +65,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".do-not-disturb-background")
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
@ -77,7 +83,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(
@ -92,7 +101,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css(
@ -138,14 +150,20 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1")
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator",
)
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2")
end
@ -162,7 +180,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
".chat-channel-row:nth-child(2)[data-chat-channel-id=\"#{dm_channel_2.id}\"]",
)
using_session(:user_1) { create_message(channel: dm_channel_2, creator: user_2) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_2, creator: user_2)
session.quit
end
expect(page).to have_css(
".chat-channel-row:nth-child(1)[data-chat-channel-id=\"#{dm_channel_2.id}\"]",
@ -190,14 +211,20 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately!
visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
)
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator",

View File

@ -34,7 +34,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "doesn't show anything" do
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id}")
@ -59,7 +62,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
Jobs.run_immediately!
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".do-not-disturb-background")
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
@ -72,7 +78,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "doesn't show anything" do
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id} .unread")
@ -91,7 +100,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "doesn't show any indicator on chat-header-icon" do
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
end
@ -109,7 +121,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "doesn't show any indicator on chat-header-icon" do
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(
".chat-header-icon .chat-channel-unread-indicator.urgent",
@ -137,7 +152,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "correctly renders notifications" do
visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css(".sidebar-row.channel-#{channel_1.id} .unread")
@ -178,12 +196,18 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do
it "correctly renders notifications" do
visit("/")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1")
expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2")
end
@ -198,7 +222,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
"#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(2) .channel-#{dm_channel_2.id}",
)
using_session(:user_1) { create_message(channel: dm_channel_2, creator: user_2) }
using_session(:user_1) do |session|
create_message(channel: dm_channel_2, creator: user_2)
session.quit
end
expect(page).to have_css(
"#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(1) .channel-#{dm_channel_2.id}",
@ -237,12 +264,13 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
session.quit
end
using_session(:current_user) do
using_session(:current_user) do |session|
expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent")
expect(page).to have_css(
".chat-header-icon .chat-channel-unread-indicator",
text: "1",
)
session.quit
end
end
end

View File

@ -17,8 +17,8 @@ module PageObjects
visit("/chat")
end
def visit_channel(channel, mobile: false)
visit(channel.url + (mobile ? "?mobile_view=1" : ""))
def visit_channel(channel)
visit(channel.url)
has_no_css?(".chat-channel--not-loaded-once")
has_no_css?(".chat-skeleton")
end

View File

@ -3,6 +3,15 @@
module PageObjects
module Pages
class ChatThread < PageObjects::Pages::Base
def composer
@composer ||= PageObjects::Components::Chat::Composer.new(".chat-thread")
end
def composer_message_details
@composer_message_details ||=
PageObjects::Components::Chat::ComposerMessageDetails.new(".chat-thread")
end
def header
find(".chat-thread__header")
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module PageObjects
module Components
module Chat
class Composer < PageObjects::Components::Base
attr_reader :context
SELECTOR = ".chat-composer__wrapper"
def initialize(context)
@context = context
end
def input
find(context).find(SELECTOR).find(".chat-composer__input")
end
def value
input.value
end
def reply_to_last_message_shortcut
input.send_keys(%i[shift arrow_up])
end
def edit_last_message_shortcut
input.send_keys(%i[arrow_up])
end
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module PageObjects
module Components
module Chat
class ComposerMessageDetails < PageObjects::Components::Base
attr_reader :context
SELECTOR = ".chat-composer-message-details"
def initialize(context)
@context = context
end
def has_message?(message)
find(context).find(SELECTOR + "[data-id=\"#{message.id}\"]")
end
def has_no_message?
find(context).has_no_css?(SELECTOR)
end
end
end
end
end

View File

@ -87,7 +87,7 @@ RSpec.describe "Reply to message - channel - drawer", type: :system, js: true do
expect(page).to have_selector(
".chat-channel .chat-reply__excerpt",
text: original_message.message,
text: original_message.excerpt,
)
channel_page.fill_composer("reply to message")

View File

@ -91,7 +91,7 @@ RSpec.describe "Reply to message - channel - full page", type: :system, js: true
expect(page).to have_selector(
".chat-channel .chat-reply__excerpt",
text: original_message.message,
text: original_message.excerpt,
)
channel_page.fill_composer("reply to message")

View File

@ -97,7 +97,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, js: true, m
expect(page).to have_selector(
".chat-channel .chat-reply__excerpt",
text: original_message.message,
text: original_message.excerpt,
)
channel_page.fill_composer("reply to message")

View File

@ -161,15 +161,17 @@ describe "Single thread in side panel", type: :system, js: true do
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
end
using_session(:tab_1) do
using_session(:tab_1) do |session|
expect(side_panel).to have_open_thread(thread)
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
thread_page.send_message("this is a test message")
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
session.quit
end
using_session(:tab_2) do
using_session(:tab_2) do |session|
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
session.quit
end
end

View File

@ -22,12 +22,13 @@ RSpec.describe "Unfollow dm channel", type: :system, js: true do
expect(page).to have_no_css(".channel-#{dm_channel_1.id}")
using_session(:user_1) do
using_session(:user_1) do |session|
text = "this is fine"
sign_in(other_user)
chat_page.visit_channel(dm_channel_1)
chat_channel_page.send_message(text)
expect(chat_channel_page).to have_message(text: text)
session.quit
end
expect(page).to have_css(".channel-#{dm_channel_1.id} .urgent")

View File

@ -198,13 +198,14 @@ RSpec.describe "User menu notifications | sidebar", type: :system, js: true do
channel.send_message("this is fine @#{other_user.username}")
find(".invite-link", wait: 5).click
using_session(:user_1) do
using_session(:user_1) do |session|
sign_in(other_user)
visit("/")
find(".header-dropdown-toggle.current-user").click
expect(find("#user-menu-button-chat-notifications")).to have_content(1)
expect(find("#quick-access-all-notifications")).to have_css(".chat-invitation.unread")
session.quit
end
end
end

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) {
this.set(
"channel",
fabricators.chatChannel({
fabricators.channel({
title: `<script>someeviltitle</script>`,
})
);

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import I18n from "I18n";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
this.channel.description =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
});

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) {
this.set(
"channel",
fabricators.chatChannel({
fabricators.channel({
title: `<script>someeviltitle</script>`,
})
);

View File

@ -5,7 +5,7 @@ import hbs from "htmlbars-inline-precompile";
import pretender from "discourse/tests/helpers/create-pretender";
import I18n from "I18n";
import { module, test } from "qunit";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
setupRenderingTest(hooks);
@ -13,7 +13,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
test("accepts an optional onLeaveChannel callback", async function (assert) {
this.foo = 1;
this.onLeaveChannel = () => (this.foo = 2);
this.channel = fabricators.directMessageChatChannel({ users: [{ id: 1 }] });
this.channel = fabricators.directMessageChannel();
await render(
hbs`<ChatChannelLeaveBtn @channel={{this.channel}} @onLeaveChannel={{this.onLeaveChannel}} />`
@ -29,7 +29,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
});
test("has a specific title for direct message channel", async function (assert) {
this.channel = fabricators.directMessageChatChannel();
this.channel = fabricators.directMessageChannel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -38,7 +38,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
});
test("has a specific title for message channel", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -48,7 +48,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
test("is not visible on mobile", async function (assert) {
this.site.mobileView = true;
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
test("displays last message sent at", async function (assert) {
let lastMessageSentAt = moment().subtract(1, "day").format();
this.channel = fabricators.directMessageChatChannel({
this.channel = fabricators.directMessageChannel({
last_message_sent_at: lastMessageSentAt,
});
@ -28,7 +28,7 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
});
test("unreadIndicator", async function (assert) {
this.channel = fabricators.directMessageChatChannel();
this.channel = fabricators.directMessageChannel();
this.channel.tracking.unreadCount = 1;
this.unreadIndicator = true;

View File

@ -3,7 +3,7 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module(
"Discourse Chat | Component | chat-channel-preview-card",
@ -11,10 +11,7 @@ module(
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set(
"channel",
fabricators.chatChannel({ chatable_type: "Category" })
);
this.set("channel", fabricators.channel({ chatable_type: "Category" }));
this.channel.description = "Important stuff is announced here.";
this.channel.title = "announcements";

View File

@ -2,14 +2,14 @@ import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-channel-row", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.categoryChatChannel = fabricators.chatChannel();
this.directMessageChatChannel = fabricators.directMessageChatChannel();
this.categoryChatChannel = fabricators.channel();
this.directMessageChannel = fabricators.directMessageChannel();
});
test("links to correct channel", async function (assert) {
@ -47,11 +47,14 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
});
test("renders correct channel metadata", async function (assert) {
this.categoryChatChannel.lastMessageSentAt = moment().toISOString();
await render(hbs`<ChatChannelRow @channel={{this.categoryChatChannel}} />`);
assert
.dom(".chat-channel-metadata")
.hasText(moment(this.categoryChatChannel.lastMessageSentAt).format("l"));
.hasText(
moment(this.categoryChatChannel.lastMessageSentAt).format("h:mm A")
);
});
test("renders membership toggling button when necessary", async function (assert) {
@ -145,11 +148,14 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
});
test("user status with direct message channel", async function (assert) {
this.directMessageChannel.chatable = fabricators.directMessage({
users: [fabricators.user()],
});
const status = { description: "Off to dentist", emoji: "tooth" };
this.directMessageChatChannel.chatable.users[0].status = status;
this.directMessageChannel.chatable.users[0].status = status;
await render(
hbs`<ChatChannelRow @channel={{this.directMessageChatChannel}} />`
hbs`<ChatChannelRow @channel={{this.directMessageChannel}} />`
);
assert.dom(".user-status-message").exists();
@ -157,9 +163,9 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
test("user status with direct message channel and multiple users", async function (assert) {
const status = { description: "Off to dentist", emoji: "tooth" };
this.directMessageChatChannel.chatable.users[0].status = status;
this.directMessageChannel.chatable.users[0].status = status;
this.directMessageChatChannel.chatable.users.push({
this.directMessageChannel.chatable.users.push({
id: 2,
username: "bill",
name: null,
@ -167,7 +173,7 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
});
await render(
hbs`<ChatChannelRow @channel={{this.directMessageChatChannel}} />`
hbs`<ChatChannelRow @channel={{this.directMessageChannel}} />`
);
assert.dom(".user-status-message").doesNotExist();

View File

@ -3,7 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import {
CHANNEL_STATUSES,
channelStatusIcon,
@ -13,7 +13,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
setupRenderingTest(hooks);
test("renders nothing when channel is opened", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`);
@ -21,7 +21,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
});
test("defaults to long format", async function (assert) {
this.channel = fabricators.chatChannel({ status: CHANNEL_STATUSES.closed });
this.channel = fabricators.channel({ status: CHANNEL_STATUSES.closed });
await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`);
@ -31,7 +31,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
});
test("accepts a format argument", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived,
});
@ -45,7 +45,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
});
test("renders the correct icon", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived,
});
@ -56,7 +56,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
test("renders archive status", async function (assert) {
this.currentUser.admin = true;
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived,
archive_failed: true,
});

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
setupRenderingTest(hooks);
test("category channel", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel,
});
@ -27,7 +27,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
test("category channel - escapes title", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel,
title: "<div class='xss'>evil</div>",
});
@ -38,7 +38,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
test("category channel - read restricted", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel,
chatable: { read_restricted: true },
});
@ -49,7 +49,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
test("category channel - not read restricted", async function (assert) {
this.channel = fabricators.chatChannel({
this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel,
chatable: { read_restricted: false },
});
@ -60,7 +60,11 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
test("direct message channel - one user", async function (assert) {
this.channel = fabricators.directMessageChatChannel();
this.channel = fabricators.directMessageChannel({
chatable: fabricators.directMessage({
users: [fabricators.user()],
}),
});
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -77,7 +81,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
test("direct message channel - multiple users", async function (assert) {
const channel = fabricators.directMessageChatChannel();
const channel = fabricators.directMessageChannel();
channel.chatable.users.push({
id: 2,

View File

@ -0,0 +1,77 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import hbs from "htmlbars-inline-precompile";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
module(
"Discourse Chat | Component | chat-composer-message-details",
function (hooks) {
setupRenderingTest(hooks);
test("data-id attribute", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert
.dom(".chat-composer-message-details")
.hasAttribute("data-id", this.message.id.toString());
});
test("editing a message has the pencil icon", async function (assert) {
this.message = fabricators.message({ editing: true });
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-composer-message-details .d-icon-pencil-alt").exists();
});
test("replying to a message has the reply icon", async function (assert) {
const firstMessage = fabricators.message();
this.message = fabricators.message({ inReplyTo: firstMessage });
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-composer-message-details .d-icon-reply").exists();
});
test("displays user avatar", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert
.dom(".chat-composer-message-details .chat-user-avatar .avatar")
.hasAttribute("title", this.message.user.username);
});
test("displays message excerpt", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-reply__excerpt").hasText(this.message.excerpt);
});
test("displays users username", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-reply__username").hasText(this.message.user.username);
});
}
);

View File

@ -4,13 +4,13 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
chat_webhook_event: { emoji: ":heart:" },
});
@ -20,7 +20,7 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
});
test("user", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
});

View File

@ -6,13 +6,13 @@ import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-message-info", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
chat_webhook_event: { username: "discobot" },
});
@ -29,7 +29,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("user", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
});
@ -42,7 +42,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("date", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
created_at: moment(),
});
@ -53,7 +53,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (with reminder)", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
bookmark: Bookmark.create({
reminder_at: moment(),
@ -69,7 +69,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (no reminder)", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
bookmark: Bookmark.create({
name: "some name",
@ -83,7 +83,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
test("user status", async function (assert) {
const status = { description: "off to dentist", emoji: "tooth" };
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { status },
});
@ -93,7 +93,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("reviewable", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
user_flag_status: 0,
});
@ -105,7 +105,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
I18n.t("chat.you_flagged")
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
reviewable_id: 1,
});
@ -119,7 +119,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("with username classes", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: {
username: "discobot",
admin: true,
@ -139,7 +139,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("without username classes", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), {
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
});

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) {
this.set(
"channel",
fabricators.chatChannel({ title: "<script>someeviltitle</script>" })
fabricators.channel({ title: "<script>someeviltitle</script>" })
);
this.set("chat", { publicChannels: [this.channel] });
this.set("selectedMessageIds", [1]);

View File

@ -55,7 +55,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
const template = hbs`
<ChatMessage
@message={{this.message}}
@channel={{this.channel}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
/>

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import {
@ -87,7 +87,7 @@ module(
});
test("displays indicator when 2 or 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -102,7 +102,7 @@ module(
});
test("displays indicator when 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -118,7 +118,7 @@ module(
});
test("displays indicator when more than 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -135,7 +135,7 @@ module(
});
test("filters current user from list of repliers", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`

View File

@ -3,7 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module(
"Discourse Chat | Component | chat-retention-reminder-text",
@ -11,7 +11,7 @@ module(
setupRenderingTest(hooks);
test("when setting is set on 0", async function (assert) {
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
this.siteSettings.chat_channel_retention_days = 0;
await render(
@ -25,7 +25,7 @@ module(
test("when channel is a public channel", async function (assert) {
const count = 10;
this.channel = fabricators.chatChannel();
this.channel = fabricators.channel();
this.siteSettings.chat_channel_retention_days = count;
await render(
@ -39,7 +39,7 @@ module(
test("when channel is a DM channel", async function (assert) {
const count = 10;
this.channel = fabricators.directMessageChatChannel();
this.channel = fabricators.directMessageChannel();
this.siteSettings.chat_dm_retention_days = count;
await render(

View File

@ -4,7 +4,7 @@ import hbs from "htmlbars-inline-precompile";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import { Promise } from "rsvp";
import fabricators from "../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit";
function mockChat(context, options = {}) {
@ -15,7 +15,7 @@ function mockChat(context, options = {}) {
});
};
mock.getDmChannelForUsernames = () => {
return Promise.resolve({ chat_channel: fabricators.chatChannel() });
return Promise.resolve({ chat_channel: fabricators.channel() });
};
return mock;
}
@ -114,7 +114,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
await fillIn(".filter-usernames", "hawk");
assert.strictEqual(query(".filter-usernames").value, "hawk");
this.set("channel", fabricators.chatChannel());
this.set("channel", fabricators.channel());
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
assert.strictEqual(query(".filter-usernames").value, "");

View File

@ -1,21 +0,0 @@
import { cloneJSON } from "discourse-common/lib/object";
// heavily inspired by https://github.com/travelperk/fabricator
export function Fabricator(Model, attributes = {}) {
return (opts) => fabricate(Model, attributes, opts);
}
function fabricate(Model, attributes, opts = {}) {
if (typeof attributes === "function") {
return attributes();
}
const extendedModel = cloneJSON({ ...attributes, ...opts });
const props = {};
for (const [key, value] of Object.entries(extendedModel)) {
props[key] = typeof value === "function" ? value() : value;
}
return Model.create(props);
}

View File

@ -1,60 +0,0 @@
import ChatChannel, {
CHATABLE_TYPES,
} from "discourse/plugins/chat/discourse/models/chat-channel";
import EmberObject from "@ember/object";
import { Fabricator } from "./fabricator";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
const userFabricator = Fabricator(EmberObject, {
id: 1,
username: "hawk",
name: null,
avatar_template: "/letter_avatar_proxy/v3/letter/t/41988e/{size}.png",
});
const categoryChatableFabricator = Fabricator(EmberObject, {
id: 1,
color: "D56353",
read_restricted: false,
name: "My category",
});
const directChannelChatableFabricator = Fabricator(EmberObject, {
users: [userFabricator({ id: 1, username: "bob" })],
});
export default {
chatChannel: Fabricator(ChatChannel, {
id: 1,
chatable_type: CHATABLE_TYPES.categoryChannel,
status: "open",
title: "My category title",
name: "My category name",
chatable: categoryChatableFabricator(),
last_message_sent_at: "2021-11-08T21:26:05.710Z",
allow_channel_wide_mentions: true,
message_bus_last_ids: {
new_mentions: 0,
new_messages: 0,
},
}),
chatChannelMessage: Fabricator(ChatMessage, {
id: 1,
chat_channel_id: 1,
user_id: 1,
cooked: "This is a test message",
}),
directMessageChatChannel: Fabricator(ChatChannel, {
id: 1,
chatable_type: CHATABLE_TYPES.directMessageChannel,
status: "open",
chatable: directChannelChatableFabricator(),
last_message_sent_at: "2021-11-08T21:26:05.710Z",
message_bus_last_ids: {
new_mentions: 0,
new_messages: 0,
},
}),
};

View File

@ -3,21 +3,20 @@ import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import fabricators from "../../helpers/fabricators";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Unit | Helpers | format-chat-date", function (hooks) {
setupRenderingTest(hooks);
test("link to chat message", async function (assert) {
const channel = fabricators.chatChannel();
this.message = ChatMessage.create(channel, {
id: 1,
chat_channel_id: channel.id,
});
const channel = fabricators.channel();
this.message = fabricators.message({ channel });
await render(hbs`{{format-chat-date this.message}}`);
assert.equal(query(".chat-time").getAttribute("href"), "/chat/c/-/1/1");
assert.equal(
query(".chat-time").getAttribute("href"),
`/chat/c/-/${channel.id}/${this.message.id}`
);
});
});

View File

@ -1,7 +1,7 @@
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { set } from "@ember/object";
import fabricators from "../../helpers/fabricators";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) {
needs.hooks.beforeEach(function () {
@ -69,7 +69,7 @@ acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) {
});
test("#canArchiveChannel", async function (assert) {
const channel = fabricators.chatChannel();
const channel = fabricators.channel();
set(this.currentUser, "has_chat_enabled", true);
set(this.currentUser, "admin", true);

View File

@ -0,0 +1,3 @@
<div class="component">
{{yield}}
</div>

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideComponent extends Component {}

View File

@ -0,0 +1,5 @@
<table class="component-properties">
<tbody>
{{yield}}
</tbody>
</table>

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideControls extends Component {}

View File

@ -0,0 +1,6 @@
<tr class="component-properties__row">
<td class="component-properties__cell">{{@name}}</td>
<td class="component-properties__cell">
{{yield}}
</td>
</tr>

View File

@ -0,0 +1 @@
<DToggleSwitch @state={{@enabled}} {{on "click" @action}} />

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideControlsToggle extends Component {}

View File

@ -0,0 +1 @@
<DButton @action={{this.toggle}} class="toggle-color-mode">Toggle color</DButton>

View File

@ -0,0 +1,45 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
const DARK = "dark";
const LIGHT = "light";
function colorSchemeOverride(type) {
const lightScheme = document.querySelector("link.light-scheme");
const darkScheme = document.querySelector("link.dark-scheme");
if (!lightScheme || !darkScheme) {
return;
}
switch (type) {
case DARK:
lightScheme.media = "none";
darkScheme.media = "all";
break;
case LIGHT:
lightScheme.media = "all";
darkScheme.media = "none";
break;
}
}
export default class ToggleColorMode extends Component {
@service keyValueStore;
@tracked colorSchemeOverride = this.default;
get default() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? DARK
: LIGHT;
}
@action
toggle() {
this.colorSchemeOverride = this.colorSchemeOverride === DARK ? LIGHT : DARK;
colorSchemeOverride(this.colorSchemeOverride);
}
}

View File

@ -1,5 +1,6 @@
<section class="styleguide">
<section class="styleguide-menu">
<ToggleColorMode />
{{#each this.categories as |c|}}
<ul>
<li class="styleguide-heading">{{i18n

View File

@ -73,6 +73,36 @@
.rendered {
width: 100%;
position: relative;
.component {
padding: 2rem;
border: 2px dotted var(--primary-low);
margin-bottom: 2rem;
}
.component-properties {
width: 100%;
&__cell {
padding: 0.5rem 0;
&:first-child {
width: 30%;
}
textarea,
input {
box-sizing: border-box;
margin: 0;
width: 100%;
}
textarea {
height: 100px;
}
}
}
}
margin-bottom: 2em;

View File

@ -101,7 +101,10 @@ module SystemHelpers
ENV["TZ"] = timezone
using_session(timezone) { freeze_time(&example) }
using_session(timezone) do |session|
freeze_time(&example)
session.quit
end
ENV["TZ"] = previous_browser_timezone
end