2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-04 01:15:08 +08:00
discourse/spec/system/search_spec.rb
Natalie Tay 9e99066b07
DEV: Expand top_tags, topic.tags, etc, to return an array of tag objects instead of tag names (#36678)
Currently in several endpoints, we return an array of strings for tags.
Our goal with this PR is to expand array tag name strings to an array of
tag objects.

#### before: Tags were returned as string arrays
```
{ "tags": ["support", "bug-report"] }
```

#### after: Tags are returned as object arrays
```
{ "tags": [{"id": 12, "name": "support", "slug": "support"}, {"id": 13, "name": "bug-report", "slug": "bug-report"}] }
```

This allows us to start referencing tags by their ids, and return more
information for a tag for future features.

This commit involves updating several areas:
- topic lists (/latest.json, /top.json, /c/:category/:id.json, etc, for
`top_tags`)
- tag chooser components (`MiniTagChooser`, `TagDrop`, etc)
- topic view (/t/:id.json)
- tag groups (/tag_groups.json, tags, parent_tag)
- category settings
- staff action logs
- synonyms
- ...

APIs that reference tags based on their names will still be supported
with a deprecation warning. Moving on, we will reference them using
their tag ids.
2026-02-02 10:03:02 +08:00

342 lines
11 KiB
Ruby

# frozen_string_literal: true
describe "Search", type: :system do
let(:search_page) { PageObjects::Pages::Search.new }
fab!(:topic)
fab!(:op) { Fabricate(:post, topic: topic) }
fab!(:post) { Fabricate(:post, topic: topic, raw: "This is a test post in a test topic") }
fab!(:topic2) { Fabricate(:topic, title: "Another test topic") }
fab!(:post2) { Fabricate(:post, topic: topic2, raw: "This is another test post in a test topic") }
let(:topic_bulk_actions_modal) { PageObjects::Modals::TopicBulkActions.new }
describe "when using full page search on mobile" do
before do
SearchIndexer.enable
SearchIndexer.index(topic, force: true)
SearchIndexer.index(topic2, force: true)
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
end
after { SearchIndexer.disable }
it "handles search term cleaning and ordering for aliases" do
# we need to be logged in for last read to show up
sign_in(post.user)
TopicUser.update_last_read(post.user, post.topic.id, 1, 1, 0)
visit("/search?q=test%20r")
expect(search_page.search_input.value).to eq("test")
# read sort order is set to 5
expect(search_page.sort_order.value).to eq("5")
visit("/search?q=test%20l")
expect(search_page.search_input.value).to eq("test")
# latest sort order is set to 1
expect(search_page.sort_order.value).to eq("1")
end
it "works and clears search page state", mobile: true do
visit("/search")
search_page.type_in_search("test")
search_page.click_search_button
expect(search_page).to have_search_result
expect(search_page).to have_no_heading_text("Search")
click_logo
expect(page).to have_current_path("/")
expect(search_page).to be_not_active
page.go_back
# ensure results are still there when using browser's history
expect(search_page).to have_search_result
click_logo
expect(page).to have_current_path("/")
search_page.click_search_icon
expect(search_page).to have_no_search_result
expect(search_page).to have_heading_text("Search")
end
it "navigates search results using J/K keys" do
visit("/search")
search_page.type_in_search("test")
search_page.click_search_button
expect(search_page).to have_search_result
results = all(".fps-result")
page.send_keys("j")
expect(results.first["class"]).to include("selected")
page.send_keys("j")
expect(results.last["class"]).to include("selected")
page.send_keys("k")
expect(results.first["class"]).to include("selected")
end
end
describe "when using full page search on desktop" do
before do
SearchIndexer.enable
SearchIndexer.index(topic, force: true)
SiteSetting.rate_limit_search_anon_user_per_minute = 4
RateLimiter.enable
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
end
after { SearchIndexer.disable }
xit "rate limits searches for anonymous users" do
queries = %w[one two three four]
visit("/search?expanded=true")
queries.each do |query|
search_page.clear_search_input
search_page.type_in_search(query)
search_page.click_search_button
end
# Rate limit error should kick in after 4 queries
expect(search_page).to have_warning_message
end
end
describe "when search menu on desktop" do
before do
SearchIndexer.enable
SearchIndexer.index(topic, force: true)
SearchIndexer.index(topic2, force: true)
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
end
after { SearchIndexer.disable }
it "still displays last topic search results after navigating away, then back" do
visit("/")
search_page.click_search_icon
search_page.type_in_search_menu("test")
search_page.click_search_menu_link
expect(search_page).to have_topic_title_for_first_search_result(topic.title)
search_page.click_first_topic
search_page.click_search_icon
expect(search_page).to have_topic_title_for_first_search_result(topic.title)
end
it "tracks search result clicks" do
expect(SearchLog.count).to eq(0)
visit("/")
search_page.click_search_icon
search_page.type_in_search_menu("test")
search_page.click_search_menu_link
expect(search_page).to have_topic_title_for_first_search_result(topic.title)
find(".search-menu-container .search-result-topic", text: topic.title).click
expect(SearchLog.count).to eq(1)
expect(SearchLog.last.search_result_id).not_to eq(nil)
log = SearchLog.last
expect(log.term).to eq("test")
expect(log.search_result_id).to eq(topic.first_post.id)
expect(log.search_type).to eq(SearchLog.search_types[:header])
end
describe "with search icon in header" do
before do
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_icon")
end
it "displays the correct search mode" do
visit("/")
expect(search_page).to have_search_icon
expect(search_page).to have_no_search_field
end
end
describe "with search field in header" do
before do
Fabricate(
:theme_site_setting_with_service,
name: "search_experience",
value: "search_field",
)
end
it "displays the correct search mode" do
visit("/")
expect(search_page).to have_search_field
expect(search_page).to have_no_search_icon
end
it "switches to search icon when header is minimized" do
5.times { Fabricate(:post, topic: topic) }
visit("/t/#{topic.id}")
expect(search_page).to have_no_search_icon
find(".timeline-date-wrapper:last-child a").click
expect(search_page).to have_search_icon
find(".timeline-date-wrapper:first-child a").click
expect(search_page).to have_no_search_icon
end
it "does not display on login, search, signup or activate account pages" do
visit("/login")
expect(search_page).to have_no_search_icon
expect(search_page).to have_no_search_field
visit("/search")
expect(search_page).to have_no_search_icon
expect(search_page).to have_no_search_field
visit("/signup")
expect(search_page).to have_no_search_icon
expect(search_page).to have_no_search_field
email_token = Fabricate(:email_token, user: Fabricate(:user, active: false))
visit("/u/activate-account/#{email_token.token}")
expect(search_page).to have_no_search_icon
expect(search_page).to have_no_search_field
end
describe "with invites" do
fab!(:invite)
it "does not display search field" do
visit("/invites/#{invite.invite_key}")
expect(search_page).to have_no_search_icon
expect(search_page).to have_no_search_field
end
end
describe "when on admin pages" do
fab!(:admin)
it "displays search icon regardless of Search experience setting" do
sign_in(admin)
visit("/admin")
expect(search_page).to have_no_search_field
expect(search_page).to have_search_icon
end
end
end
end
describe "bulk actions" do
fab!(:admin)
fab!(:tag1, :tag)
before do
SearchIndexer.enable
SearchIndexer.index(topic, force: true)
SearchIndexer.index(post, force: true)
SearchIndexer.index(topic2, force: true)
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
sign_in(admin)
end
after { SearchIndexer.disable }
it "allows the user to perform bulk actions on the topic search results" do
visit("/search?q=test")
expect(page).to have_content(topic.title)
find(".search-info .bulk-select").click
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .bulk-select input").click
find(".search-info .bulk-select-topics-dropdown-trigger").click
find(".bulk-select-topics-dropdown-content .append-tags").click
expect(topic_bulk_actions_modal).to be_open
tag_selector = PageObjects::Components::SelectKit.new(".tag-chooser")
tag_selector.search(tag1.name)
tag_selector.select_row_by_name(tag1.name)
tag_selector.collapse
topic_bulk_actions_modal.click_bulk_topics_confirm
expect(
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .discourse-tags"),
).to have_content(tag1.name)
end
it "allows the user to delete posts in bulk" do
visit("/search?q=This%20is%20a%20test%20post")
expect(page).to have_content(post.raw)
find(".search-info .bulk-select").click
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .bulk-select input").click
find(".search-info .bulk-select-topics-dropdown-trigger").click
find(".bulk-select-topics-dropdown-content .delete-posts").click
find(".dialog-content")
click_button "OK"
expect(page).to have_no_content(post.raw)
expect(Post.with_deleted.find_by(id: post.id).deleted_at).to be_present
end
end
describe "Private Message Icon in Search Results" do
fab!(:user)
fab!(:other_user, :user)
fab!(:pm_topic) do
Fabricate(
:private_message_topic,
user: user,
recipient: other_user,
title: "PM about searchable things",
)
end
fab!(:pm_post) do
Fabricate(:post, topic: pm_topic, user: user, raw: "Secret PM content searchable")
end
fab!(:regular_topic) { Fabricate(:topic, title: "Regular topic about searchable things") }
fab!(:regular_post) do
Fabricate(:post, topic: regular_topic, raw: "Regular post content searchable")
end
before do
SearchIndexer.enable
SearchIndexer.index(pm_topic, force: true)
SearchIndexer.index(regular_topic, force: true)
sign_in(user)
end
after { SearchIndexer.disable }
it "handles different PM search filters correctly" do
pm_filters = %w[in:messages in:personal in:personal-direct in:all-pms]
pm_filters.each do |filter|
visit("/search?q=searchable%20#{filter}")
if page.has_css?(".fps-result", minimum: 1)
expect(page).to have_css(".fps-result .topic-status .d-icon-envelope", count: 0),
"Expected no PM icons for filter: #{filter}"
end
end
end
it "shows PM envelope icon in mixed search results with in:all filter" do
# Search with in:all filter to get mixed results (both PM and public topics)
visit("/search?q=searchable%20in:all")
# The PM envelope icon should be on the PM topic specifically
pm_result = page.find(".fps-result", text: "PM about searchable things")
expect(pm_result).to have_css(".topic-status .d-icon-envelope")
# The regular topic should NOT have the PM envelope icon
regular_result = page.find(".fps-result", text: "Regular topic about searchable things")
expect(regular_result).to have_no_css(".topic-status .d-icon-envelope")
end
end
end