0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/spec/system/composer/prosemirror_toolbar_spec.rb
chapoi a76d4bb135
FEATURE: Add a small text option to the composer text sizes toolbar button (#42020)
Previously, the composer's "Headings" toolbar button only offered
heading levels and a paragraph reset, with no built-in way to insert
smaller text.

This change renames the button to "Text sizes" and adds a "Small" option
that wraps the selection in `<small>` tags. It toggles on/off and
highlights the active state in both the plain textarea and rich
(ProseMirror) editors, reusing the existing `<small>` inline HTML
support.

<img width="582" height="582" alt="CleanShot 2026-07-24 at 14 15 57@2x"
src="https://github.com/user-attachments/assets/9e76c1d3-bea9-4f3c-a159-7e27ac725217"
/>

It re-uses the same icon as `paragraph` does, for lack of a better
option. The differentiating element is that paragraph doesn't have an
active highlight and small does:
<img width="1552" height="504" alt="CleanShot 2026-07-24 at 14 16 49@2x"
src="https://github.com/user-attachments/assets/981fc522-5c64-42d8-acba-f7dc2ee6d808"
/>

---------

Co-authored-by: Renato Atilio <renato@discourse.org>
2026-08-03 11:28:59 +02:00

358 lines
12 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "Composer - ProseMirror - Toolbar" do
include_context "with prosemirror editor"
describe "toolbar state updates" do
it "updates the toolbar state following the cursor position" do
open_composer
expect(page).to have_css(".toolbar__button.bold.--active", count: 0)
expect(page).to have_css(".toolbar__button.italic.--active", count: 0)
expect(page).to have_css(".toolbar__button.heading.--active", count: 0)
expect(page).to have_css(".toolbar__button.link.--active", count: 0)
expect(page).to have_css(".toolbar__button.list.--active", count: 0)
expect(page).to have_css(".toolbar__button.code.--active", count: 0)
expect(page).to have_css(".toolbar__button.blockquote.--active", count: 0)
composer.type_content("> - [***many `styles`***](https://example.com)")
composer.send_keys(:left, :left)
expect(page).to have_css(".toolbar__button.bold.--active", count: 1)
expect(page).to have_css(".toolbar__button.italic.--active", count: 1)
expect(page).to have_css(".toolbar__button.link.--active", count: 1)
expect(page).to have_css(".toolbar__button.list.--active", count: 1)
expect(page).to have_css(".toolbar__button.code.--active", count: 1)
expect(page).to have_css(".toolbar__button.blockquote.--active", count: 1)
end
end
describe "trailing paragraph" do
it "ensures there is always a trailing paragraph" do
open_composer
expect(rich).to have_css("p", count: 1)
composer.type_content("This is a test")
expect(rich).to have_css("p", count: 1)
expect(rich).to have_css("p", text: "This is a test", count: 1)
composer.send_keys([SystemHelpers::PLATFORM_KEY_MODIFIER, :shift, "_"]) # Insert a horizontal rule
expect(rich).to have_css("hr", count: 1)
expect(rich).to have_css("p", count: 2) # New paragraph inserted after the ruler
end
end
describe "auto-linking/unlinking while typing" do
it "auto-links non-protocol URLs and removes the link when no longer a URL" do
open_composer
composer.type_content("www.example.com and also mid-paragraph www.example2.com")
expect(rich).to have_css("a", text: "www.example.com")
expect(rich).to have_css("a", text: "www.example2.com")
expect(rich).to have_css("a", count: 2)
composer.send_keys(:backspace)
composer.send_keys(:backspace)
expect(rich).to have_css("a", count: 1)
composer.type_content("om")
expect(rich).to have_css("a", text: "www.example2.com")
end
it "auto-links protocol URLs" do
open_composer
composer.type_content("https://example.com")
expect(rich).to have_css("a", text: "https://example.com")
composer.send_keys(:backspace)
composer.send_keys(:backspace)
expect(rich).to have_css("a", text: "https://example.c")
end
it "doesn't auto-link immediately following a `" do
open_composer
composer.type_content("`https://example.com`")
expect(rich).to have_css("code", text: "https://example.com")
expect(rich).to have_no_css("a", text: "https://example.com")
end
it "doesn't auto-link within code marks" do
open_composer
composer.type_content("`code mark`")
composer.send_keys(:left)
composer.type_content(" https://example.com")
expect(rich).to have_css("code", text: "code mark https://example.com")
expect(rich).to have_no_css("a", text: "https://example.com")
end
it "doesn't continue a <https://url> markup='autolink'" do
open_composer
composer.type_content("<https://example.com>.de")
expect(rich).to have_css("a", text: "https://example.com")
expect(rich).to have_no_css("a", text: "https://example.com.de")
composer.toggle_rich_editor
expect(composer).to have_value("<https://example.com>.de")
end
end
describe "emojis" do
it "has the only-emoji class if 1-3 emojis are 'alone'" do
open_composer
composer.type_content("> :smile: ")
expect(rich).to have_css(".only-emoji", count: 1)
composer.type_content(":P ")
expect(rich).to have_css(".only-emoji", count: 2)
composer.type_content(":D ")
expect(rich).to have_css(".only-emoji", count: 3)
composer.type_content("Hey!")
expect(rich).to have_no_css(".only-emoji")
end
it "preserves formatting marks when replacing text with emojis using :code: pattern" do
open_composer
composer.type_content("**bold :smile:**")
expect(rich).to have_css("strong img.emoji")
expect(rich).to have_css("strong", text: "bold")
composer.toggle_rich_editor
expect(composer).to have_value("**bold :smile:**")
end
it "preserves formatting marks when replacing text with emojis using text shortcuts" do
open_composer
composer.type_content("*italics :) *")
expect(rich).to have_css("em img.emoji")
expect(rich).to have_css("em", text: "italics")
composer.toggle_rich_editor
expect(composer).to have_value("*italics :slight_smile:* ")
end
it "preserves link marks when replacing text with emojis" do
open_composer
composer.type_content("[link text :heart:](https://example.com)")
expect(rich).to have_css("a img.emoji")
expect(rich).to have_css("a", text: "link text")
composer.toggle_rich_editor
expect(composer).to have_value("[link text :heart:](https://example.com)")
end
end
describe "list toolbar" do
it "shows the list dropdown on mobile", mobile: true do
open_composer
list_menu = composer.list_menu
list_menu.expand
expect(page).to have_css("[data-name='list-bullet']")
expect(page).to have_css("[data-name='list-ordered']")
end
it "can apply a bullet list from the dropdown" do
open_composer
composer.type_content("A list item")
list_menu = composer.list_menu
list_menu.expand
list_menu.option("[data-name='list-bullet']").click
expect(rich).to have_css("ul li", text: "A list item")
end
it "can apply an ordered list from the dropdown" do
open_composer
composer.type_content("A list item")
list_menu = composer.list_menu
list_menu.expand
list_menu.option("[data-name='list-ordered']").click
expect(rich).to have_css("ol li", text: "A list item")
end
it "splits multi-line plain text into bullet list items" do
cdp.allow_clipboard
open_composer
cdp.copy_paste("apple\nbanana\ncoconut")
composer.select_all
list_menu = composer.list_menu
list_menu.expand
list_menu.option("[data-name='list-bullet']").click
expect(rich).to have_css("ul li", count: 3)
expect(rich).to have_css("ul li", text: "apple")
expect(rich).to have_css("ul li", text: "banana")
expect(rich).to have_css("ul li", text: "coconut")
end
it "splits multi-line plain text into ordered list items" do
cdp.allow_clipboard
open_composer
cdp.copy_paste("apple\nbanana\ncoconut")
composer.select_all
list_menu = composer.list_menu
list_menu.expand
list_menu.option("[data-name='list-ordered']").click
expect(rich).to have_css("ol li", count: 3)
expect(rich).to have_css("ol li", text: "apple")
expect(rich).to have_css("ol li", text: "banana")
expect(rich).to have_css("ol li", text: "coconut")
end
end
describe "heading toolbar" do
it "updates toolbar active state and icon based on current heading level" do
open_composer
composer.type_content("## This is a test\n#### And this is another test")
expect(page).to have_css(".toolbar__button.heading.--active", count: 1)
expect(find(".toolbar__button.heading")).to have_css(".d-icon-discourse-h4")
composer.send_keys(:up)
expect(page).to have_css(".toolbar__button.heading.--active", count: 1)
expect(find(".toolbar__button.heading")).to have_css(".d-icon-discourse-h2")
composer.select_all
expect(page).to have_no_css(".toolbar__button.heading.--active")
expect(find(".toolbar__button.heading")).to have_css(".d-icon-discourse-text")
end
it "puts a check next to current heading level in toolbar dropdown, or no check if multiple formats are selected" do
open_composer
composer.type_content("## This is a test\n#### And this is another test")
heading_menu = composer.heading_menu
heading_menu.expand
expect(heading_menu.option("[data-name='heading-4']")).to have_css(".d-icon-check")
heading_menu.collapse
composer.select_range_rich_editor(0, 0)
expect(find(".toolbar__button.heading")).to have_css(".d-icon-discourse-h2")
heading_menu.expand
expect(heading_menu.option("[data-name='heading-2']")).to have_css(".d-icon-check")
heading_menu.collapse
composer.select_all
expect(page).to have_no_css(".toolbar__button.heading.--active")
heading_menu.expand
expect(heading_menu.option("[data-name='heading-2']")).to have_no_css(".d-icon-check")
expect(heading_menu.option("[data-name='heading-4']")).to have_no_css(".d-icon-check")
end
it "can change heading level or reset to paragraph" do
open_composer
composer.type_content("This is a test")
heading_menu = composer.heading_menu
heading_menu.expand
heading_menu.option("[data-name='heading-2']").click
expect(rich).to have_css("h2", text: "This is a test")
heading_menu.expand
heading_menu.option("[data-name='heading-3']").click
expect(rich).to have_css("h3", text: "This is a test")
heading_menu.expand
heading_menu.option("[data-name='heading-paragraph']").click
expect(rich).to have_css("p", text: "This is a test")
end
it "can insert a heading on an empty line" do
open_composer
heading_menu = composer.heading_menu
heading_menu.expand
heading_menu.option("[data-name='heading-2']").click
composer.type_content("This is a test")
expect(rich).to have_css("h2", text: "This is a test")
end
it "lets the user apply and remove small text" do
open_composer
composer.type_content("This is a test")
composer.select_all
composer.send_keys([SystemHelpers::PLATFORM_KEY_MODIFIER, :alt, "5"])
expect(rich).to have_css("p small", text: "This is a test")
rich.find("small").click
heading_menu = composer.heading_menu
heading_menu.expand
expect(heading_menu.option("[data-name='heading-small']")).to have_css(".d-icon-check")
expect(heading_menu.option("[data-name='heading-paragraph']")).to have_no_css(".d-icon-check")
heading_menu.option("[data-name='heading-small']").click
expect(rich).to have_no_css("small")
expect(rich).to have_css("p", text: "This is a test")
end
it "lets the user switch between a heading and small text" do
open_composer
composer.type_content("This is a test")
composer.select_all
heading_menu = composer.heading_menu
heading_menu.expand
heading_menu.option("[data-name='heading-2']").click
expect(rich).to have_css("h2", text: "This is a test")
composer.select_all
heading_menu.expand
heading_menu.option("[data-name='heading-small']").click
expect(rich).to have_css("p small", text: "This is a test")
expect(rich).to have_no_css("h2")
composer.select_all
heading_menu.expand
heading_menu.option("[data-name='heading-2']").click
expect(rich).to have_css("h2", text: "This is a test")
expect(rich).to have_no_css("small")
end
end
end