0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-12 02:52:01 +08:00
discourse/spec/system/page_objects/pages/review.rb
Alan Guo Xiang Tan b03428cfec
FIX: Resolve all of a deleted spammer's reviewables (#40780)
When a moderator picks "Delete User" or "Delete and Block User" on one
of the spammer's flagged posts, deleting the user is meant to settle all
of their reviewables, since there is no user left to act on. Previously
only part of that happened:

- `UserDestroyer#agree_with_flags` resolved the spammer's other flagged
posts by checking for the `agree_and_keep` action, but a flag on a
hidden post only offers `agree_and_keep_hidden`, so those flags were
skipped and stayed pending forever.
- Queued posts by the spammer were never touched, so they also stayed
pending.
- `UserDestroyer#destroy` skips resolving the spammer's account
reviewable when a `reviewable_id` option is present. That option is
meant to prevent the account reviewable from resolving itself twice when
the deletion starts from it, but the guard only checked presence, so a
deletion starting from a flagged post also skipped the account
reviewable.
- The browser updates review queue rows from message bus broadcasts.
Those can arrive after the page has stopped waiting, so the acting
moderator kept seeing the affected rows as "Pending" until a full
reload. Acting on one of those stale rows returns a 403 because the
target user no longer exists.

This PR resolves every reviewable tied to the deleted user and updates
the acting moderator's review queue without a reload.
2026-06-15 13:34:15 +08:00

294 lines
8.7 KiB
Ruby
Vendored

# frozen_string_literal: true
module PageObjects
module Pages
class Review < PageObjects::Pages::Base
POST_BODY_TOGGLE_SELECTOR = ".post-body__toggle-btn"
POST_BODY_COLLAPSED_SELECTOR = ".post-body.is-collapsed"
REVIEWABLE_ACTION_DROPDOWN = ".reviewable-action-dropdown"
def visit_reviewable(reviewable)
page.visit("/review/#{reviewable.id}")
self
end
def select_bundled_action(reviewable, value, bundle_index: nil)
within(reviewable_by_id(reviewable.id)) do
dropdown =
if bundle_index
PageObjects::Components::SelectKit.new(
"#{REVIEWABLE_ACTION_DROPDOWN}:nth-of-type(#{bundle_index})",
)
else
reviewable_action_dropdown
end
dropdown.select_row_by_value(value)
end
end
def select_action(reviewable, value)
within(reviewable_by_id(reviewable.id)) do
find(".reviewable-action.#{value.dasherize}").click
end
end
def delete_user_from_reviewable(reviewable, action)
select_bundled_action(reviewable, action, bundle_index: 1)
end
def click_post_body_toggle
find(POST_BODY_TOGGLE_SELECTOR).click
end
def has_post_body_toggle?
page.has_css?(POST_BODY_TOGGLE_SELECTOR)
end
def has_no_post_body_toggle?
page.has_no_css?(POST_BODY_TOGGLE_SELECTOR)
end
def has_post_body_collapsed?
page.has_css?(POST_BODY_COLLAPSED_SELECTOR)
end
def has_no_post_body_collapsed?
page.has_no_css?(POST_BODY_COLLAPSED_SELECTOR)
end
def has_reviewable_action_dropdown?
page.has_css?(REVIEWABLE_ACTION_DROPDOWN)
end
def has_no_reviewable_action_dropdown?
page.has_no_css?(REVIEWABLE_ACTION_DROPDOWN)
end
def has_reviewable_items?(count:)
page.has_css?(".review-item", count: count)
end
def has_reviewable_with_rejection_reason?(reviewable, rejection_reason)
reviewable_by_id(reviewable.id).has_css?(
".reviewable-user-details.reject-reason .value",
text: rejection_reason,
)
end
def has_scrub_button?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_css?(".scrub-rejected-user button") }
end
def has_no_scrub_button?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_no_css?(".scrub-rejected-user button") }
end
def click_scrub_user_button
find(".user-scrub").click
end
def has_reviewable_with_scrubbed_by?(reviewable, scrubbed_by)
within(reviewable_by_id(reviewable.id)) do
page.has_css?(".reviewable-user-details.scrubbed-by .value", text: scrubbed_by)
end
end
def has_reviewable_with_scrubbed_reason?(reviewable, scrubbed_reason)
within(reviewable_by_id(reviewable.id)) do
page.has_css?(".reviewable-user-details.scrubbed-reason .value", text: scrubbed_reason)
end
end
def has_reviewable_with_scrubbed_at?(reviewable, scrubbed_at)
within(reviewable_by_id(reviewable.id)) do
page.has_css?(".reviewable-user-details.scrubbed-at .value", text: scrubbed_at)
end
end
def has_no_error_dialog_visible?
page.has_no_css?("dialog-container .dialog-content")
end
def click_ignore_all_unknown_reviewables
find(".unknown-reviewables__options button").click
find(".dialog-footer .btn-danger").click
end
def has_information_about_unknown_reviewables_visible?
page.has_css?(".unknown-reviewables")
end
def has_no_information_about_unknown_reviewables_visible?
page.has_no_css?(".unknown-reviewables")
end
def has_listing_for_unknown_reviewables_plugin?(reviewable_type, plugin_name)
page.has_css?(
".unknown-reviewables ul li",
text:
I18n.t(
"js.review.unknown.reviewable_known_source",
reviewableType: reviewable_type,
pluginName: plugin_name,
),
)
end
def has_listing_for_unknown_reviewables_unknown_source?(reviewable_type)
page.has_css?(
".unknown-reviewables ul li",
text:
I18n.t("js.review.unknown.reviewable_unknown_source", reviewableType: reviewable_type),
)
end
def click_claim_reviewable
find(".reviewable-claimed-topic .claim").click
end
def click_unclaim_reviewable
find(".reviewable-claimed-topic .unclaim").click
end
def has_context_question?(reviewable, text)
within(reviewable_by_id(reviewable.id)) do
page.has_css?(".review-item__aside-title", text: text)
end
end
def has_no_context_question?(reviewable, text)
within(reviewable_by_id(reviewable.id)) do
page.has_no_css?(".review-item__aside-title", text: text)
end
end
def flag_reason_component
PageObjects::Components::Review::FlagReason.new
end
def topic_link_component
PageObjects::Components::Review::TopicLink.new
end
def has_history_items?(count:)
page.has_css?(".timeline-event", count: count)
end
def has_claimed_history_item?(user)
page.has_css?(".timeline-event__icon .d-icon-user-plus") && page.has_text?("Claimed by")
end
def has_unclaimed_history_item?(user)
page.has_css?(".timeline-event__icon .d-icon-user-xmark") && page.has_text?("Unclaimed by")
end
def has_created_at_history_item?
page.has_css?(".timeline-event__icon .d-icon-pen-to-square") &&
page.has_text?("Post created by")
end
def click_timeline_tab
find(".action-list li.timeline").click
end
def click_insights_tab
find(".action-list li.insights").click
end
def has_reviewable_with_status?(reviewable, status)
within(reviewable_by_id(reviewable.id)) do
page.has_css?(".review-item__status.--#{status}")
end
end
def has_reviewable_with_approved_status?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_css?(".review-item__status.--approved") }
end
def has_reviewable_with_rejected_status?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_css?(".review-item__status.--rejected") }
end
def has_rejected_item_in_timeline?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_text?("Rejected by") }
end
def has_reviewable_with_pending_status?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_css?(".review-item__status.--pending") }
end
def has_reviewable_with_ignored_status?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_css?(".review-item__status.--ignored") }
end
def has_approved_item_in_timeline?(reviewable)
within(reviewable_by_id(reviewable.id)) { page.has_text?("Approved by") }
end
def has_reviewables?(reviewables)
reviewable_ids = reviewables.map(&:id)
page.has_css?(".review-item", count: reviewables.size) &&
reviewable_ids.all? { |id| page.has_css?(".review-item[data-reviewable-id='#{id}']") }
end
def click_approve_user_button
find(".user-approve-user").click
end
def fill_post_content(content)
find(".d-editor-input").fill_in(with: content)
end
def save_post_edit
find(".reviewable-action.save-edit").click
end
def has_ip_lookup_info?
page.has_css?(".reviewable-ip-lookup")
end
def has_no_ip_lookup_info?
page.has_no_css?(".reviewable-ip-lookup")
end
def has_ip_location?(location)
page.has_text?(location)
end
def has_ip_hostname?(hostname)
page.has_text?(hostname)
end
def has_ip_lookup_modal?
page.has_css?(".ip-lookup-other-accounts-modal")
end
def has_account_in_modal?(username)
within(".ip-lookup-other-accounts-modal") { page.has_text?(username) }
end
def has_other_accounts_link?(count:)
page.has_button?(I18n.t("js.ip_lookup.other_accounts_with_ip", count: count))
end
def click_other_accounts_link
find(".ip-lookup-other-accounts-link").click
end
def click_edit_post_button
find(".reviewable-action.edit").click
end
private
def reviewable_by_id(id)
find(".review-item[data-reviewable-id=\"#{id}\"]")
end
def reviewable_action_dropdown
@reviewable_action_dropdown ||=
PageObjects::Components::SelectKit.new(REVIEWABLE_ACTION_DROPDOWN)
end
end
end
end