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