0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/spec/system/page_objects/components/composer.rb
chapoi a7bcfad6bb
UX: Composer redesign (#41781)
Introduces a new composer redesign behind the `enable_composer_redesign`
upcoming
change, which is `conceptual` for now and thus not shown to admins.

This change modernises the composer UI further, moving the toolbar to
the
bottom and trying an experimental approach to create more space for
writing on mobile

**Desktop**

<img width="781" height="685" alt="image"
src="https://github.com/user-attachments/assets/461e39f1-4409-4f8b-96b9-78a0c7a6877a"
/>
<img width="1490" height="681" alt="image"
src="https://github.com/user-attachments/assets/c1422bc6-c0e2-4d75-8110-d24c507bbc4c"
/>


**Mobile**

<img width="373" height="858" alt="image"
src="https://github.com/user-attachments/assets/d3a3048b-a532-4151-b7f4-efac3b7ebe25"
/>

<img width="376" height="854" alt="image"
src="https://github.com/user-attachments/assets/2a42503a-de55-4684-8edd-789c54eccd69"
/>

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
2026-07-20 17:56:48 +10:00

479 lines
12 KiB
Ruby
Vendored

# frozen_string_literal: true
module PageObjects
module Components
class Composer < PageObjects::Components::Base
COMPOSER_ID = "#reply-control"
AUTOCOMPLETE_MENU = ".autocomplete.ac-emoji"
HASHTAG_MENU = ".autocomplete.hashtag-autocomplete"
MENTION_MENU = ".autocomplete.ac-user"
RICH_EDITOR = ".d-editor-input.ProseMirror"
POST_LANGUAGE_SELECTOR = ".post-language-selector-trigger"
def initialize(composer_id = COMPOSER_ID)
@composer_id = composer_id
end
def rich_editor
find(RICH_EDITOR)
end
def has_rich_editor?
page.has_css?(RICH_EDITOR)
end
def has_no_rich_editor?
page.has_no_css?(RICH_EDITOR)
end
def opened?
page.has_css?("#{@composer_id}.open")
end
def closed?
page.has_css?("#{@composer_id}.closed", visible: :all)
end
def minimized?
page.has_css?("#{@composer_id}.draft")
end
def open_composer_actions
find(".composer-action-title .btn").click
self
end
def open_composer_actions_new
find(".composer-actions-trigger").click
self
end
def click_toolbar_button(button_class)
find(".d-editor-button-bar button.#{button_class}").click
self
end
def heading_menu
PageObjects::Components::DMenu.new(find(".d-editor-button-bar button.heading"))
end
def list_menu
PageObjects::Components::DMenu.new(find(".d-editor-button-bar button.list"))
end
def focus
find(composer_input_selector).click
self
end
def fill_title(title)
find("#{@composer_id} #reply-title").fill_in(with: title)
self
end
def has_input_title?(value)
has_field?("reply-title", with: value)
end
# Methods for when the enable_composer_redesign upcoming change is enabled.
def has_footer_toolbar?
page.has_css?("#{@composer_id} .composer-footer__toolbar .d-editor-button-bar")
end
def has_title_below_category_row?
page.has_css?("#{@composer_id} .title-and-category + .title-input")
end
def has_pm_recipients_in_category_row?
page.has_css?("#{@composer_id} .title-and-category .user-selector")
end
def has_toggle_toolbar_button?
page.has_css?("#{@composer_id} .toggle-toolbar")
end
# Methods for when the enable_composer_redesign upcoming change is disabled.
def has_no_footer_toolbar?
page.has_no_css?("#{@composer_id} .composer-footer")
end
def has_inline_toolbar?
page.has_css?("#{@composer_id} .d-editor-textarea-wrapper .d-editor-button-bar")
end
def has_title_in_category_row?
page.has_css?("#{@composer_id} .title-and-category .title-input")
end
def toggle_toolbar
find("#{@composer_id} .toggle-toolbar").click
self
end
def has_visible_toolbar?
page.has_css?("#{@composer_id} .d-editor-button-bar")
end
def has_no_visible_toolbar?
page.has_no_css?("#{@composer_id} .d-editor-button-bar")
end
def fill_content(content)
find("#{@composer_id} .d-editor .d-editor-input").fill_in(with: content)
self
end
def minimize
find("#{@composer_id} .toggle-minimize").click
self
end
def append_content(content)
current_content = composer_input.value
composer_input.set(current_content + content)
self
end
def fill_form_template_field(field, content)
form_template_field(field).fill_in(with: content)
self
end
def type_content(content)
composer_input.send_keys(content)
self
end
def clear_content
fill_content("")
end
def has_content?(content)
composer_input.value == content
end
def has_value?(value)
within(@composer_id) do
if value.nil?
has_no_field?(class: "d-editor-input")
else
has_field?(class: "d-editor-input", with: value)
end
end
end
def has_popup_content?(content)
composer_popup.has_content?(content)
end
def has_no_action?(action)
!actions.include?(action)
end
def has_no_action_id?(action_id)
has_no_css?(".composer-actions-dropdown [data-action-id='#{action_id}']")
end
def select_action(action)
find(action(action)).click
self
end
def select_action_by_id(action_id)
find(".composer-actions-dropdown [data-action-id='#{action_id}']").click
self
end
def reply_button_focused?
page.has_css?("#{@composer_id} .btn-primary:focus")
end
def create
find("#{@composer_id} .btn-primary").click
end
def action(action_title)
".composer-action-title .select-kit-collection li[title='#{action_title}']"
end
def actions
all(".composer-action-title .select-kit-collection li").map { |el| el[:title] }
end
def button_label
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 category_chooser
Components::SelectKit.new(".category-chooser")
end
def locale
find("#{@composer_id} .d-editor-button-bar #{POST_LANGUAGE_SELECTOR}")
end
def set_locale(locale)
click_toolbar_button("post-language-selector-trigger")
within("#d-menu-portals", visible: false) { find("button", text: locale).click }
end
def switch_category(category_name)
category_chooser.expand
category_chooser.select_row_by_name(category_name)
end
def preview
find("#{@composer_id} .d-editor-preview-wrapper")
end
def has_discard_draft_modal?
page.has_css?(".discard-draft-modal")
end
def has_hashtag_autocomplete?
has_css?(HASHTAG_MENU)
end
def has_mention_autocomplete?
has_css?(MENTION_MENU)
end
def mention_menu_autocomplete_username_list
find(MENTION_MENU).all("a").map { |a| a.text }
end
def has_emoji_autocomplete?
has_css?(AUTOCOMPLETE_MENU)
end
def has_no_emoji_autocomplete?
has_no_css?(AUTOCOMPLETE_MENU)
end
def has_emoji_autocomplete_selected?(index)
has_css?("#{AUTOCOMPLETE_MENU} ul li:nth-child(#{index}) a.selected")
end
EMOJI_SUGGESTION_SELECTOR = "#{AUTOCOMPLETE_MENU} .emoji-shortname"
def has_emoji_suggestion?(emoji)
has_css?(EMOJI_SUGGESTION_SELECTOR, text: emoji)
end
def has_no_emoji_suggestion?(emoji)
has_no_css?(EMOJI_SUGGESTION_SELECTOR, text: emoji)
end
def has_emoji_preview?(emoji)
page.has_css?(emoji_preview_selector(emoji))
end
def has_no_emoji_preview?(emoji)
page.has_no_css?(emoji_preview_selector(emoji))
end
def composer_input_selector
"#{@composer_id} .d-editor-input"
end
def has_no_composer_input?
page.has_no_css?(composer_input_selector)
end
def has_composer_input?
page.has_css?(composer_input_selector)
end
def has_composer_preview?
page.has_css?("#{@composer_id} .d-editor-preview-wrapper")
end
def has_no_composer_preview?
page.has_no_css?("#{@composer_id} .d-editor-preview-wrapper")
end
def has_composer_preview_toggle?
page.has_css?("#{@composer_id} .toggle-preview")
end
def has_no_composer_preview_toggle?
page.has_no_css?("#{@composer_id} .toggle-preview")
end
def has_form_template?
page.has_css?(".form-template-form__wrapper")
end
def has_form_template_field?(field)
page.has_css?(".form-template-field[data-field-type='#{field}']")
end
def has_form_template_field_required_indicator?(field)
page.has_css?(
".form-template-field[data-field-type='#{field}'] .form-template-field__required-indicator",
)
end
FORM_TEMPLATE_CHOOSER_SELECTOR = ".composer-select-form-template"
def has_no_form_template_chooser?
page.has_no_css?(FORM_TEMPLATE_CHOOSER_SELECTOR)
end
def has_form_template_chooser?
page.has_css?(FORM_TEMPLATE_CHOOSER_SELECTOR)
end
def has_form_template_field_error?(error)
page.has_css?(".form-template-field__error", text: error, visible: :all)
end
def has_no_form_template_field_error?(error)
page.has_no_css?(".form-template-field__error", text: error, visible: :all)
end
def has_form_template_field_label?(label)
page.has_css?(".form-template-field__label", text: label)
end
def has_form_template_field_description?(description)
page.has_css?(".form-template-field__description", text: description)
end
def has_post_error?(error)
page.has_css?(".popup-tip", text: error, visible: all)
end
def has_no_post_error?(error)
page.has_no_css?(".popup-tip", text: error, visible: all)
end
def composer_input
find("#{@composer_id} .d-editor-input")
end
def composer_popup
find("#{@composer_id} .composer-popup")
end
def form_template_field(field)
find(".form-template-field[data-field-type='#{field}']")
end
def move_cursor_after(text)
execute_script(<<~JS, text)
const text = arguments[0];
const composer = document.querySelector("#{@composer_id} .d-editor-input");
const index = composer.value.indexOf(text);
const position = index + text.length;
composer.focus();
composer.setSelectionRange(position, position);
JS
end
def select_all
find(composer_input_selector).send_keys([PLATFORM_KEY_MODIFIER, "a"])
end
def select_range(start_index, length)
execute_script(<<~JS, text)
const composer = document.querySelector("#{@composer_id} .d-editor-input");
composer.focus();
composer.setSelectionRange(#{start_index}, #{length});
JS
end
def select_range_rich_editor(start_index, length)
focus
select_text_range(RICH_EDITOR, start_index, length)
end
def submit
find("#{@composer_id} .save-or-cancel .create").click
end
def discard
find("#{@composer_id} .discard-button").click
end
def close
find("#{@composer_id} .toggle-save-and-close").click
end
def has_no_in_progress_uploads?
find("#{@composer_id}").has_no_css?("#file-uploading")
end
def has_in_progress_uploads?
find("#{@composer_id}").has_css?("#file-uploading")
end
def select_pm_user(username)
select_kit = PageObjects::Components::SelectKit.new("#private-message-users")
select_kit.expand
select_kit.search(username)
select_kit.select_row_by_value(username)
select_kit.collapse
end
def has_rich_editor_active?
find("#{@composer_id}").has_css?(".d-editor-container.--rich-editor-enabled")
end
def has_no_rich_editor_active?
find("#{@composer_id}").has_css?(".d-editor-container.--markdown-editor-enabled")
end
def has_markdown_editor_active?
has_no_rich_editor_active?
end
def toggle_rich_editor
rich = page.find(".composer-toggle-switch")["data-rich-editor"]
editor_toggle_switch.click
if rich
has_no_rich_editor_active?
else
has_rich_editor_active?
end
self
end
def has_toggle_switch?
page.has_css?("#{@composer_id} .composer-toggle-switch")
end
def has_no_toggle_switch?
page.has_no_css?("#{@composer_id} .composer-toggle-switch")
end
def editor_toggle_switch
find("#{@composer_id} .composer-toggle-switch")
end
def image_grid
Components::ComposerImageGrid.new(rich_editor)
end
private
def emoji_preview_selector(emoji)
".d-editor-preview .emoji[title=':#{emoji}:']"
end
end
end
end