From 967010e5459f57b7112ae17498c379603bd00d8e Mon Sep 17 00:00:00 2001 From: David Battersby Date: Thu, 13 Apr 2023 15:38:54 +0800 Subject: [PATCH] FEATURE: Add an emoji deny list site setting (#20929) This feature will allow sites to define which emoji are not allowed. Emoji in this list should be excluded from the set we show in the core emoji picker used in the composer for posts when emoji are enabled. And they should not be allowed to be chosen to be added to messages or as reactions in chat. This feature prevents denied emoji from appearing in the following scenarios: - topic title and page title - private messages (topic title and body) - inserting emojis into a chat - reacting to chat messages - using the emoji picker (composer, user status etc) - using search within emoji picker It also takes into account the various ways that emojis can be accessed, such as: - emoji autocomplete suggestions - emoji favourites (auto populates when adding to emoji deny list for example) - emoji inline translations - emoji skintones (ie. for certain hand gestures) --- .../discourse/app/components/d-editor.js | 10 +- .../discourse/app/components/emoji-picker.js | 1 + .../javascripts/discourse/app/lib/text.js | 3 + .../javascripts/pretty-text/addon/emoji.js | 9 +- .../pretty-text/addon/pretty-text.js | 2 + .../engines/discourse-markdown/emoji.js | 17 ++- app/models/emoji.rb | 32 +++++- app/serializers/site_serializer.rb | 9 ++ .../initializers/014-track-setting-changes.rb | 2 + config/locales/server.en.yml | 1 + config/site_settings.yml | 5 + lib/pretty_text.rb | 1 + .../app/controllers/chat/emojis_controller.rb | 2 +- .../discourse/components/chat-composer.js | 4 +- .../discourse/components/chat-emoji-picker.js | 16 +-- .../chat/spec/system/chat_composer_spec.rb | 22 ++++ .../chat/spec/system/react_to_message_spec.rb | 12 +++ spec/models/emoji_spec.rb | 9 ++ .../api/schemas/json/site_response.json | 3 + spec/system/emojis/emoji_deny_list_spec.rb | 101 ++++++++++++++++++ .../page_objects/components/composer.rb | 26 +++++ .../page_objects/components/emoji_picker.rb | 19 ++++ .../page_objects/pages/admin_settings.rb | 21 ++++ spec/system/page_objects/pages/topic.rb | 4 + 24 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 spec/system/emojis/emoji_deny_list_spec.rb create mode 100644 spec/system/page_objects/components/emoji_picker.rb diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 03734ae4b8c..41a435e3d8e 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -530,7 +530,11 @@ export default Component.extend(TextareaTextManipulation, { if (term === "") { if (this.emojiStore.favorites.length) { - return resolve(this.emojiStore.favorites.slice(0, 5)); + return resolve( + this.emojiStore.favorites + .filter((f) => !this.site.denied_emojis?.includes(f)) + .slice(0, 5) + ); } else { return resolve([ "slight_smile", @@ -555,12 +559,13 @@ export default Component.extend(TextareaTextManipulation, { return resolve([allTranslations[full]]); } + const emojiDenied = this.get("site.denied_emojis") || []; const match = term.match(/^:?(.*?):t([2-6])?$/); if (match) { const name = match[1]; const scale = match[2]; - if (isSkinTonableEmoji(name)) { + if (isSkinTonableEmoji(name) && !emojiDenied.includes(name)) { if (scale) { return resolve([`${name}:t${scale}`]); } else { @@ -572,6 +577,7 @@ export default Component.extend(TextareaTextManipulation, { const options = emojiSearch(term, { maxResults: 5, diversity: this.emojiStore.diversity, + exclude: emojiDenied, }); return resolve(options); diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 36510e9a38a..f1da2a1dd17 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -380,6 +380,7 @@ export default Component.extend({ if (filter) { results.innerHTML = emojiSearch(filter.toLowerCase(), { diversity: this.emojiStore.diversity, + exclude: this.site.denied_emojis, }) .map(this._replaceEmoji) .join(""); diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index f87aca89a40..c8cfc3fc55e 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -19,6 +19,7 @@ function getOpts(opts) { currentUser: context.currentUser, censoredRegexp: context.site.censored_regexp, customEmojiTranslation: context.site.custom_emoji_translation, + emojiDenyList: context.site.denied_emojis, siteSettings: context.siteSettings, formatUsername, watchedWordsReplace: context.site.watched_words_replace, @@ -96,6 +97,7 @@ function createPrettyText(options) { function emojiOptions() { let siteSettings = helperContext().siteSettings; + let context = helperContext(); if (!siteSettings.enable_emoji) { return; } @@ -105,6 +107,7 @@ function emojiOptions() { emojiSet: siteSettings.emoji_set, enableEmojiShortcuts: siteSettings.enable_emoji_shortcuts, inlineEmoji: siteSettings.enable_inline_emoji_translation, + emojiDenyList: context.site.denied_emojis, emojiCDNUrl: siteSettings.external_emoji_url, }; } diff --git a/app/assets/javascripts/pretty-text/addon/emoji.js b/app/assets/javascripts/pretty-text/addon/emoji.js index 4094480496b..6fa708814bb 100644 --- a/app/assets/javascripts/pretty-text/addon/emoji.js +++ b/app/assets/javascripts/pretty-text/addon/emoji.js @@ -88,6 +88,11 @@ export function performEmojiUnescape(string, opts) { classes += ` ${opts.class}`; } + // hides denied emojis and aliases from the emoji picker + if (opts.emojiDenyList?.includes(emojiVal)) { + return ""; + } + const isReplacable = (isEmoticon || hasEndingColon || isUnicodeEmoticon) && isReplacableInlineEmoji(string, index, opts.inlineEmoji); @@ -184,6 +189,7 @@ let toSearch; export function emojiSearch(term, options) { const maxResults = options?.maxResults; const diversity = options?.diversity; + const exclude = options?.exclude || []; if (maxResults === 0) { return []; } @@ -200,7 +206,8 @@ export function emojiSearch(term, options) { function addResult(t) { const val = aliasMap.get(t) || t; - if (!results.includes(val)) { + // dont add skin tone variations or alias of denied emoji to search results + if (!results.includes(val) && !exclude.includes(val)) { if (diversity && diversity > 1 && isSkinTonableEmoji(val)) { results.push(`${val}:t${diversity}`); } else { diff --git a/app/assets/javascripts/pretty-text/addon/pretty-text.js b/app/assets/javascripts/pretty-text/addon/pretty-text.js index 631a9fb7f8b..7d572eb5be5 100644 --- a/app/assets/javascripts/pretty-text/addon/pretty-text.js +++ b/app/assets/javascripts/pretty-text/addon/pretty-text.js @@ -43,6 +43,7 @@ export function buildOptions(state) { customEmojiTranslation, watchedWordsReplace, watchedWordsLink, + emojiDenyList, featuresOverride, markdownItRules, additionalOptions, @@ -88,6 +89,7 @@ export function buildOptions(state) { disableEmojis, watchedWordsReplace, watchedWordsLink, + emojiDenyList, featuresOverride, markdownItRules, additionalOptions, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js index ce7f619e94b..0d59c83bf03 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js @@ -196,7 +196,8 @@ function applyEmoji( enableShortcuts, inlineEmoji, customEmojiTranslation, - watchedWordsReplacer + watchedWordsReplacer, + emojiDenyList ) { let result = null; let start = 0; @@ -224,6 +225,16 @@ function applyEmoji( }); } + // prevent denied emoji and aliases from being rendered + if (emojiDenyList?.length > 0) { + emojiDenyList.forEach((emoji) => { + if (content?.match(emoji)) { + const regex = new RegExp(`:${emoji}:`, "g"); + content = content.replace(regex, ""); + } + }); + } + let end = content.length; for (let i = 0; i < content.length - 1; i++) { @@ -346,6 +357,7 @@ export function setup(helper) { opts.emojiSet = siteSettings.emoji_set || ""; opts.customEmoji = state.customEmoji; opts.emojiCDNUrl = siteSettings.external_emoji_url; + opts.emojiDenyList = state.emojiDenyList; }); helper.registerPlugin((md) => { @@ -358,7 +370,8 @@ export function setup(helper) { md.options.discourse.features.emojiShortcuts, md.options.discourse.features.inlineEmoji, md.options.discourse.customEmojiTranslation, - md.options.discourse.watchedWordsReplace + md.options.discourse.watchedWordsReplace, + md.options.discourse.emojiDenyList ) ) ); diff --git a/app/models/emoji.rb b/app/models/emoji.rb index 602239b33b5..4d85b4eb530 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -28,6 +28,14 @@ class Emoji Discourse.cache.fetch(cache_key("standard_emojis")) { load_standard } end + def self.allowed + Discourse.cache.fetch(cache_key("allowed_emojis")) { load_allowed } + end + + def self.denied + Discourse.cache.fetch(cache_key("denied_emojis")) { load_denied } + end + def self.aliases db["aliases"] end @@ -114,7 +122,7 @@ class Emoji end def self.clear_cache - %w[custom standard translations all].each do |key| + %w[custom standard translations allowed denied all].each do |key| Discourse.cache.delete(cache_key("#{key}_emojis")) end global_emoji_cache.clear @@ -150,6 +158,26 @@ class Emoji db["emojis"].map { |e| Emoji.create_from_db_item(e) }.compact end + def self.load_allowed + denied_emojis = denied + all_emojis = load_standard + load_custom + + if denied_emojis.present? + all_emojis.reject { |e| denied_emojis.include?(e.name) } + else + all_emojis + end + end + + def self.load_denied + if SiteSetting.emoji_deny_list.present? + denied_emoji = SiteSetting.emoji_deny_list.split("|") + if denied_emoji.size > 0 + denied_emoji.concat(denied_emoji.flat_map { |e| Emoji.aliases[e] }.compact) + end + end + end + def self.load_custom result = [] @@ -243,6 +271,8 @@ class Emoji end def self.lookup_unicode(name) + return "" if denied&.include?(name) + @reverse_map ||= begin map = {} diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index c518e0cf9b1..3737f604da2 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -41,6 +41,7 @@ class SiteSerializer < ApplicationSerializer :anonymous_default_sidebar_tags, :anonymous_sidebar_sections, :whispers_allowed_groups_names, + :denied_emojis, ) has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer @@ -280,6 +281,14 @@ class SiteSerializer < ApplicationSerializer scope.can_see_whispers? end + def denied_emojis + @denied_emojis ||= Emoji.denied + end + + def include_denied_emojis? + denied_emojis.present? + end + private def ordered_flags(flags) diff --git a/config/initializers/014-track-setting-changes.rb b/config/initializers/014-track-setting-changes.rb index 7872dcd129b..6fa3a40b613 100644 --- a/config/initializers/014-track-setting-changes.rb +++ b/config/initializers/014-track-setting-changes.rb @@ -63,4 +63,6 @@ DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value| if name == :reviewable_low_priority_threshold && Reviewable.min_score_for_priority(:medium) > 0 Reviewable.set_priorities(low: new_value) end + + Emoji.clear_cache && Discourse.request_refresh! if name == :emoji_deny_list end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6459ed17605..d9363058edb 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2264,6 +2264,7 @@ en: emoji_set: "How would you like your emoji?" emoji_autocomplete_min_chars: "Minimum number of characters required to trigger autocomplete emoji popup" enable_inline_emoji_translation: "Enables translation for inline emojis (without any space or punctuation before)" + emoji_deny_list: "These emoji will not be available to use in menus or shortcodes." approve_post_count: "The amount of posts from a new or basic user that must be approved" approve_unless_trust_level: "Posts for users below this trust level must be approved" diff --git a/config/site_settings.yml b/config/site_settings.yml index fa1869239d0..ca9bd51d4df 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1012,6 +1012,11 @@ posting: zh_TW: true ja: true ko: true + emoji_deny_list: + type: emoji_list + default: "" + client: true + refresh: true approve_post_count: default: 0 approve_unless_trust_level: diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 0f679713ec9..986dbccecd0 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -202,6 +202,7 @@ module PrettyText __optInput.customEmoji = #{custom_emoji.to_json}; __optInput.customEmojiTranslation = #{Plugin::CustomEmoji.translations.to_json}; __optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer; + __optInput.emojiDenyList = #{Emoji.denied.to_json}; __optInput.lookupUploadUrls = __lookupUploadUrls; __optInput.censoredRegexp = #{WordWatcher.serializable_word_matcher_regexp(:censor).to_json}; __optInput.watchedWordsReplace = #{WordWatcher.word_matcher_regexps(:replace).to_json}; diff --git a/plugins/chat/app/controllers/chat/emojis_controller.rb b/plugins/chat/app/controllers/chat/emojis_controller.rb index 6d70cc4af96..e27f8ae537e 100644 --- a/plugins/chat/app/controllers/chat/emojis_controller.rb +++ b/plugins/chat/app/controllers/chat/emojis_controller.rb @@ -3,7 +3,7 @@ module Chat class EmojisController < ::Chat::BaseController def index - emojis = Emoji.all.group_by(&:group) + emojis = Emoji.allowed.group_by(&:group) render json: MultiJson.dump(emojis) end end diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js index 4bce7b50170..a1172d47129 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js @@ -480,12 +480,13 @@ export default Component.extend(TextareaTextManipulation, { return resolve([allTranslations[full]]); } + const emojiDenied = this.get("site.denied_emojis") || []; const match = term.match(/^:?(.*?):t([2-6])?$/); if (match) { const name = match[1]; const scale = match[2]; - if (isSkinTonableEmoji(name)) { + if (isSkinTonableEmoji(name) && !emojiDenied.includes(name)) { if (scale) { return resolve([`${name}:t${scale}`]); } else { @@ -497,6 +498,7 @@ export default Component.extend(TextareaTextManipulation, { const options = emojiSearch(term, { maxResults: 5, diversity: this.chatEmojiReactionStore.diversity, + exclude: emojiDenied, }); return resolve(options); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js index 23143ed8054..15463a22004 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js @@ -51,13 +51,15 @@ export default class ChatEmojiPicker extends Component { get groups() { const emojis = this.chatEmojiPickerManager.emojis; const favorites = { - favorites: this.chatEmojiReactionStore.favorites.map((name) => { - return { - name, - group: "favorites", - url: emojiUrlFor(name), - }; - }), + favorites: this.chatEmojiReactionStore.favorites + .filter((f) => !this.site.denied_emojis?.includes(f)) + .map((name) => { + return { + name, + group: "favorites", + url: emojiUrlFor(name), + }; + }), }; return { diff --git a/plugins/chat/spec/system/chat_composer_spec.rb b/plugins/chat/spec/system/chat_composer_spec.rb index 9f8e53796b6..fde6408d1a0 100644 --- a/plugins/chat/spec/system/chat_composer_spec.rb +++ b/plugins/chat/spec/system/chat_composer_spec.rb @@ -102,6 +102,17 @@ RSpec.describe "Chat composer", type: :system, js: true do expect(find(".chat-composer-input").value).to eq(":grimacing:") end + + it "removes denied emojis from insert emoji picker" do + SiteSetting.emoji_deny_list = "monkey|peach" + + chat.visit_channel(channel_1) + channel.open_action_menu + channel.click_action_button("emoji") + + expect(page).to have_no_selector("[data-emoji='monkey']") + expect(page).to have_no_selector("[data-emoji='peach']") + end end context "when adding an emoji through the autocomplete" do @@ -117,6 +128,17 @@ RSpec.describe "Chat composer", type: :system, js: true do expect(find(".chat-composer-input").value).to eq(":grimacing: ") end + + it "doesn't suggest denied emojis and aliases" do + SiteSetting.emoji_deny_list = "peach|poop" + chat.visit_channel(channel_1) + + find(".chat-composer-input").fill_in(with: ":peac") + expect(page).to have_no_selector(".emoji-shortname", text: "peach") + + find(".chat-composer-input").fill_in(with: ":hank") # alias + expect(page).to have_no_selector(".emoji-shortname", text: "poop") + end end context "when opening emoji picker through more button of the autocomplete" do diff --git a/plugins/chat/spec/system/react_to_message_spec.rb b/plugins/chat/spec/system/react_to_message_spec.rb index 3589d747d1a..dedf724afb0 100644 --- a/plugins/chat/spec/system/react_to_message_spec.rb +++ b/plugins/chat/spec/system/react_to_message_spec.rb @@ -111,6 +111,18 @@ RSpec.describe "React to message", type: :system, js: true do expect(channel).to have_reaction(message_1, reaction_1.emoji) end + + it "removes denied emojis and aliases from reactions" do + SiteSetting.emoji_deny_list = "fu" + + sign_in(current_user) + chat.visit_channel(category_channel_1) + channel.hover_message(message_1) + find(".chat-message-actions .react-btn").click + + expect(page).to have_no_css(".chat-emoji-picker [data-emoji=\"fu\"]") + expect(page).to have_no_css(".chat-emoji-picker [data-emoji=\"middle_finger\"]") + end end context "when using frequent reactions" do diff --git a/spec/models/emoji_spec.rb b/spec/models/emoji_spec.rb index 2590dcfb56b..2c48c01cb1d 100644 --- a/spec/models/emoji_spec.rb +++ b/spec/models/emoji_spec.rb @@ -23,6 +23,11 @@ RSpec.describe Emoji do end describe ".lookup_unicode" do + before do + SiteSetting.emoji_deny_list = "peach" + Emoji.clear_cache + end + it "should return the emoji" do expect(Emoji.lookup_unicode("blonde_man")).to eq("👱") end @@ -34,6 +39,10 @@ RSpec.describe Emoji do it "should return a skin toned emoji" do expect(Emoji.lookup_unicode("blonde_woman:t6")).to eq("👱🏿‍♀️") end + + it "should not return a fu emoji when emoji is in emoji deny list site setting" do + expect(Emoji.lookup_unicode("peach")).not_to eq("🍑") + end end describe ".url_for" do diff --git a/spec/requests/api/schemas/json/site_response.json b/spec/requests/api/schemas/json/site_response.json index 8223bcf80bc..09bc1621926 100644 --- a/spec/requests/api/schemas/json/site_response.json +++ b/spec/requests/api/schemas/json/site_response.json @@ -765,6 +765,9 @@ }, "whispers_allowed_groups_names" : { "type": "array" + }, + "denied_emojis" : { + "type": "array" } }, "required": [ diff --git a/spec/system/emojis/emoji_deny_list_spec.rb b/spec/system/emojis/emoji_deny_list_spec.rb new file mode 100644 index 00000000000..36d2dcce56d --- /dev/null +++ b/spec/system/emojis/emoji_deny_list_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +describe "Emoji deny list", type: :system, js: true do + let(:topic_page) { PageObjects::Pages::Topic.new } + let(:composer) { PageObjects::Components::Composer.new } + let(:emoji_picker) { PageObjects::Components::EmojiPicker.new } + fab!(:admin) { Fabricate(:admin) } + + before { sign_in(admin) } + + describe "when editing admin settings" do + before { SiteSetting.emoji_deny_list = "" } + let(:site_settings_page) { PageObjects::Pages::AdminSettings.new } + + it "should allow admin to update emoji deny list" do + site_settings_page.visit_category("posting") + + site_settings_page.select_from_emoji_list("emoji_deny_list", "fu", false) + site_settings_page.select_from_emoji_list("emoji_deny_list", "poop") + + expect(site_settings_page.values_in_list("emoji_deny_list")).to eq(%w[fu poop]) + end + end + + describe "when visiting topics" do + SiteSetting.emoji_deny_list = "monkey" + Emoji.clear_cache + + fab!(:topic) { Fabricate(:topic, title: "Time for :monkey: business") } + fab!(:post) { Fabricate(:post, topic: topic, raw: "We have no time to :monkey: around!") } + + it "should remove denied emojis from page title, heading and body" do + topic_page.visit_topic(topic) + expect(page.title).to eq("Time for business - Discourse") + expect(topic_page).to have_topic_title("Time for business") + expect(page).not_to have_css(".emoji[title=':monkey:']") + end + end + + describe "when using composer" do + before do + SiteSetting.emoji_deny_list = "fu|poop" + Emoji.clear_cache && Discourse.request_refresh! + end + + fab!(:topic) { Fabricate(:topic) } + fab!(:post) { Fabricate(:post, topic: topic) } + + it "should remove denied emojis from emoji picker" do + topic_page.visit_topic_and_open_composer(topic) + expect(composer).to be_opened + + composer.click_toolbar_button(10) + expect(composer.emoji_picker).to be_visible + + expect(emoji_picker.has_emoji?("fu")).to eq(false) + end + + it "should not show denied emojis and aliases in emoji autocomplete" do + topic_page.visit_topic_and_open_composer(topic) + + composer.type_content(":poop") # shows no results + expect(composer).not_to have_emoji_autocomplete + + composer.clear_content + + composer.type_content(":middle") # middle_finger is alias + expect(composer).not_to have_emoji_suggestion("fu") + end + + it "should not show denied emoji in preview" do + topic_page.visit_topic_and_open_composer(topic) + + composer.fill_content(":wave:") + expect(composer).to have_emoji_preview("wave") + + composer.clear_content + + composer.fill_content(":fu:") + expect(composer).not_to have_emoji_preview("fu") + end + end + + describe "when using private messages" do + before do + SiteSetting.emoji_deny_list = "pancakes|monkey" + Emoji.clear_cache && Discourse.request_refresh! + end + + fab!(:topic) do + Fabricate(:private_message_topic, title: "Want to catch up for :pancakes: today?") + end + fab!(:post) { Fabricate(:post, topic: topic, raw: "Can we use the :monkey: emoji here?") } + + it "should remove denied emojis from message title and body" do + topic_page.visit_topic(topic) + expect(topic_page).to have_topic_title("Want to catch up for today?") + expect(post).not_to have_css(".emoji[title=':monkey:']") + end + end +end diff --git a/spec/system/page_objects/components/composer.rb b/spec/system/page_objects/components/composer.rb index 01d0012e079..2cd15633d2a 100644 --- a/spec/system/page_objects/components/composer.rb +++ b/spec/system/page_objects/components/composer.rb @@ -4,6 +4,7 @@ module PageObjects module Components class Composer < PageObjects::Components::Base COMPOSER_ID = "#reply-control" + AUTOCOMPLETE_MENU = ".autocomplete.ac-emoji" def opened? page.has_css?("#{COMPOSER_ID}.open") @@ -14,6 +15,11 @@ module PageObjects self end + def click_toolbar_button(number) + find(".d-editor-button-bar button:nth-child(#{number})").click + self + end + def fill_title(title) find("#{COMPOSER_ID} #reply-title").fill_in(with: title) self @@ -54,6 +60,26 @@ module PageObjects find("#{COMPOSER_ID} .btn-primary .d-button-label") end + def emoji_picker + find("#{COMPOSER_ID} .emoji-picker") + end + + def emoji_autocomplete + find(AUTOCOMPLETE_MENU) + end + + def has_emoji_autocomplete? + has_css?(AUTOCOMPLETE_MENU) + end + + def has_emoji_suggestion?(emoji) + has_css?("#{AUTOCOMPLETE_MENU} .emoji-shortname", text: emoji) + end + + def has_emoji_preview?(emoji) + page.has_css?(".d-editor-preview .emoji[title=':#{emoji}:']") + end + def composer_input find("#{COMPOSER_ID} .d-editor .d-editor-input") end diff --git a/spec/system/page_objects/components/emoji_picker.rb b/spec/system/page_objects/components/emoji_picker.rb new file mode 100644 index 00000000000..715d129ccce --- /dev/null +++ b/spec/system/page_objects/components/emoji_picker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class EmojiPicker < PageObjects::Components::Base + def select_emoji(emoji_name) + find(".emoji-picker .emoji[title='#{emoji_name}']").click + end + + def search_emoji(emoji_name) + find(".emoji-picker .search input").fill_in(with: emoji_name) + end + + def has_emoji?(emoji_name) + page.has_css?(".emoji-picker .emoji[title='#{emoji_name}']") + end + end + end +end diff --git a/spec/system/page_objects/pages/admin_settings.rb b/spec/system/page_objects/pages/admin_settings.rb index e7187490ee2..c813ec6dd36 100644 --- a/spec/system/page_objects/pages/admin_settings.rb +++ b/spec/system/page_objects/pages/admin_settings.rb @@ -8,11 +8,32 @@ module PageObjects self end + def visit_category(category) + page.visit("/admin/site_settings/category/#{category}") + self + end + def toggle_setting(setting_name, text = "") setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']") setting.find(".setting-value span", text: text).click setting.find(".setting-controls button.ok").click end + + def select_from_emoji_list(setting_name, text = "", save_changes = true) + setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']") + setting.find(".setting-value .value-list > .value button").click + setting.find(".setting-value .emoji-picker .emoji[title='#{text}']").click + setting.find(".setting-controls button.ok").click if save_changes + end + + def values_in_list(setting_name) + vals = [] + setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']") + setting + .all(:css, ".setting-value .values .value .value-input span") + .map { |e| vals << e.text } + vals + end end end end diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb index a9a75c8ba67..649f15c11b9 100644 --- a/spec/system/page_objects/pages/topic.rb +++ b/spec/system/page_objects/pages/topic.rb @@ -30,6 +30,10 @@ module PageObjects self end + def has_topic_title?(text) + has_css?("h1 .fancy-title", text: text) + end + def has_post_content?(post) post_by_number(post).has_content? post.raw end