Commit 9098031d authored by Zack Cuddy's avatar Zack Cuddy Committed by Bob Van Landuyt

Global Search - Left Sidebar

This creates a search facet sidebar
for issues and merge requests.

This also converts the
status and confidential
filter from a dropdown
to a radio button.
parent c67a1159
<script>
import { mapState } from 'vuex';
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
export default {
name: 'DropdownFilter',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
props: {
filterData: {
type: Object,
required: true,
},
},
computed: {
...mapState(['query']),
scope() {
return this.query.scope;
},
supportedScopes() {
return Object.values(this.filterData.scopes);
},
initialFilter() {
return this.query[this.filterData.filterParam];
},
filter() {
return this.initialFilter || this.filterData.filters.ANY.value;
},
filtersArray() {
return this.filterData.filterByScope[this.scope];
},
selectedFilter: {
get() {
if (this.filtersArray.some(({ value }) => value === this.filter)) {
return this.filter;
}
return this.filterData.filters.ANY.value;
},
set(filter) {
// we need to remove the pagination cursor to ensure the
// relevancy of the new results
visitUrl(
setUrlParams({
page: null,
[this.filterData.filterParam]: filter,
}),
);
},
},
selectedFilterText() {
const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
if (!f || f === this.filterData.filters.ANY) {
return sprintf(s__('Any %{header}'), { header: this.filterData.header });
}
return f.label;
},
showDropdown() {
return this.supportedScopes.includes(this.scope);
},
},
methods: {
dropDownItemClass(filter) {
return {
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
filter === this.filterData.filters.ANY,
};
},
isFilterSelected(filter) {
return filter === this.selectedFilter;
},
handleFilterChange(filter) {
this.selectedFilter = filter;
},
},
};
</script>
<template>
<gl-dropdown
v-if="showDropdown"
:text="selectedFilterText"
class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4"
menu-class="gl-w-full! gl-pl-0"
>
<header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ filterData.header }}
</header>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="f in filtersArray"
:key="f.value"
:is-check-item="true"
:is-checked="isFilterSelected(f.value)"
:class="dropDownItemClass(f)"
@click="handleFilterChange(f.value)"
>
{{ f.label }}
</gl-dropdown-item>
</gl-dropdown>
</template>
import { __ } from '~/locale';
const header = __('Confidentiality');
const filters = {
ANY: {
label: __('Any'),
value: null,
},
CONFIDENTIAL: {
label: __('Confidential'),
value: 'yes',
},
NOT_CONFIDENTIAL: {
label: __('Not confidential'),
value: 'no',
},
};
const scopes = {
ISSUES: 'issues',
};
const filterByScope = {
[scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL],
};
const filterParam = 'confidential';
export default {
header,
filters,
scopes,
filterByScope,
filterParam,
};
import { __ } from '~/locale';
const header = __('Status');
const filters = {
ANY: {
label: __('Any'),
value: 'all',
},
OPEN: {
label: __('Open'),
value: 'opened',
},
CLOSED: {
label: __('Closed'),
value: 'closed',
},
MERGED: {
label: __('Merged'),
value: 'merged',
},
};
const scopes = {
ISSUES: 'issues',
MERGE_REQUESTS: 'merge_requests',
};
const filterByScope = {
[scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED],
[scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED],
};
const filterParam = 'state';
export default {
header,
filters,
scopes,
filterByScope,
filterParam,
};
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import DropdownFilter from './components/dropdown_filter.vue';
import stateFilterData from './constants/state_filter_data';
import confidentialFilterData from './constants/confidential_filter_data';
Vue.use(Translate);
const mountDropdownFilter = (store, { id, filterData }) => {
const el = document.getElementById(id);
if (!el) return false;
return new Vue({
el,
store,
render(createElement) {
return createElement(DropdownFilter, {
props: {
filterData,
},
});
},
});
};
const dropdownFilters = [
{
id: 'js-search-filter-by-state',
filterData: stateFilterData,
},
{
id: 'js-search-filter-by-confidential',
filterData: confidentialFilterData,
},
];
export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter));
import { queryToObject } from '~/lib/utils/url_utility'; import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store'; import createStore from './store';
import initDropdownFilters from './dropdown_filter';
import { initSidebar } from './sidebar'; import { initSidebar } from './sidebar';
import initGroupFilter from './group_filter'; import initGroupFilter from './group_filter';
export default () => { export default () => {
const store = createStore({ query: queryToObject(window.location.search) }); const store = createStore({ query: queryToObject(window.location.search) });
if (gon.features.searchFacets) {
initSidebar(store); initSidebar(store);
} else {
initDropdownFilters(store);
}
initGroupFilter(store); initGroupFilter(store);
}; };
<script>
import { mapActions, mapState } from 'vuex';
import { GlButton, GlLink } from '@gitlab/ui';
import StatusFilter from './status_filter.vue';
import ConfidentialityFilter from './confidentiality_filter.vue';
export default {
name: 'GlobalSearchSidebar',
components: {
GlButton,
GlLink,
StatusFilter,
ConfidentialityFilter,
},
computed: {
...mapState(['query']),
showReset() {
return this.query.state || this.query.confidential;
},
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
},
};
</script>
<template>
<form
class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mb gl-mt-5"
@submit.prevent="applyQuery"
>
<status-filter />
<confidentiality-filter />
<div class="gl-display-flex gl-align-items-center gl-mt-3">
<gl-button variant="success" type="submit">{{ __('Apply') }}</gl-button>
<gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
__('Reset filters')
}}</gl-link>
</div>
</form>
</template>
...@@ -21,5 +21,6 @@ export default { ...@@ -21,5 +21,6 @@ export default {
<template> <template>
<div v-if="showDropdown"> <div v-if="showDropdown">
<radio-filter :filter-data="$options.confidentialFilterData" /> <radio-filter :filter-data="$options.confidentialFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div> </div>
</template> </template>
...@@ -21,5 +21,6 @@ export default { ...@@ -21,5 +21,6 @@ export default {
<template> <template>
<div v-if="showDropdown"> <div v-if="showDropdown">
<radio-filter :filter-data="$options.stateFilterData" /> <radio-filter :filter-data="$options.stateFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import StatusFilter from './components/status_filter.vue'; import GlobalSearchSidebar from './components/app.vue';
import ConfidentialityFilter from './components/confidentiality_filter.vue';
Vue.use(Translate); Vue.use(Translate);
const mountRadioFilters = (store, { id, component }) => { export const initSidebar = store => {
const el = document.getElementById(id); const el = document.getElementById('js-search-sidebar');
if (!el) return false; if (!el) return false;
...@@ -14,21 +13,7 @@ const mountRadioFilters = (store, { id, component }) => { ...@@ -14,21 +13,7 @@ const mountRadioFilters = (store, { id, component }) => {
el, el,
store, store,
render(createElement) { render(createElement) {
return createElement(component); return createElement(GlobalSearchSidebar);
}, },
}); });
}; };
const radioFilters = [
{
id: 'js-search-filter-by-state',
component: StatusFilter,
},
{
id: 'js-search-filter-by-confidential',
component: ConfidentialityFilter,
},
];
export const initSidebar = store =>
[...radioFilters].map(filter => mountRadioFilters(store, filter));
import Api from '~/api'; import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => { export const fetchGroups = ({ commit }, search) => {
...@@ -18,3 +19,11 @@ export const fetchGroups = ({ commit }, search) => { ...@@ -18,3 +19,11 @@ export const fetchGroups = ({ commit }, search) => {
export const setQuery = ({ commit }, { key, value }) => { export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value }); commit(types.SET_QUERY, { key, value });
}; };
export const applyQuery = ({ state }) => {
visitUrl(setUrlParams({ ...state.query, page: null }));
};
export const resetQuery = ({ state }) => {
visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null }));
};
...@@ -23,10 +23,6 @@ class SearchController < ApplicationController ...@@ -23,10 +23,6 @@ class SearchController < ApplicationController
search_term_present && !params[:project_id].present? search_term_present && !params[:project_id].present?
end end
before_action do
push_frontend_feature_flag(:search_facets)
end
layout 'search' layout 'search'
feature_category :global_search feature_category :global_search
......
- if @search_objects.to_a.empty? - if @search_objects.to_a.empty?
= render partial: "search/results/filters" .gl-display-md-flex
- if %w(issues merge_requests).include?(@scope)
#js-search-sidebar.gl-display-flex.gl-flex-direction-column.col-md-3.gl-mr-4{ }
.gl-w-full
= render partial: "search/results/empty" = render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
- else - else
...@@ -24,9 +27,11 @@ ...@@ -24,9 +27,11 @@
.gl-display-md-flex.gl-flex-direction-column .gl-display-md-flex.gl-flex-direction-column
= render partial: 'search/sort_dropdown' = render partial: 'search/sort_dropdown'
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render partial: "search/results/filters"
.results.gl-mt-3 .results.gl-display-md-flex.gl-mt-3
- if %w(issues merge_requests).include?(@scope)
#js-search-sidebar{ }
.gl-w-full
- if @scope == 'commits' - if @scope == 'commits'
%ul.content-list.commit-list %ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects = render partial: "search/results/commit", collection: @search_objects
......
.d-lg-flex.align-items-end
#js-search-filter-by-state{ 'v-cloak': true }
#js-search-filter-by-confidential{ 'v-cloak': true }
- if %w(issues merge_requests).include?(@scope)
%hr.gl-mt-4.gl-mb-4
---
title: Global Search - Left Sidebar
merge_request: 46595
author:
type: added
---
name: search_facets
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46809
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46595
group: group::global search
type: development
default_enabled: false
...@@ -22935,6 +22935,9 @@ msgstr "" ...@@ -22935,6 +22935,9 @@ msgstr ""
msgid "Reset authorization key?" msgid "Reset authorization key?"
msgstr "" msgstr ""
msgid "Reset filters"
msgstr ""
msgid "Reset health check access token" msgid "Reset health check access token"
msgstr "" msgstr ""
......
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { MOCK_QUERY } from 'jest/search/mock_data';
import * as urlUtils from '~/lib/utils/url_utility';
import initStore from '~/search/store';
import DropdownFilter from '~/search/dropdown_filter/components/dropdown_filter.vue';
import stateFilterData from '~/search/dropdown_filter/constants/state_filter_data';
import confidentialFilterData from '~/search/dropdown_filter/constants/confidential_filter_data';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.fn(),
}));
const localVue = createLocalVue();
localVue.use(Vuex);
describe('DropdownFilter', () => {
let wrapper;
let store;
const createStore = options => {
store = initStore({ query: MOCK_QUERY, ...options });
};
const createComponent = (props = { filterData: stateFilterData }) => {
wrapper = shallowMount(DropdownFilter, {
localVue,
store,
propsData: {
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
store = null;
});
const findGlDropdown = () => wrapper.find(GlDropdown);
const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
const firstDropDownItem = () => findGlDropdownItems().at(0);
describe('StatusFilter', () => {
describe('template', () => {
describe.each`
scope | showDropdown
${'issues'} | ${true}
${'merge_requests'} | ${true}
${'projects'} | ${false}
${'milestones'} | ${false}
${'users'} | ${false}
${'notes'} | ${false}
${'wiki_blobs'} | ${false}
${'blobs'} | ${false}
`(`dropdown`, ({ scope, showDropdown }) => {
beforeEach(() => {
createStore({ query: { ...MOCK_QUERY, scope } });
createComponent();
});
it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
expect(findGlDropdown().exists()).toBe(showDropdown);
});
});
describe.each`
initialFilter | label
${stateFilterData.filters.ANY.value} | ${`Any ${stateFilterData.header}`}
${stateFilterData.filters.OPEN.value} | ${stateFilterData.filters.OPEN.label}
${stateFilterData.filters.CLOSED.value} | ${stateFilterData.filters.CLOSED.label}
`(`filter text`, ({ initialFilter, label }) => {
describe(`when initialFilter is ${initialFilter}`, () => {
beforeEach(() => {
createStore({ query: { ...MOCK_QUERY, [stateFilterData.filterParam]: initialFilter } });
createComponent();
});
it(`sets dropdown label to ${label}`, () => {
expect(findGlDropdown().attributes('text')).toBe(label);
});
});
});
});
describe('Filter options', () => {
beforeEach(() => {
createStore();
createComponent();
});
it('renders a dropdown item for each filterOption', () => {
expect(findDropdownItemsText()).toStrictEqual(
stateFilterData.filterByScope[stateFilterData.scopes.ISSUES].map(v => {
return v.label;
}),
);
});
it('clicking a dropdown item calls setUrlParams', () => {
const filter = stateFilterData.filters[Object.keys(stateFilterData.filters)[0]].value;
firstDropDownItem().vm.$emit('click');
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
page: null,
[stateFilterData.filterParam]: filter,
});
});
it('clicking a dropdown item calls visitUrl', () => {
firstDropDownItem().vm.$emit('click');
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
describe('ConfidentialFilter', () => {
describe('template', () => {
describe.each`
scope | showDropdown
${'issues'} | ${true}
${'merge_requests'} | ${false}
${'projects'} | ${false}
${'milestones'} | ${false}
${'users'} | ${false}
${'notes'} | ${false}
${'wiki_blobs'} | ${false}
${'blobs'} | ${false}
`(`dropdown`, ({ scope, showDropdown }) => {
beforeEach(() => {
createStore({ query: { ...MOCK_QUERY, scope } });
createComponent({ filterData: confidentialFilterData });
});
it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
expect(findGlDropdown().exists()).toBe(showDropdown);
});
});
describe.each`
initialFilter | label
${confidentialFilterData.filters.ANY.value} | ${`Any ${confidentialFilterData.header}`}
${confidentialFilterData.filters.CONFIDENTIAL.value} | ${confidentialFilterData.filters.CONFIDENTIAL.label}
${confidentialFilterData.filters.NOT_CONFIDENTIAL.value} | ${confidentialFilterData.filters.NOT_CONFIDENTIAL.label}
`(`filter text`, ({ initialFilter, label }) => {
describe(`when initialFilter is ${initialFilter}`, () => {
beforeEach(() => {
createStore({
query: { ...MOCK_QUERY, [confidentialFilterData.filterParam]: initialFilter },
});
createComponent({ filterData: confidentialFilterData });
});
it(`sets dropdown label to ${label}`, () => {
expect(findGlDropdown().attributes('text')).toBe(label);
});
});
});
});
});
describe('Filter options', () => {
beforeEach(() => {
createStore();
createComponent({ filterData: confidentialFilterData });
});
it('renders a dropdown item for each filterOption', () => {
expect(findDropdownItemsText()).toStrictEqual(
confidentialFilterData.filterByScope[confidentialFilterData.scopes.ISSUES].map(v => {
return v.label;
}),
);
});
it('clicking a dropdown item calls setUrlParams', () => {
const filter =
confidentialFilterData.filters[Object.keys(confidentialFilterData.filters)[0]].value;
firstDropDownItem().vm.$emit('click');
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
page: null,
[confidentialFilterData.filterParam]: filter,
});
});
it('clicking a dropdown item calls visitUrl', () => {
firstDropDownItem().vm.$emit('click');
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink } from '@gitlab/ui';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GlobalSearchSidebar', () => {
let wrapper;
const actionSpies = {
applyQuery: jest.fn(),
resetQuery: jest.fn(),
};
const createComponent = initialState => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
...initialState,
},
actions: actionSpies,
});
wrapper = shallowMount(GlobalSearchSidebar, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSidebarForm = () => wrapper.find('form');
const findStatusFilter = () => wrapper.find(StatusFilter);
const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
const findApplyButton = () => wrapper.find(GlButton);
const findResetLinkButton = () => wrapper.find(GlLink);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders StatusFilter always', () => {
expect(findStatusFilter().exists()).toBe(true);
});
it('renders ConfidentialityFilter always', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
it('renders ApplyButton always', () => {
expect(findApplyButton().exists()).toBe(true);
});
describe('ResetLinkButton', () => {
describe('with no filter selected', () => {
beforeEach(() => {
createComponent({ query: {} });
});
it('does not render', () => {
expect(findResetLinkButton().exists()).toBe(false);
});
});
describe('with filter selected', () => {
it('does render when a filter selected', () => {
expect(findResetLinkButton().exists()).toBe(true);
});
});
});
});
describe('actions', () => {
beforeEach(() => {
createComponent();
});
it('clicking ApplyButton calls applyQuery', () => {
findSidebarForm().trigger('submit');
expect(actionSpies.applyQuery).toHaveBeenCalled();
});
it('clicking ResetLinkButton calls resetQuery', () => {
findResetLinkButton().vm.$emit('click');
expect(actionSpies.resetQuery).toHaveBeenCalled();
});
});
});
...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/search/store/actions'; import * as actions from '~/search/store/actions';
import * as types from '~/search/store/mutation_types'; import * as types from '~/search/store/mutation_types';
import * as urlUtils from '~/lib/utils/url_utility';
import state from '~/search/store/state'; import state from '~/search/store/state';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -42,6 +43,47 @@ describe('Global Search Store Actions', () => { ...@@ -42,6 +43,47 @@ describe('Global Search Store Actions', () => {
}); });
}); });
}); });
describe('setQuery', () => {
const payload = { key: 'key1', value: 'value1' };
it('calls the SET_QUERY mutation', done => {
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
});
});
describe('applyQuery', () => {
beforeEach(() => {
urlUtils.setUrlParams = jest.fn();
urlUtils.visitUrl = jest.fn();
});
it('calls visitUrl and setParams with the state.query', () => {
testAction(actions.applyQuery, null, state, [], [], () => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
describe('resetQuery', () => {
beforeEach(() => {
urlUtils.setUrlParams = jest.fn();
urlUtils.visitUrl = jest.fn();
});
it('calls visitUrl and setParams with empty values', () => {
testAction(actions.resetQuery, null, state, [], [], () => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
...state.query,
page: null,
state: null,
confidential: null,
});
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
}); });
describe('setQuery', () => { describe('setQuery', () => {
......
...@@ -43,7 +43,7 @@ RSpec.describe 'search/_results' do ...@@ -43,7 +43,7 @@ RSpec.describe 'search/_results' do
let_it_be(:wiki_blob) { create(:wiki_page, project: project, content: '*') } let_it_be(:wiki_blob) { create(:wiki_page, project: project, content: '*') }
let_it_be(:user) { create(:admin) } let_it_be(:user) { create(:admin) }
%w[issues blobs notes wiki_blobs merge_requests milestones].each do |search_scope| %w[issues merge_requests].each do |search_scope|
context "when scope is #{search_scope}" do context "when scope is #{search_scope}" do
let(:scope) { search_scope } let(:scope) { search_scope }
let(:search_objects) { Gitlab::ProjectSearchResults.new(user, '*', project: project).objects(scope) } let(:search_objects) { Gitlab::ProjectSearchResults.new(user, '*', project: project).objects(scope) }
...@@ -55,16 +55,30 @@ RSpec.describe 'search/_results' do ...@@ -55,16 +55,30 @@ RSpec.describe 'search/_results' do
expect(rendered).to have_selector('[data-track-property=search_result]') expect(rendered).to have_selector('[data-track-property=search_result]')
end end
it 'renders the state filter drop down' do it 'does render the sidebar' do
render render
expect(rendered).to have_selector('#js-search-filter-by-state') expect(rendered).to have_selector('#js-search-sidebar')
end
end
end
%w[blobs notes wiki_blobs milestones].each do |search_scope|
context "when scope is #{search_scope}" do
let(:scope) { search_scope }
let(:search_objects) { Gitlab::ProjectSearchResults.new(user, '*', project: project).objects(scope) }
it 'renders the click text event tracking attributes' do
render
expect(rendered).to have_selector('[data-track-event=click_text]')
expect(rendered).to have_selector('[data-track-property=search_result]')
end end
it 'renders the confidential drop down' do it 'does not render the sidebar' do
render render
expect(rendered).to have_selector('#js-search-filter-by-confidential') expect(rendered).not_to have_selector('#js-search-sidebar')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment