diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2017-01-30 16:53:18 -0600 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2017-03-07 23:10:32 -0600 |
commit | f44fb5cfd0cc4baada4d88f9724c74fc44326637 (patch) | |
tree | e2ff8e5e9b0d14b87aad353abffdc9b7979d6a29 /spec | |
parent | b5cb1115f4e3357118465ea4becf031b4ea598a6 (diff) | |
download | gitlab-ce-f44fb5cfd0cc4baada4d88f9724c74fc44326637.tar.gz |
Add filtered search visual tokensfiltered-search-visual-tokens
Diffstat (limited to 'spec')
20 files changed, 1677 insertions, 291 deletions
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index ede6aa0c201..4dcc56a97d1 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -1,6 +1,9 @@ require 'rails_helper' describe 'Dropdown assignee', :feature, :js do + include FilteredSearchHelpers + include WaitForAjax + let!(:project) { create(:empty_project) } let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } @@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do click_assignee(user_jacob.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ") + expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }]) + expect_filtered_search_input_empty end it 'fills in the assignee username when the assignee has been filtered' do @@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do click_assignee(user.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user.username} ") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:none ") + expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 59e302f0e2d..19a00618b12 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown author', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do click_author(user_jacob.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user_jacob.username} ") + expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }]) + expect_filtered_search_input_empty end it 'fills in the author username when the author has been filtered' do click_author(user.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user.username} ") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 04dd54ab459..01b657bcada 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown hint', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end it 'opens the assignee dropdown when you click on assignee' do @@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) - expect(filtered_search.value).to eq('assignee:') + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty end it 'opens the milestone dropdown when you click on milestone' do @@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) - expect(filtered_search.value).to eq('milestone:') + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty end it 'opens the label dropdown when you click on label' do @@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) - expect(filtered_search.value).to eq('label:') + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty end end @@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end it 'opens the assignee dropdown when you click on assignee' do @@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) - expect(filtered_search.value).to eq('assignee:') + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty end it 'opens the milestone dropdown when you click on milestone' do @@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) - expect(filtered_search.value).to eq('milestone:') + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty end it 'opens the label dropdown when you click on label' do @@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) - expect(filtered_search.value).to eq('label:') + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty + end + end + + describe 'reselecting from dropdown' do + it 'reuses existing author text' do + filtered_search.send_keys('author:') + filtered_search.send_keys(:backspace) + click_hint('author') + + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing assignee text' do + filtered_search.send_keys('assignee:') + filtered_search.send_keys(:backspace) + click_hint('assignee') + + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing milestone text' do + filtered_search.send_keys('milestone:') + filtered_search.send_keys(:backspace) + click_hint('milestone') + + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing label text' do + filtered_search.send_keys('label:') + filtered_search.send_keys(:backspace) + click_hint('label') + + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty end end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index ab3b868fd3a..b192064b693 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do filtered_search.native.send_keys(:down, :down, :enter) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end end @@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do end it 'filters by case-insensitive name with or without symbol' do - search_for_label('b') + filtered_search.send_keys('b') expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible @@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do clear_search_field init_label_search - search_for_label('~bu') + filtered_search.send_keys('~bu') expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible @@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name when the label is partially filled' do @@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") + expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") + expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") + expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }]) + expect_filtered_search_input_empty end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") + expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name with special characters' do click_label(special_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{special_label.title} ") + expect_tokens([{ name: 'label', value: "~#{special_label.title}" }]) + expect_filtered_search_input_empty end it 'selects `no label`' do find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:none ") + expect_tokens([{ name: 'label', value: 'none' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 0ce16715b86..0324fcad0a0 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown milestone', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name when the milestone is partially filled' do @@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ") + expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ") + expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ") + expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }]) + expect_filtered_search_input_empty end it 'selects `no milestone`' do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:none ") + expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_filtered_search_input_empty end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:upcoming ") + expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 0420e64d42c..35bd37933bc 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' describe 'Filter issues', js: true, feature: true do include FilteredSearchHelpers @@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") + expect_tokens([{ name: 'author', value: user.username }]) expect_issues_list_count(5) + expect_filtered_search_input_empty end it 'filters issues by invalid author' do @@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do end context 'author with other filters' do + search_term = 'issue' + it 'filters issues by searched author and text' do - search = "author:@#{user.username} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} #{search_term}") + expect_tokens([{ name: 'author', value: user.username }]) expect_issues_list_count(3) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee and text' do - search = "author:@#{user.username} assignee:@#{user.username} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(3) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee, label, and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee, label, milestone and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do describe 'filter issues by assignee' do context 'only assignee' do it 'filters issues by searched assignee' do - search = "assignee:@#{user.username}" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: user.username }]) expect_issues_list_count(5) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by no assignee' do - search = "assignee:none" - input_filtered_search(search) + input_filtered_search('assignee:none') + expect_tokens([{ name: 'assignee', value: 'none' }]) expect_issues_list_count(8, 1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by invalid assignee' do @@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do end context 'assignee with other filters' do + let(:search_term) { 'searchTerm' } + it 'filters issues by searched assignee and text' do - search = "assignee:@#{user.username} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} #{search_term}") + expect_tokens([{ name: 'assignee', value: user.username }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author and text' do - search = "assignee:@#{user.username} author:@#{user.username} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author, label, text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author, label, milestone and text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do end describe 'filter issues by label' do + let(:search_term) { 'bug' } + context 'only label' do it 'filters issues by searched label' do - search = "label:~#{bug_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title}") + expect_tokens([{ name: 'label', value: bug_label.title }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by no label' do - search = "label:none" - input_filtered_search(search) + input_filtered_search('label:none') + expect_tokens([{ name: 'label', value: 'none' }]) expect_issues_list_count(9, 1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by invalid label' do @@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by multiple labels' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by label containing special characters' do @@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - search = "label:~#{special_label.title}" - input_filtered_search(search) - + input_filtered_search("label:~#{special_label.title}") + expect_tokens([{ name: 'label', value: special_label.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'does not show issues' do - new_label = create(:label, project: project, title: "new_label") + new_label = create(:label, project: project, title: 'new_label') - search = "label:~#{new_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{new_label.title}") + expect_tokens([{ name: 'label', value: new_label.title }]) expect_no_issues_list() - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end @@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - search = "label:~'#{special_multiple_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{special_multiple_label.title}'") + # filtered search defaults quotations to double quotes + expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }]) expect_issues_list_count(1) - # filtered search defaults quotations to double quotes - expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") + expect_filtered_search_input_empty end it 'single quotes' do - search = "label:~'#{multiple_words_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{multiple_words_label.title}'") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") + expect_filtered_search_input_empty end it 'double quotes' do - search = "label:~\"#{multiple_words_label.title}\"" - input_filtered_search(search) + input_filtered_search("label:~\"#{multiple_words_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'single quotes containing double quotes' do @@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) double_quotes_label_issue.labels << double_quotes_label - search = "label:~'#{double_quotes_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{double_quotes_label.title}'") + expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'double quotes containing single quotes' do @@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - search = "label:~\"#{single_quotes_label.title}\"" - input_filtered_search(search) + input_filtered_search("label:~\"#{single_quotes_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end context 'label with other filters' do it 'filters issues by searched label and text' do - search = "label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([{ name: 'label', value: caps_sensitive_label.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author, assignee and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author, assignee, milestone and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end context 'multiple labels with other filters' do it 'filters issues by searched label, label2, and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author, assignee and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end context 'issue label clicked' do before do find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click - sleep 1 end it 'filters' do @@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do end it 'displays in search bar' do - expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_filtered_search_input_empty end end @@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") + expect_tokens([{ name: 'milestone', value: milestone.title }]) expect_issues_list_count(5) + expect_filtered_search_input_empty end it 'filters issues by no milestone' do input_filtered_search("milestone:none") + expect_tokens([{ name: 'milestone', value: 'none' }]) expect_issues_list_count(7, 1) + expect_filtered_search_input_empty end it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") + expect_tokens([{ name: 'milestone', value: 'upcoming' }]) expect_issues_list_count(1) + expect_filtered_search_input_empty end it 'filters issues by invalid milestones' do @@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - search = "milestone:%#{special_milestone.title}" - input_filtered_search(search) + input_filtered_search("milestone:%#{special_milestone.title}") + expect_tokens([{ name: 'milestone', value: special_milestone.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - search = "milestone:%#{new_milestone.title}" - input_filtered_search(search) + input_filtered_search("milestone:%#{new_milestone.title}") + expect_tokens([{ name: 'milestone', value: new_milestone.title }]) expect_no_issues_list() - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end context 'milestone with other filters' do + search_term = 'bug' + it 'filters issues by searched milestone and text' do - search = "milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} #{search_term}") + expect_tokens([{ name: 'milestone', value: milestone.title }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author, assignee and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author, assignee, label and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" - input_filtered_search(search) - + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}") + + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: bug_label.title } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do end end - describe 'overwrites selected filter' do - it 'changes author' do - input_filtered_search("author:@#{user.username}", submit: false) - - select_search_at_index(3) - - page.within '#js-dropdown-author' do - click_button user2.username - end - - expect(filtered_search.value).to eq("author:@#{user2.username} ") - end - - it 'changes label' do - input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false) - - select_search_at_index(27) - - page.within '#js-dropdown-label' do - click_button label.name - end - - expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ") - end - - it 'changes label correctly space is in previous label' do - input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false) - - select_search_at_index(0) - - page.within '#js-dropdown-label' do - click_button label.name - end - - expect(filtered_search.value).to eq("label:~#{label.name} ") - end - end - describe 'filter issues by text' do context 'only text' do it 'filters issues by searched text' do @@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do context 'searched text with other filters' do it 'filters issues by searched text and author' do + # After searching, all search terms are placed at the end input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} bug report") + expect_filtered_search_input('bug report') end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") + expect_filtered_search_input('bug report') end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") + expect_filtered_search_input('bug report with') end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") + expect_filtered_search_input('bug report with everything') end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") + expect_filtered_search_input('bug report with everything you') end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") + expect_filtered_search_input('bug report with everything you thought') end end @@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do before do input_filtered_search('bug') - # Wait for search results to load - sleep 2 + # This ensures that the search is performed + expect_issues_list_count(4, 1) end it 'open state' do diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 90eb60eb337..59244d65eec 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Search bar', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do it 'selects item' do filtered_search.native.send_keys(:down, :down, :enter) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb new file mode 100644 index 00000000000..b62a6d7913d --- /dev/null +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -0,0 +1,306 @@ +require 'rails_helper' + +describe 'Visual tokens', js: true, feature: true do + include FilteredSearchHelpers + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') } + let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) } + let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) } + let!(:label) { create(:label, project: project, title: 'abc') } + let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') } + + let(:filtered_search) { find('.filtered-search') } + let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") } + let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") } + let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") } + let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") } + + def is_input_focused + page.evaluate_script("document.activeElement.classList.contains('filtered-search')") + end + + before do + project.add_user(user, :master) + project.add_user(user_rock, :master) + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'editing author token' do + before do + input_filtered_search('author:@root assignee:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + end + + it 'opens author dropdown' do + expect(page).to have_css('#js-dropdown-author', visible: true) + end + + it 'makes value editable' do + expect_filtered_search_input('@root') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-author', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-author', visible: false) + end + + describe 'selecting different author from dropdown' do + before do + filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click + end + + it 'changes value in visual token' do + expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}") + end + + it 'moves input to the right' do + expect(is_input_focused).to eq(true) + end + end + end + + describe 'editing assignee token' do + before do + input_filtered_search('assignee:@root author:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + end + + it 'opens assignee dropdown' do + expect(page).to have_css('#js-dropdown-assignee', visible: true) + end + + it 'makes value editable' do + expect_filtered_search_input('@root') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-assignee', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-assignee', visible: false) + end + + describe 'selecting static option from dropdown' do + before do + find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click + end + + it 'changes value in visual token' do + expect(first('.tokens-container .filtered-search-token .value').text).to eq('none') + end + + it 'moves input to the right' do + expect(is_input_focused).to eq(true) + end + end + end + + describe 'editing milestone token' do + before do + input_filtered_search('milestone:%10.0 author:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item') + end + + it 'opens milestone dropdown' do + expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible + expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible + expect(page).to have_css('#js-dropdown-milestone', visible: true) + end + + it 'selects static option from dropdown' do + find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click + + expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming') + expect(is_input_focused).to eq(true) + end + + it 'makes value editable' do + expect_filtered_search_input('%10.0') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-milestone', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-milestone', visible: false) + end + end + + describe 'editing label token' do + before do + input_filtered_search("label:~#{label.title} author:none", submit: false) + first('.tokens-container .filtered-search-token').double_click + first('#js-dropdown-label .filter-dropdown .filter-dropdown-item') + end + + it 'opens label dropdown' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + expect(page).to have_css('#js-dropdown-label', visible: true) + end + + it 'selects option from dropdown' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + + find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click + + expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"") + expect(is_input_focused).to eq(true) + end + + it 'makes value editable' do + expect_filtered_search_input("~#{label.title}") + end + + it 'filters value' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + + filtered_search.send_keys(:backspace) + + filter_label_dropdown.find('.filter-dropdown-item') + + expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-label', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-label', visible: false) + end + end + + describe 'add new token after editing existing token' do + before do + input_filtered_search('author:@root assignee:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + filtered_search.send_keys(' ') + end + + describe 'opens dropdowns' do + it 'opens hint dropdown' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'opens author dropdown' do + filtered_search.send_keys('author:') + expect(page).to have_css('#js-dropdown-author', visible: true) + end + + it 'opens assignee dropdown' do + filtered_search.send_keys('assignee:') + expect(page).to have_css('#js-dropdown-assignee', visible: true) + end + + it 'opens milestone dropdown' do + filtered_search.send_keys('milestone:') + expect(page).to have_css('#js-dropdown-milestone', visible: true) + end + + it 'opens label dropdown' do + filtered_search.send_keys('label:') + expect(page).to have_css('#js-dropdown-label', visible: true) + end + end + + describe 'creates visual tokens' do + it 'creates author token' do + filtered_search.send_keys('author:@thomas ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Author') + expect(token.find('.value').text).to eq('@thomas') + end + + it 'creates assignee token' do + filtered_search.send_keys('assignee:@thomas ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Assignee') + expect(token.find('.value').text).to eq('@thomas') + end + + it 'creates milestone token' do + filtered_search.send_keys('milestone:none ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Milestone') + expect(token.find('.value').text).to eq('none') + end + + it 'creates label token' do + filtered_search.send_keys('label:~Backend ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Label') + expect(token.find('.value').text).to eq('~Backend') + end + end + + it 'does not tokenize incomplete token' do + filtered_search.send_keys('author:') + + find('#content-body').click + token = page.all('.tokens-container .js-visual-token')[1] + + expect_filtered_search_input_empty + expect(token.find('.name').text).to eq('Author') + end + end +end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 55f3c1863ff..5ba9743e926 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do context 'filter by label enhancement and bug in issues list' do before do - input_filtered_search('label:~bug label:~enhancement') + input_filtered_search('label:~bug label:~enhancement ') end it 'applies the filters' do diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 5608cda28f8..265a0cfc198 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) input_filtered_search('milestone:none') + expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_filtered_search_input_empty + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 6579a88d4ab..70e3997e716 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do describe 'for assignee from mr#index' do let(:search_query) { "assignee:@#{user.username}" } + def expect_assignee_visual_tokens + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty + end + before do input_filtered_search(search_query) @@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do context 'assignee', js: true do it 'updates to current user' do - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end end end describe 'for milestone from mr#index' do - let(:search_query) { "milestone:%#{milestone.title}" } + let(:search_query) { "milestone:%\"#{milestone.title}\"" } + + def expect_milestone_visual_tokens + expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }]) + expect_filtered_search_input_empty + end before do input_filtered_search(search_query) @@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do context 'milestone', js: true do it 'updates to current milestone' do - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end end end @@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do input_filtered_search('label:none') expect_mr_list_count(1) - expect_filtered_search_input('label:none') + expect_tokens([{ name: 'label', value: 'none' }]) + expect_filtered_search_input_empty end it 'filters by a label' do input_filtered_search("label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~#{label.title}") + expect_tokens([{ name: 'label', value: "~#{label.title}" }]) + expect_filtered_search_input_empty end it "filters by `won't fix` and another label" do input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + expect_tokens([ + { name: 'label', value: "~\"#{wontfix.title}\"" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty end it "filters by `won't fix` label followed by another label after page load" do input_filtered_search("label:~\"#{wontfix.title}\"") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\"") - - input_filtered_search_keys(" label:~#{label.title}") + expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }]) + expect_filtered_search_input_empty - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + expect_tokens([ + { name: 'label', value: "~\"#{wontfix.title}\"" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty end end @@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do input_filtered_search("assignee:@#{user.username}") expect_mr_list_count(1) - expect_filtered_search_input("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty - input_filtered_search_keys(" label:~#{label.title}") + input_filtered_search_keys("label:~#{label.title} ") expect_mr_list_count(1) @@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do end context 'assignee and label', js: true do + def expect_assignee_label_visual_tokens + expect_tokens([ + { name: 'assignee', value: "@#{user.username}" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty + end + it 'updates to current assignee and label' do - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end end end @@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(' label:~bug') expect_mr_list_count(1) + expect_tokens([{ name: 'label', value: '~bug' }]) + expect_filtered_search_input('Bug') end it 'filters by text and milestone' do @@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(' milestone:%8') expect_mr_list_count(1) + expect_tokens([{ name: 'milestone', value: '%8' }]) + expect_filtered_search_input('Bug') end it 'filters by text and assignee' do @@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(" assignee:@#{user.username}") expect_mr_list_count(1) + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input('Bug') end it 'filters by text and author' do @@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(" author:@#{user.username}") expect_mr_list_count(1) + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input('Bug') end end end @@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do it 'filter by current user' do visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id) - expect_filtered_search_input("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'filter by new user' do @@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id) - expect_filtered_search_input("assignee:@#{new_user.username}") + expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }]) + expect_filtered_search_input_empty end end @@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do it 'filter by current user' do visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id) - expect_filtered_search_input("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'filter by new user' do @@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id) - expect_filtered_search_input("author:@#{new_user.username}") + expect_tokens([{ name: 'author', value: "@#{new_user.username}" }]) + expect_filtered_search_input_empty end end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 58f11499e3f..6fed1568fcf 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Issues filter reset button', feature: true, js: true do +feature 'Merge requests filter clear button', feature: true, js: true do include FilteredSearchHelpers include MergeRequestHelpers include WaitForAjax @@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do context 'when a milestone filter has been applied' do it 'resets the milestone filter' do visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when a label filter has been applied' do it 'resets the label filter' do visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when a text search has been conducted' do it 'resets the text search filter' do visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when author filter has been applied' do it 'resets the author filter' do visit_merge_requests(project, author_username: user.username) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when assignee filter has been applied' do it 'resets the assignee filter' do visit_merge_requests(project, assignee_username: user.username) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when all filters have been applied' do - it 'resets all filters' do + it 'clears all filters' do visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when no filters have been applied' do - it 'the reset link should not be visible' do + it 'the clear button should not be visible' do visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) expect(page).not_to have_css(clear_search_css) end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 7da05defa81..a6560a81096 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe "Search", feature: true do + include FilteredSearchHelpers include WaitForAjax let(:user) { create(:user) } @@ -170,7 +171,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her issues page when issues authored is clicked' do @@ -178,7 +180,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her MR page when MR assigned is clicked' do @@ -186,7 +189,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her MR page when MR authored is clicked' do @@ -194,7 +198,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.filtered-search').value).to eq("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end end diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index fa9d03c8a9a..c16f77c53a2 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user'); it('should not return the double quote found in value', () => { spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: { - value: '"johnny appleseed', - }, + lastToken: '"johnny appleseed', }); expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); @@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user'); it('should not return the single quote found in value', () => { spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: { - value: '\'larry boy', - }, + lastToken: '\'larry boy', }); expect(dropdownUser.getSearchInput()).toBe('larry boy'); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 1e2d7582d5b..5c65903701b 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager'); }); it('should filter without symbol', () => { - input.value = ':roo'; + input.value = 'roo'; const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); @@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager'); expect(updatedItem.droplab_hidden).toBe(false); }); - it('should filter with colon', () => { - input.value = 'roo'; - - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); - describe('filters multiple word title', () => { const multipleWordItem = { title: 'Community Contributions', }; it('should filter with double quote', () => { - input.value = 'label:"'; + input.value = '"'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and symbol', () => { - input.value = 'label:~"'; + input.value = '~"'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and multiple words', () => { - input.value = 'label:"community con'; + input.value = '"community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote, symbol and multiple words', () => { - input.value = 'label:~"community con'; + input.value = '~"community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote', () => { - input.value = 'label:\''; + input.value = '\''; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and symbol', () => { - input.value = 'label:~\''; + input.value = '~\''; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and multiple words', () => { - input.value = 'label:\'community con'; + input.value = '\'community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote, symbol and multiple words', () => { - input.value = 'label:~\'community con'; + input.value = '~\'community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index ed0b0196ec4..a1da3396d7b 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,4 +1,5 @@ require('~/extensions/array'); +require('~/filtered_search/filtered_search_visual_tokens'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); @@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager'); } beforeEach(() => { - const input = document.createElement('input'); - input.classList.add('filtered-search'); - document.body.appendChild(input); - }); - - afterEach(() => { - document.querySelector('.filtered-search').outerHTML = ''; + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search"> + </li> + </ul> + `); }); describe('input has no existing value', () => { it('should add just tokenName', () => { gl.FilteredSearchDropdownManager.addWordToInput('milestone'); - expect(getInputValue()).toBe('milestone:'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('milestone'); + expect(getInputValue()).toBe(''); }); it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); + + let token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(getInputValue()).toBe(''); + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - expect(getInputValue()).toBe('label:none '); + // We have to get that reference again + // Because gl.FilteredSearchDropdownManager deletes the previous token + token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('none'); + expect(getInputValue()).toBe(''); }); }); @@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager'); it('should be able to just add tokenName', () => { setInputValue('a'); gl.FilteredSearchDropdownManager.addWordToInput('author'); - expect(getInputValue()).toBe('author:'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(getInputValue()).toBe(''); }); it('should replace tokenValue', () => { - setInputValue('author:roo'); - gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); - expect(getInputValue()).toBe('author:@root '); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + + setInputValue('roo'); + gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(token.querySelector('.value').innerText).toBe('@root'); + expect(getInputValue()).toBe(''); }); it('should add tokenValues containing spaces', () => { - setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label'); + + setInputValue('"test '); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - expect(getInputValue()).toBe('label:~\'"test me"\' '); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); + expect(getInputValue()).toBe(''); }); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 98959dda242..81c1d81d181 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_manager'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); (() => { describe('Filtered Search Manager', () => { - describe('search', () => { - let manager; - const defaultParams = '?scope=all&utf8=✓&state=opened'; + let input; + let manager; + let tokensContainer; + const placeholder = 'Search or filter results...'; - function getInput() { - return document.querySelector('.filtered-search'); - } + function dispatchBackspaceEvent(element, eventType) { + const backspaceKey = 8; + const event = new Event(eventType); + event.keyCode = backspaceKey; + element.dispatchEvent(event); + } - beforeEach(() => { - setFixtures(` - <input type='text' class='filtered-search' /> - `); + function dispatchDeleteEvent(element, eventType) { + const deleteKey = 46; + const event = new Event(eventType); + event.keyCode = deleteKey; + element.dispatchEvent(event); + } - spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); - spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + beforeEach(() => { + setFixtures(` + <div class="filtered-search-input-container"> + <form> + <ul class="tokens-container list-unstyled"> + ${FilteredSearchSpecHelper.createInputHTML(placeholder)} + </ul> + <button class="clear-search" type="button"> + <i class="fa fa-times"></i> + </button> + </form> + </div> + `); - manager = new gl.FilteredSearchManager(); - }); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); - afterEach(() => { - getInput().outerHTML = ''; - }); + input = document.querySelector('.filtered-search'); + tokensContainer = document.querySelector('.tokens-container'); + manager = new gl.FilteredSearchManager(); + }); - it('should search with a single word', () => { - getInput().value = 'searchTerm'; + describe('search', () => { + const defaultParams = '?scope=all&utf8=✓&state=opened'; + + it('should search with a single word', (done) => { + input.value = 'searchTerm'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); + done(); }); manager.search(); }); - it('should search with multiple words', () => { - getInput().value = 'awesome search terms'; + it('should search with multiple words', (done) => { + input.value = 'awesome search terms'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); + done(); }); manager.search(); }); - it('should search with special characters', () => { - getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; + it('should search with special characters', (done) => { + input.value = '~!@#$%^&*()_+{}:<>,.?/'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); + done(); }); manager.search(); }); }); + + describe('handleInputPlaceholder', () => { + it('should render placeholder when there is no input', () => { + expect(input.placeholder).toEqual(placeholder); + }); + + it('should not render placeholder when there is input', () => { + input.value = 'test words'; + + const event = new Event('input'); + input.dispatchEvent(event); + + expect(input.placeholder).toEqual(''); + }); + + it('should not render placeholder when there are tokens and no input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + + const event = new Event('input'); + input.dispatchEvent(event); + + expect(input.placeholder).toEqual(''); + }); + }); + + describe('checkForBackspace', () => { + describe('tokens and no input', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + }); + + it('removes last token', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + dispatchBackspaceEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + }); + + it('sets the input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(input.value).toEqual('~bug'); + }); + }); + + it('does not remove token or change input when there is existing input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + input.value = 'text'; + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('text'); + }); + }); + + describe('removeSelectedToken', () => { + function getVisualTokens() { + return tokensContainer.querySelectorAll('.js-visual-token'); + } + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + }); + + it('removes selected token when the backspace key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(getVisualTokens().length).toEqual(0); + }); + + it('removes selected token when the delete key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); + + dispatchDeleteEvent(document, 'keydown'); + + expect(getVisualTokens().length).toEqual(0); + }); + + it('updates the input placeholder after removal', () => { + manager.handleInputPlaceholder(); + + expect(input.placeholder).toEqual(''); + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(input.placeholder).not.toEqual(''); + expect(getVisualTokens().length).toEqual(0); + }); + + it('updates the clear button after removal', () => { + manager.toggleClearSearchButton(); + + const clearButton = document.querySelector('.clear-search'); + + expect(clearButton.classList.contains('hidden')).toEqual(false); + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(clearButton.classList.contains('hidden')).toEqual(true); + expect(getVisualTokens().length).toEqual(0); + }); + }); + + describe('unselects token', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); + + it('unselects token when input is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + + // Click directly on input attached to document + // so that the click event will propagate properly + document.querySelector('.filtered-search').click(); + + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); + expect(selectedToken.classList.contains('selected')).toEqual(false); + }); + + it('unselects token when document.body is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + + document.body.click(); + + expect(selectedToken.classList.contains('selected')).toEqual(false); + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); + }); + }); }); })(); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js new file mode 100644 index 00000000000..3c0e894ea31 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -0,0 +1,587 @@ +require('~/filtered_search/filtered_search_visual_tokens'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); + +describe('Filtered Search Visual Tokens', () => { + let tokensContainer; + + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + ${FilteredSearchSpecHelper.createInputHTML()} + </ul> + `); + tokensContainer = document.querySelector('.tokens-container'); + }); + + describe('getLastVisualTokenBeforeInput', () => { + it('returns when there are no visual tokens', () => { + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(null); + expect(isLastVisualTokenValid).toEqual(true); + }); + + describe('input is the last item in tokensContainer', () => { + it('returns when there is one visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns when there is an incomplete visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'), + ); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(false); + }); + + it('returns when there are multiple visual tokens', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const items = document.querySelectorAll('.tokens-container .js-visual-token'); + + expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns when there are multiple visual tokens and an incomplete visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const items = document.querySelectorAll('.tokens-container .js-visual-token'); + + expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); + expect(isLastVisualTokenValid).toEqual(false); + }); + }); + + describe('input is a middle item in tokensContainer', () => { + it('returns last token before input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns last partial token before input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(false); + }); + }); + }); + + describe('unselectTokens', () => { + it('does nothing when there are no tokens', () => { + const beforeHTML = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.unselectTokens(); + + expect(tokensContainer.innerHTML).toEqual(beforeHTML); + }); + + it('removes the selected class from buttons', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)} + `); + + const selected = tokensContainer.querySelector('.js-visual-token .selected'); + expect(selected.classList.contains('selected')).toEqual(true); + + gl.FilteredSearchVisualTokens.unselectTokens(); + + expect(selected.classList.contains('selected')).toEqual(false); + }); + }); + + describe('selectToken', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); + + it('removes the selected class if it has selected class', () => { + const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); + firstTokenButton.classList.add('selected'); + + gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + + expect(firstTokenButton.classList.contains('selected')).toEqual(false); + }); + + describe('has no selected class', () => { + it('adds selected class', () => { + const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); + + gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + + expect(firstTokenButton.classList.contains('selected')).toEqual(true); + }); + + it('removes selected class from other tokens', () => { + const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable'); + tokenButtons[1].classList.add('selected'); + + gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]); + + expect(tokenButtons[0].classList.contains('selected')).toEqual(true); + expect(tokenButtons[1].classList.contains('selected')).toEqual(false); + }); + }); + }); + + describe('removeSelectedToken', () => { + it('does not remove when there are no selected tokens', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeSelectedToken(); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + }); + + it('removes selected token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeSelectedToken(); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null); + }); + }); + + describe('createVisualTokenElementHTML', () => { + let tokenElement; + + beforeEach(() => { + setFixtures(` + <div class="test-area"> + ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()} + </div> + `); + + tokenElement = document.querySelector('.test-area').firstElementChild; + }); + + it('contains name div', () => { + expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); + }); + + it('contains value div', () => { + expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); + }); + + it('contains selectable class', () => { + expect(tokenElement.classList.contains('selectable')).toEqual(true); + }); + + it('contains button role', () => { + expect(tokenElement.getAttribute('role')).toEqual('button'); + }); + }); + + describe('addVisualTokenElement', () => { + it('renders search visual tokens', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-term')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('search term'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('renders filter visual token name', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('renders filter visual token name and value', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('label'); + expect(token.querySelector('.value').innerText).toEqual('Frontend'); + }); + + it('inserts visual token before input', () => { + tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root')); + + gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + const tokens = tokensContainer.querySelectorAll('.js-visual-token'); + const labelToken = tokens[0]; + const assigneeToken = tokens[1]; + + expect(labelToken.classList.contains('filtered-search-token')).toEqual(true); + expect(labelToken.querySelector('.name').innerText).toEqual('label'); + expect(labelToken.querySelector('.value').innerText).toEqual('Frontend'); + + expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true); + expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee'); + expect(assigneeToken.querySelector('.value').innerText).toEqual('@root'); + }); + }); + + describe('addValueToPreviousVisualTokenElement', () => { + it('does not add when previous visual token element has no value', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'), + ); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + + expect(original).toEqual(tokensContainer.innerHTML); + }); + + it('does not add when previous visual token element is a search', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + `); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + + expect(original).toEqual(tokensContainer.innerHTML); + }); + + it('adds value to previous visual filter token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'), + ); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + const updatedToken = tokensContainer.querySelector('.js-visual-token'); + + expect(updatedToken.querySelector('.name').innerText).toEqual('label'); + expect(updatedToken.querySelector('.value').innerText).toEqual('value'); + expect(original).not.toEqual(tokensContainer.innerHTML); + }); + }); + + describe('addFilterVisualToken', () => { + it('creates visual token with just tokenName', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('creates visual token with just tokenValue', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); + gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value').innerText).toEqual('%8.17'); + }); + + it('creates full visual token', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('assignee'); + expect(token.querySelector('.value').innerText).toEqual('@john'); + }); + }); + + describe('addSearchVisualToken', () => { + it('creates search visual token', () => { + gl.FilteredSearchVisualTokens.addSearchVisualToken('search term'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-term')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('search term'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('appends to previous search visual token if previous token was a search token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + `); + + gl.FilteredSearchVisualTokens.addSearchVisualToken('append this'); + const token = tokensContainer.querySelector('.filtered-search-term'); + + expect(token.querySelector('.name').innerText).toEqual('search term append this'); + expect(token.querySelector('.value')).toEqual(null); + }); + }); + + describe('getLastTokenPartial', () => { + it('should get last token value', () => { + const value = '~bug'; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value), + ); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value); + }); + + it('should get last token name if there is no value', () => { + const name = 'assignee'; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name), + ); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name); + }); + + it('should return empty when there are no tokens', () => { + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(''); + }); + }); + + describe('removeLastTokenPartial', () => { + it('should remove the last token value if it exists', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null); + }); + + it('should remove the last token name if there is no value', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null); + }); + + it('should not remove anything when there are no tokens', () => { + const html = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.innerHTML).toEqual(html); + }); + }); + + describe('tokenizeInput', () => { + it('does not do anything if there is no input', () => { + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + expect(tokensContainer.innerHTML).toEqual(original); + }); + + it('adds search visual token if previous visual token is valid', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'), + ); + + const input = document.querySelector('.filtered-search'); + input.value = 'some value'; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + const newToken = tokensContainer.querySelector('.filtered-search-term'); + + expect(input.value).toEqual(''); + expect(newToken.querySelector('.name').innerText).toEqual('some value'); + expect(newToken.querySelector('.value')).toEqual(null); + }); + + it('adds value to previous visual token element if previous visual token is invalid', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'), + ); + + const input = document.querySelector('.filtered-search'); + input.value = '@john'; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + const updatedToken = tokensContainer.querySelector('.filtered-search-token'); + + expect(input.value).toEqual(''); + expect(updatedToken.querySelector('.name').innerText).toEqual('assignee'); + expect(updatedToken.querySelector('.value').innerText).toEqual('@john'); + }); + }); + + describe('editToken', () => { + let input; + let token; + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')} + `); + + input = document.querySelector('.filtered-search'); + token = document.querySelector('.js-visual-token'); + }); + + it('tokenize\'s existing input', () => { + input.value = 'some text'; + spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough(); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); + expect(input.value).not.toEqual('some text'); + }); + + it('moves input to the token position', () => { + expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null); + expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null); + }); + + it('input contains the visual token value', () => { + gl.FilteredSearchVisualTokens.editToken(token); + + expect(input.value).toEqual('none'); + }); + + describe('selected token is a search term token', () => { + beforeEach(() => { + token = document.querySelector('.filtered-search-term'); + }); + + it('token is removed', () => { + expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null); + }); + + it('input has the same value as removed token', () => { + expect(input.value).toEqual(''); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(input.value).toEqual('search'); + }); + }); + }); + + describe('moveInputTotheRight', () => { + it('does nothing if the input is already the right most element', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'), + ); + + spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough(); + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + + expect(gl.FilteredSearchVisualTokens.tokenizeInput).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled(); + }); + + it('tokenize\'s input', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + document.querySelector('.filtered-search').value = 'none'; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + const value = tokensContainer.querySelector('.js-visual-token .value'); + + expect(value.innerText).toEqual('none'); + }); + + it('converts input into search term token if last token is valid', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + document.querySelector('.filtered-search').value = 'test'; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + const searchValue = tokensContainer.querySelector('.filtered-search-term .name'); + + expect(searchValue.innerText).toEqual('test'); + }); + + it('moves the input to the right most element', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + + expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js new file mode 100644 index 00000000000..c891518fce9 --- /dev/null +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -0,0 +1,52 @@ +class FilteredSearchSpecHelper { + static createFilterVisualTokenHTML(name, value, isSelected) { + return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML; + } + + static createFilterVisualToken(name, value, isSelected = false) { + const li = document.createElement('li'); + li.classList.add('js-visual-token', 'filtered-search-token'); + + li.innerHTML = ` + <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> + <div class="name">${name}</div> + <div class="value">${value}</div> + </div> + `; + + return li; + } + + static createNameFilterVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-token"> + <div class="name">${name}</div> + </li> + `; + } + + static createSearchVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-term"> + <div class="name">${name}</div> + </li> + `; + } + + static createInputHTML(placeholder = '') { + return ` + <li class="input-token"> + <input type='text' class='filtered-search' placeholder='${placeholder}' /> + </li> + `; + } + + static createTokensContainerHTML(html, inputPlaceholder) { + return ` + ${html} + ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)} + `; + } +} + +module.exports = FilteredSearchSpecHelper; diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 58f6636e680..f21b85ec10b 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -3,16 +3,20 @@ module FilteredSearchHelpers page.find('.filtered-search') end + # Enables input to be set (similar to copy and paste) def input_filtered_search(search_term, submit: true) - filtered_search.set(search_term) + # Add an extra space to engage visual tokens + filtered_search.set("#{search_term} ") if submit filtered_search.send_keys(:enter) end end + # Enables input to be added character by character def input_filtered_search_keys(search_term) - filtered_search.send_keys(search_term) + # Add an extra space to engage visual tokens + filtered_search.send_keys("#{search_term} ") filtered_search.send_keys(:enter) end @@ -34,4 +38,32 @@ module FilteredSearchHelpers # This ensures the dropdown is shown expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') end + + def expect_filtered_search_input_empty + expect(find('.filtered-search').value).to eq('') + end + + # Iterates through each visual token inside + # .tokens-container to make sure the correct names and values are rendered + def expect_tokens(tokens) + page.find '.filtered-search-input-container .tokens-container' do + page.all(:css, '.tokens-container li').each_with_index do |el, index| + token_name = tokens[index][:name] + token_value = tokens[index][:value] + + expect(el.find('.name')).to have_content(token_name) + if token_value + expect(el.find('.value')).to have_content(token_value) + end + end + end + end + + def default_placeholder + 'Search or filter results...' + end + + def get_filtered_search_placeholder + find('.filtered-search')['placeholder'] + end end |