discourse/spec/system/composer/drafts_spec.rb
Régis HANOL 18ba9fe86e
FIX: use editing-specific text for composer discard and fix false dirty state (#39149)
When editing a post, the composer's cancel button said "Discard" and the
confirmation modal said "Do you want to discard your post?" — which
sounds like you're deleting the post itself. Additionally, the modal
appeared even when no edits were made.

**BEFORE**

| Scenario | Cancel button | Modal message | Modal OK |

|-----------------|---------------|------------------------------------|----------|
| Create topic | Discard | Do you want to discard your post? | Discard |
| Reply | Discard | Do you want to discard your post? | Discard |
| Edit | Discard | Do you want to discard your post? | Discard |
| Private message | Discard | Do you want to discard your post? |
Discard |

\+ 🐛 bug: edit modal appears even with no changes

**AFTER**

| Scenario | Cancel button | Modal message | Modal OK |

|-----------------|---------------|---------------------------------------|-----------------|
| Create topic | Discard | Do you want to discard your post? | Discard |
| Reply | Discard | Do you want to discard your post? | Discard |
| Edit | Cancel edit | Do you want to discard your changes? | Discard
changes |
| Private message | Discard | Do you want to discard your post? |
Discard |

The false dirty state had two causes:

1. `anyDirty` included `titleDirty` even when editing non-first posts
where the title isn't editable. The title was set to the topic title
during the `serialize(_edit_topic_serializer)` call but `originalTitle`
was only set for first posts, making `titleDirty` always true. Fixed by
scoping `titleDirty` in `anyDirty` to only apply when `canEditTitle` is
true, and by always setting `originalTitle` when loading a post for
editing.

2. `hasMetaData` had inverted logic —
`isEmpty(Object.keys(this.metaData))` returned true when metadata was
empty, not when it had keys.

https://meta.discourse.org/t/399942
2026-04-14 23:07:35 +02:00

452 lines
14 KiB
Ruby
Vendored
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
describe "Composer - Drafts" do
fab!(:topic, :topic_with_op)
fab!(:current_user, :admin)
let(:toasts) { PageObjects::Components::Toasts.new }
let(:topic_page) { PageObjects::Pages::Topic.new }
let(:composer) { PageObjects::Components::Composer.new }
let(:discard_draft_modal) { PageObjects::Modals::DiscardDraft.new }
before { sign_in(current_user) }
context "when clicking X (save and close)" do
it "saves the draft and shows a toast" do
visit "/new-topic"
expect(composer).to be_opened
composer.fill_title("this is a test topic")
composer.fill_content("a b c d e f g")
composer.close
expect(toasts).to have_success(I18n.t("js.composer.draft_saved"))
expect(Draft.where(user: current_user).count).to eq(1)
end
context "when only a title and category is specified" do
fab!(:category_1, :category)
fab!(:category_2, :category)
it "saves the draft and shows a toast" do
visit "/new-topic"
expect(composer).to be_opened
composer.fill_title("this is a test topic")
composer.switch_category(category_1.name)
composer.close
expect(toasts).to have_success(I18n.t("js.composer.draft_saved"))
expect(Draft.where(user: current_user).count).to eq(1)
end
end
context "when only title is specified and it is less than min_topic_title_length" do
it "does saves the draft and shows a toast" do
visit "/new-topic"
expect(composer).to be_opened
composer.fill_title("x")
composer.close
expect(composer).to be_closed
expect(toasts).to have_success(I18n.t("js.composer.draft_saved"))
expect(Draft.where(user: current_user).count).to eq(1)
end
end
context "when only body is specified and it is less than min_post_length" do
it "does saves the draft and shows a toast" do
visit "/new-topic"
expect(composer).to be_opened
composer.fill_content("x")
composer.close
expect(composer).to be_closed
expect(toasts).to have_success(I18n.t("js.composer.draft_saved"))
expect(Draft.where(user: current_user).count).to eq(1)
end
end
end
context "when clicking discard" do
let(:dialog) { PageObjects::Components::Dialog.new }
before { Jobs.run_immediately! }
it "does not show confirmation if there is no user input in the composer" do
visit "/new-topic"
expect(composer).to be_opened
composer.discard
expect(discard_draft_modal).to be_closed
expect(composer).to be_closed
end
it "destroys draft after discard confirmation" do
visit "/new-topic"
composer.fill_title("this is a test topic")
composer.fill_content("a b c d e f g")
try_until_success(reason: "Relies on an Ember debounce to update the draft") do
expect(Draft.where(user: current_user).count).to eq(1)
end
composer.discard
expect(discard_draft_modal).to be_open
discard_draft_modal.click_discard
expect(discard_draft_modal).to be_closed
expect(composer).to be_closed
expect(Draft.where(user: current_user).count).to eq(0)
end
context "when only a title and category is specified" do
fab!(:category_1, :category)
fab!(:category_2, :category)
it "shows Discard draft confirmation modal and hides it on Cancel button click" do
visit "/new-topic"
expect(composer).to be_opened
composer.fill_title("this is a test topic")
composer.switch_category(category_1.name)
composer.discard
expect(discard_draft_modal).to be_open
discard_draft_modal.click_cancel
expect(discard_draft_modal).to be_closed
end
end
end
context "when discarding while editing a post" do
fab!(:post_1) { Fabricate(:post, topic:, user: current_user) }
before { Jobs.run_immediately! }
it "does not show the discard modal when there are no changes" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
expect(composer).to be_opened
composer.discard
expect(discard_draft_modal).to be_closed
expect(composer).to be_closed
end
it "does not show the discard modal when editing the first post with no changes" do
first_post = topic.first_post
first_post.update!(user: current_user)
topic_page.visit_topic(topic)
topic_page.click_post_action_button(first_post, :edit)
expect(composer).to be_opened
composer.discard
expect(discard_draft_modal).to be_closed
expect(composer).to be_closed
end
it "shows the discard modal with editing-specific text when there are changes" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
composer.fill_content("edited content here")
composer.discard
expect(discard_draft_modal).to be_open
expect(page).to have_css(
".discard-draft-modal",
text: I18n.t("js.post.cancel_composer.confirm_edit"),
)
expect(page).to have_css(
".discard-draft-modal__discard-btn",
text: I18n.t("js.post.cancel_composer.discard_edit"),
)
end
it "shows the discard modal with editing-specific text when editing the first post" do
first_post = topic.first_post
first_post.update!(user: current_user)
topic_page.visit_topic(topic)
topic_page.click_post_action_button(first_post, :edit)
composer.fill_content("edited content here")
composer.discard
expect(discard_draft_modal).to be_open
expect(page).to have_css(
".discard-draft-modal",
text: I18n.t("js.post.cancel_composer.confirm_edit"),
)
end
it "shows the cancel edit button instead of discard" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
expect(composer).to be_opened
expect(page).to have_css(".discard-button", text: I18n.t("js.composer.cancel_edit"))
end
end
context "when discarding while creating a post" do
before { Jobs.run_immediately! }
it "shows the discard modal with post-specific text when there are changes" do
visit "/new-topic"
composer.fill_title("this is a test topic")
composer.fill_content("a b c d e f g")
composer.discard
expect(discard_draft_modal).to be_open
expect(page).to have_css(
".discard-draft-modal",
text: I18n.t("js.post.cancel_composer.confirm"),
)
expect(page).to have_css(
".discard-draft-modal__discard-btn",
text: I18n.t("js.post.cancel_composer.discard"),
)
end
it "shows the discard button" do
visit "/new-topic"
expect(composer).to be_opened
expect(page).to have_css(".discard-button", text: I18n.t("js.composer.discard"))
end
end
context "when editing different post" do
fab!(:post_1) { Fabricate(:post, topic:, user: current_user) }
fab!(:post_2) { Fabricate(:post, topic:, user: current_user) }
it "shows the discard modal when there are changes in the composer" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
composer.fill_content("a b c d e f g")
composer.minimize
topic_page.click_post_action_button(post_2, :edit)
expect(discard_draft_modal).to be_open
end
it "doesn't show the discard modal when there are no changes in the composer" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
composer.minimize
topic_page.click_post_action_button(post_2, :edit)
expect(discard_draft_modal).to be_closed
expect(composer).to be_opened
end
end
context "when editing the same post" do
fab!(:post_1) { Fabricate(:post, topic:, user: current_user) }
it "doesnt show the discard modal even if there are changes in the composer" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
composer.fill_content("a b c d e f g")
composer.minimize
topic_page.click_post_action_button(post_1, :edit)
expect(discard_draft_modal).to be_closed
expect(composer).to be_opened
expect(composer).to have_content("a b c d e f g")
composer.minimize
expect(composer).to be_minimized
topic_page.click_post_action_button(post_1, :edit)
expect(discard_draft_modal).to be_closed
expect(composer).to be_opened
end
it "doesnt show the discard modal when there are no changes in the composer" do
topic_page.visit_topic(post_1.topic)
topic_page.click_post_action_button(post_1, :edit)
composer.minimize
topic_page.click_post_action_button(post_1, :edit)
expect(discard_draft_modal).to be_closed
expect(composer).to be_opened
end
end
context "when replying to a different topic with an active draft" do
fab!(:other_topic, :topic_with_op)
let(:topic_reply_choice_dialog) { PageObjects::Components::TopicReplyChoiceDialog.new }
let(:topic_list) { PageObjects::Components::TopicList.new }
before do
topic.first_post.update!(raw: "This is the original topic OP content.")
topic.first_post.rebake!
other_topic.first_post.update!(raw: "This is the other topic OP content.")
other_topic.first_post.rebake!
end
def visit_topic_and_save_draft
topic_page.visit_topic(topic)
topic_page.click_reply_button
expect(composer).to be_opened
composer.fill_content("a b c d e f g")
composer.close
expect(toasts).to have_success(I18n.t("js.composer.draft_saved"))
expect(Draft.where(user: current_user).count).to eq(1)
topic_page.visit_topic(topic)
topic_page.click_reply_button
expect(composer).to be_opened
expect(composer).to have_content("a b c d e f g")
end
context "when clicking the original topic in the topic reply choice dialog" do
it "replies with the current content to the original topic" do
visit_topic_and_save_draft
# We have to navigate by clicking through the app to keep the
# composer open.
click_logo
expect(topic_list).to have_topic(other_topic)
topic_list.visit_topic(other_topic)
expect(topic_page).to have_post_content(
post_number: 1,
content: "This is the other topic OP content.",
)
composer.create
expect(topic_reply_choice_dialog).to be_open
expect(topic_reply_choice_dialog).to have_reply_on_original_topic(topic)
topic_reply_choice_dialog.click_reply_on_original
expect(topic_reply_choice_dialog).to be_closed
expect(composer).to be_closed
expect(topic_page).to have_post_content(
post_number: 1,
content: "This is the original topic OP content.",
)
expect(topic_page).to have_post_content(post_number: 2, content: "a b c d e f g")
expect(topic.reload.posts.last.raw).to eq("a b c d e f g")
end
end
context "when clicking the new topic in the topic reply choice dialog" do
it "replies with the current content to the new topic" do
visit_topic_and_save_draft
# We have to navigate by clicking through the app to keep the
# composer open.
click_logo
expect(topic_list).to have_topic(other_topic)
topic_list.visit_topic(other_topic)
expect(topic_page).to have_post_content(
post_number: 1,
content: "This is the other topic OP content.",
)
composer.create
expect(topic_reply_choice_dialog).to be_open
expect(topic_reply_choice_dialog).to have_reply_here_topic(other_topic)
topic_reply_choice_dialog.click_reply_here
expect(topic_reply_choice_dialog).to be_closed
expect(composer).to be_closed
expect(topic_page).to have_post_content(
post_number: 1,
content: "This is the other topic OP content.",
)
expect(topic_page).to have_post_content(post_number: 2, content: "a b c d e f g")
expect(other_topic.reload.posts.last.raw).to eq("a b c d e f g")
end
end
context "when clicking Cancel in the topic reply choice dialog" do
it "saves the current draft and will save future changes to the draft" do
visit_topic_and_save_draft
# We have to navigate by clicking through the app to keep the
# composer open.
click_logo
expect(topic_list).to have_topic(other_topic)
topic_list.visit_topic(other_topic)
expect(topic_page).to have_post_content(
post_number: 1,
content: "This is the other topic OP content.",
)
composer.create
expect(topic_reply_choice_dialog).to be_open
topic_reply_choice_dialog.click_cancel
expect(topic_reply_choice_dialog).to be_closed
composer.fill_content("This is my updated draft content, wow very impressive.")
try_until_success(reason: "Relies on waiting a few seconds for the draft to autosave") do
draft = Draft.where(user: current_user).first
expect(JSON.parse(draft.data)["reply"]).to eq(
"This is my updated draft content, wow very impressive.",
)
end
end
end
end
context "when replying as a linked topic" do
fab!(:category)
it "does not leave a stale reply draft after posting a linked topic" do
topic_page.visit_topic(topic)
topic_page.click_reply_button
expect(composer).to be_opened
composer.fill_content("this is a reply draft")
try_until_success(reason: "wait for draft to be saved") do
expect(Draft.where(user: current_user).count).to eq(1)
end
composer.open_composer_actions
composer.select_action(I18n.t("js.composer.composer_actions.reply_as_new_topic.label"))
expect(composer).to be_opened
composer.fill_title("This is a linked topic title for testing")
composer.fill_content("this is a linked topic body content for testing")
composer.switch_category(category.name)
composer.create
expect(composer).to be_closed
expect(Topic.last.title).to eq("This is a linked topic title for testing")
expect(Draft.where(user: current_user).count).to eq(0)
end
end
end