Commit 1e5ef1a2 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch...

Merge branch '249529-productivity-analytics-use-similarity-search-option-for-graphql-projects-queries' into 'master'

PA: Use similarity search option for GraphQL projects queries

See merge request gitlab-org/gitlab!43539
parents 26cfa3c1 86665d7d
...@@ -210,6 +210,7 @@ export default { ...@@ -210,6 +210,7 @@ export default {
:key="currentGroup.id" :key="currentGroup.id"
class="js-projects-dropdown-filter project-select" class="js-projects-dropdown-filter project-select"
:group-id="currentGroup.id" :group-id="currentGroup.id"
:group-namespace="currentGroupPath"
:query-params="projectsQueryParams" :query-params="projectsQueryParams"
:multi-select="$options.multiProjectSelect" :multi-select="$options.multiProjectSelect"
:default-projects="selectedProjects" :default-projects="selectedProjects"
......
...@@ -4,7 +4,6 @@ import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter ...@@ -4,7 +4,6 @@ import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
import { accessLevelReporter, projectsPerPage } from '../constants'; import { accessLevelReporter, projectsPerPage } from '../constants';
import { SIMILARITY_ORDER, LAST_ACTIVITY_AT } from '../../shared/constants';
export default { export default {
components: { components: {
...@@ -44,10 +43,8 @@ export default { ...@@ -44,10 +43,8 @@ export default {
}, },
projectsQueryParams() { projectsQueryParams() {
return { return {
per_page: projectsPerPage, first: projectsPerPage,
with_shared: false, // exclude forks includeSubgroups: true,
order_by: this.glFeatures.analyticsSimilaritySearch ? SIMILARITY_ORDER : LAST_ACTIVITY_AT,
include_subgroups: true,
}; };
}, },
}, },
...@@ -59,13 +56,8 @@ export default { ...@@ -59,13 +56,8 @@ export default {
this.$emit('groupSelected', { groupId: id, groupNamespace: full_path }); this.$emit('groupSelected', { groupId: id, groupNamespace: full_path });
}, },
onProjectsSelected(selectedProjects) { onProjectsSelected(selectedProjects) {
let projectNamespace = null; const projectNamespace = selectedProjects[0]?.fullPath || null;
let projectId = null; const projectId = selectedProjects[0]?.id || null;
if (selectedProjects.length) {
projectNamespace = selectedProjects[0].path_with_namespace;
projectId = selectedProjects[0].id;
}
this.setProjectPath(projectNamespace); this.setProjectPath(projectNamespace);
this.$emit('projectSelected', { this.$emit('projectSelected', {
...@@ -98,6 +90,8 @@ export default { ...@@ -98,6 +90,8 @@ export default {
:default-projects="projects" :default-projects="projects"
:query-params="projectsQueryParams" :query-params="projectsQueryParams"
:group-id="groupId" :group-id="groupId"
:group-namespace="groupNamespace"
:use-graphql="true"
@selected="onProjectsSelected" @selected="onProjectsSelected"
/> />
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import store from './store'; import store from './store';
import FilterDropdowns from './components/filter_dropdowns.vue'; import FilterDropdowns from './components/filter_dropdowns.vue';
import DateRange from '../shared/components/daterange.vue'; import DateRange from '../shared/components/daterange.vue';
import ProductivityAnalyticsApp from './components/app.vue'; import ProductivityAnalyticsApp from './components/app.vue';
import FilteredSearchProductivityAnalytics from './filtered_search_productivity_analytics'; import FilteredSearchProductivityAnalytics from './filtered_search_productivity_analytics';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getLabelsEndpoint, getMilestonesEndpoint } from './utils'; import { getLabelsEndpoint, getMilestonesEndpoint } from './utils';
import { buildGroupFromDataset, buildProjectFromDataset } from '../shared/utils'; import { buildGroupFromDataset, buildProjectFromDataset } from '../shared/utils';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const container = document.getElementById('js-productivity-analytics'); const container = document.getElementById('js-productivity-analytics');
const groupProjectSelectContainer = container.querySelector('.js-group-project-select-container'); const groupProjectSelectContainer = container.querySelector('.js-group-project-select-container');
...@@ -65,6 +74,7 @@ export default () => { ...@@ -65,6 +74,7 @@ export default () => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: groupProjectSelectContainer, el: groupProjectSelectContainer,
apolloProvider,
store, store,
created() { created() {
// let's not fetch any data by default since we might not have a valid group yet // let's not fetch any data by default since we might not have a valid group yet
...@@ -76,8 +86,8 @@ export default () => { ...@@ -76,8 +86,8 @@ export default () => {
this.initFilteredSearch({ this.initFilteredSearch({
groupNamespace: group.full_path, groupNamespace: group.full_path,
groupId: group.id, groupId: group.id,
projectNamespace: project ? project.path_with_namespace : null, projectNamespace: project?.path_with_namespace || null,
projectId: project ? project.id : null, projectId: container.dataset.projectId || null,
}); });
// let's fetch data now since we do have a valid group // let's fetch data now since we do have a valid group
...@@ -93,7 +103,12 @@ export default () => { ...@@ -93,7 +103,12 @@ export default () => {
this.initFilteredSearch({ groupNamespace, groupId }); this.initFilteredSearch({ groupNamespace, groupId });
}, },
onProjectSelected({ groupNamespace, groupId, projectNamespace, projectId }) { onProjectSelected({ groupNamespace, groupId, projectNamespace, projectId }) {
this.initFilteredSearch({ groupNamespace, groupId, projectNamespace, projectId }); this.initFilteredSearch({
groupNamespace,
groupId,
projectNamespace,
projectId: getIdFromGraphQLId(projectId),
});
}, },
initFilteredSearch({ groupNamespace, groupId, projectNamespace = '', projectId = null }) { initFilteredSearch({ groupNamespace, groupId, projectNamespace = '', projectId = null }) {
// let's unbind attached event handlers first and reset the template // let's unbind attached event handlers first and reset the template
......
...@@ -11,8 +11,10 @@ import { ...@@ -11,8 +11,10 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { n__, s__, __ } from '~/locale'; import { n__, s__, __ } from '~/locale';
import Api from '~/api'; import Api from '~/api';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DATA_REFETCH_DELAY } from '../constants'; import { DATA_REFETCH_DELAY } from '../constants';
import { filterBySearchTerm } from '../utils'; import { filterBySearchTerm } from '../utils';
import getProjects from '../graphql/projects.query.graphql';
export default { export default {
name: 'ProjectsDropdownFilter', name: 'ProjectsDropdownFilter',
...@@ -30,6 +32,10 @@ export default { ...@@ -30,6 +32,10 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
groupNamespace: {
type: String,
required: true,
},
multiSelect: { multiSelect: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -50,6 +56,11 @@ export default { ...@@ -50,6 +56,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
useGraphql: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -121,6 +132,31 @@ export default { ...@@ -121,6 +132,31 @@ export default {
}, },
fetchData() { fetchData() {
this.loading = true; this.loading = true;
if (this.useGraphql) {
return this.$apollo
.query({
query: getProjects,
variables: {
groupFullPath: this.groupNamespace,
search: this.searchTerm,
...this.queryParams,
},
})
.then(response => {
const {
data: {
group: {
projects: { nodes },
},
},
} = response;
this.loading = false;
this.projects = nodes;
});
}
return Api.groupProjects(this.groupId, this.searchTerm, this.queryParams, projects => { return Api.groupProjects(this.groupId, this.searchTerm, this.queryParams, projects => {
this.projects = projects; this.projects = projects;
this.loading = false; this.loading = false;
...@@ -129,6 +165,11 @@ export default { ...@@ -129,6 +165,11 @@ export default {
isProjectSelected(id) { isProjectSelected(id) {
return this.selectedProjects ? this.selectedProjectIds.includes(id) : false; return this.selectedProjects ? this.selectedProjectIds.includes(id) : false;
}, },
getEntityId(project) {
if (this.useGraphql) return getIdFromGraphQLId(project.id);
return project?.id || null;
},
}, },
}; };
</script> </script>
...@@ -143,8 +184,8 @@ export default { ...@@ -143,8 +184,8 @@ export default {
<div class="gl-display-flex gl-flex-fill-1"> <div class="gl-display-flex gl-flex-fill-1">
<gl-avatar <gl-avatar
v-if="isOnlyOneProjectSelected" v-if="isOnlyOneProjectSelected"
:src="selectedProjects[0].avatar_url" :src="useGraphql ? selectedProjects[0].avatarUrl : selectedProjects[0].avatar_url"
:entity-id="selectedProjects[0].id" :entity-id="getEntityId(selectedProjects[0])"
:entity-name="selectedProjects[0].name" :entity-name="selectedProjects[0].name"
:size="16" :size="16"
shape="rect" shape="rect"
...@@ -170,9 +211,9 @@ export default { ...@@ -170,9 +211,9 @@ export default {
class="gl-mr-2 vertical-align-middle" class="gl-mr-2 vertical-align-middle"
:alt="project.name" :alt="project.name"
:size="16" :size="16"
:entity-id="project.id" :entity-id="getEntityId(project)"
:entity-name="project.name" :entity-name="project.name"
:src="project.avatar_url" :src="useGraphql ? project.avatarUrl : project.avatar_url"
shape="rect" shape="rect"
/> />
{{ project.name }} {{ project.name }}
......
query getGroupProjects(
$groupFullPath: ID!
$search: String!
$first: Int!
$includeSubgroups: Boolean = false
) {
group(fullPath: $groupFullPath) {
projects(
search: $search
first: $first
includeSubgroups: $includeSubgroups
sort: SIMILARITY
) {
nodes {
id
name
avatarUrl
fullPath
}
}
}
}
...@@ -39,11 +39,11 @@ export const buildGroupFromDataset = dataset => { ...@@ -39,11 +39,11 @@ export const buildGroupFromDataset = dataset => {
* @returns {Object} - A project object * @returns {Object} - A project object
*/ */
export const buildProjectFromDataset = dataset => { export const buildProjectFromDataset = dataset => {
const { projectId, projectName, projectPathWithNamespace, projectAvatarUrl } = dataset; const { projectGid, projectName, projectPathWithNamespace, projectAvatarUrl } = dataset;
if (projectId) { if (projectGid) {
return { return {
id: Number(projectId), id: projectGid,
name: projectName, name: projectName,
path_with_namespace: projectPathWithNamespace, path_with_namespace: projectPathWithNamespace,
avatar_url: projectAvatarUrl, avatar_url: projectAvatarUrl,
......
...@@ -66,6 +66,7 @@ module Analytics ...@@ -66,6 +66,7 @@ module Analytics
def project_data_attributes def project_data_attributes
{ {
id: project.id, id: project.id,
gid: project.to_gid.to_s,
name: project.name, name: project.name,
path_with_namespace: project.path_with_namespace, path_with_namespace: project.path_with_namespace,
avatar_url: project.avatar_url avatar_url: project.avatar_url
......
...@@ -21,7 +21,7 @@ describe('FilterDropdowns component', () => { ...@@ -21,7 +21,7 @@ describe('FilterDropdowns component', () => {
const groupId = 1; const groupId = 1;
const groupNamespace = 'gitlab-org'; const groupNamespace = 'gitlab-org';
const projectPath = 'gitlab-org/gitlab-test'; const projectPath = 'gitlab-org/gitlab-test';
const projectId = 10; const projectId = 'gid://gitlab/Project/1';
beforeEach(() => { beforeEach(() => {
const { const {
...@@ -41,6 +41,7 @@ describe('FilterDropdowns component', () => { ...@@ -41,6 +41,7 @@ describe('FilterDropdowns component', () => {
...modules, ...modules,
}, },
}); });
wrapper = shallowMount(FilterDropdowns, { wrapper = shallowMount(FilterDropdowns, {
localVue, localVue,
store: mockStore, store: mockStore,
...@@ -71,6 +72,7 @@ describe('FilterDropdowns component', () => { ...@@ -71,6 +72,7 @@ describe('FilterDropdowns component', () => {
describe('with a group selected', () => { describe('with a group selected', () => {
beforeEach(() => { beforeEach(() => {
wrapper.vm.groupId = groupId; wrapper.vm.groupId = groupId;
mockStore.state.filters.groupNamespace = groupNamespace;
}); });
it('renders the projects dropdown', () => { it('renders the projects dropdown', () => {
...@@ -107,7 +109,7 @@ describe('FilterDropdowns component', () => { ...@@ -107,7 +109,7 @@ describe('FilterDropdowns component', () => {
describe('when the list of selected projects is not empty', () => { describe('when the list of selected projects is not empty', () => {
beforeEach(() => { beforeEach(() => {
mockStore.state.filters.groupNamespace = groupNamespace; mockStore.state.filters.groupNamespace = groupNamespace;
wrapper.vm.onProjectsSelected([{ id: projectId, path_with_namespace: `${projectPath}` }]); wrapper.vm.onProjectsSelected([{ id: projectId, fullPath: `${projectPath}` }]);
}); });
it('invokes setProjectPath action', () => { it('invokes setProjectPath action', () => {
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue';
import getProjects from 'ee/analytics/shared/graphql/projects.query.graphql';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { LAST_ACTIVITY_AT } from 'ee/analytics/shared/constants'; import { LAST_ACTIVITY_AT } from 'ee/analytics/shared/constants';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -9,6 +10,27 @@ jest.mock('~/api', () => ({ ...@@ -9,6 +10,27 @@ jest.mock('~/api', () => ({
groupProjects: jest.fn(), groupProjects: jest.fn(),
})); }));
const mockGraphqlProjects = [
{
id: 'gid://gitlab/Project/1',
name: 'Gitlab Test',
fullPath: 'gitlab-org/gitlab-test',
avatarUrl: `${TEST_HOST}/images/home/nasa.svg`,
},
{
id: 'gid://gitlab/Project/2',
name: 'Gitlab Shell',
fullPath: 'gitlab-org/gitlab-shell',
avatarUrl: null,
},
{
id: 'gid://gitlab/Project/3',
name: 'Foo',
fullPath: 'gitlab-org/foo',
avatarUrl: null,
},
];
const projects = [ const projects = [
{ {
id: 1, id: 1,
...@@ -27,13 +49,26 @@ const projects = [ ...@@ -27,13 +49,26 @@ const projects = [
}, },
]; ];
const defaultMocks = {
$apollo: {
query: jest.fn().mockResolvedValue({
data: { group: { projects: { nodes: mockGraphqlProjects } } },
}),
},
};
let spyQuery;
describe('ProjectsDropdownFilter component', () => { describe('ProjectsDropdownFilter component', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
spyQuery = defaultMocks.$apollo.query;
wrapper = mount(ProjectsDropdownFilter, { wrapper = mount(ProjectsDropdownFilter, {
mocks: { ...defaultMocks },
propsData: { propsData: {
groupId: 1, groupId: 1,
groupNamespace: 'gitlab-org',
...props, ...props,
}, },
}); });
...@@ -43,12 +78,6 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -43,12 +78,6 @@ describe('ProjectsDropdownFilter component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
beforeEach(() => {
Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
});
const findDropdown = () => wrapper.find(GlDropdown); const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => const findDropdownItems = () =>
...@@ -60,203 +89,362 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -60,203 +89,362 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownButton = () => findDropdown().find('.dropdown-toggle'); const findDropdownButton = () => findDropdown().find('.dropdown-toggle');
const findDropdownButtonAvatar = () => findDropdown().find('.gl-avatar'); const findDropdownButtonAvatar = () => findDropdown().find('.gl-avatar');
const findDropdownButtonAvatarAtIndex = index => findDropdownAtIndex(index).find('img.gl-avatar');
const findDropdownButtonIdentIconAtIndex = index =>
findDropdownAtIndex(index).find('div.gl-avatar-identicon');
const selectDropdownItemAtIndex = index => const selectDropdownItemAtIndex = index =>
findDropdownAtIndex(index) findDropdownAtIndex(index)
.find('button') .find('button')
.trigger('click'); .trigger('click');
describe('queryParams are applied when fetching data', () => { describe('when using the REST API', () => {
describe('queryParams are applied when fetching data', () => {
beforeEach(() => {
Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
createComponent({
queryParams: {
per_page: 50,
with_shared: false,
order_by: LAST_ACTIVITY_AT,
},
});
});
it('applies the correct queryParams when making an api call', () => {
expect(Api.groupProjects).toHaveBeenCalledWith(
expect.any(Number),
expect.any(String),
expect.objectContaining({ per_page: 50, with_shared: false, order_by: LAST_ACTIVITY_AT }),
expect.any(Function),
);
});
});
});
describe('when using the GraphQL API', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
useGraphql: true,
queryParams: { queryParams: {
per_page: 50, first: 50,
with_shared: false, includeSubgroups: true,
order_by: LAST_ACTIVITY_AT,
}, },
}); });
}); });
it('applies the correct queryParams when making an api call', () => { it('applies the correct queryParams when making an api call', async () => {
expect(Api.groupProjects).toHaveBeenCalledWith( wrapper.setData({ searchTerm: 'gitlab' });
expect.any(Number),
expect.any(String), expect(spyQuery).toHaveBeenCalledTimes(1);
expect.objectContaining({ per_page: 50, with_shared: false, order_by: LAST_ACTIVITY_AT }),
expect.any(Function), await wrapper.vm.$nextTick(() => {
); expect(spyQuery).toHaveBeenCalledWith({
query: getProjects,
variables: {
search: 'gitlab',
groupFullPath: wrapper.vm.groupNamespace,
first: 50,
includeSubgroups: true,
},
});
});
}); });
}); });
describe('when passed a an array of defaultProject as prop', () => { describe('when passed a an array of defaultProject as prop', () => {
beforeEach(() => { describe('when using the RESTP API', () => {
createComponent({ beforeEach(() => {
defaultProjects: [projects[0]], Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
createComponent({
defaultProjects: [projects[0]],
});
}); });
});
it("displays the defaultProject's name", () => { it("displays the defaultProject's name", () => {
expect(findDropdownButton().text()).toContain(projects[0].name); expect(findDropdownButton().text()).toContain(projects[0].name);
}); });
it("renders the defaultProject's avatar", () => { it("renders the defaultProject's avatar", () => {
expect(findDropdownButtonAvatar().exists()).toBe(true); expect(findDropdownButtonAvatar().exists()).toBe(true);
}); });
it('marks the defaultProject as selected', () => { it('marks the defaultProject as selected', () => {
expect(findDropdownAtIndex(0).props('isChecked')).toBe(true); expect(findDropdownAtIndex(0).props('isChecked')).toBe(true);
});
}); });
});
describe('when multiSelect is false', () => { describe('when using the GraphQL API', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ multiSelect: false }); createComponent({
}); useGraphql: true,
defaultProjects: [mockGraphqlProjects[0]],
});
});
describe('displays the correct information', () => { it("displays the defaultProject's name", () => {
it('contains 3 items', () => { expect(findDropdownButton().text()).toContain(mockGraphqlProjects[0].name);
expect(findDropdownItems()).toHaveLength(3);
}); });
it('renders an avatar when the project has an avatar_url', () => { it("renders the defaultProject's avatar", () => {
expect( expect(findDropdownButtonAvatar().exists()).toBe(true);
findDropdownAtIndex(0)
.find('img.gl-avatar')
.exists(),
).toBe(true);
expect(
findDropdownAtIndex(0)
.find('div.gl-avatar-identicon')
.exists(),
).toBe(false);
}); });
it("renders an identicon when the project doesn't have an avatar_url", () => {
expect( it('marks the defaultProject as selected', () => {
findDropdownAtIndex(1) expect(findDropdownAtIndex(0).props('isChecked')).toBe(true);
.find('img.gl-avatar')
.exists(),
).toBe(false);
expect(
findDropdownAtIndex(1)
.find('div.gl-avatar-identicon')
.exists(),
).toBe(true);
}); });
}); });
});
describe('on project click', () => { describe('when multiSelect is false', () => {
it('should emit the "selected" event with the selected project', () => { describe('when using the RESTP API', () => {
selectDropdownItemAtIndex(0); beforeEach(() => {
Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]); createComponent({ multiSelect: false });
}); });
it('should change selection when new project is clicked', () => { describe('displays the correct information', () => {
selectDropdownItemAtIndex(1); it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]); it('renders an avatar when the project has an avatar_url', () => {
expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
});
it("renders an identicon when the project doesn't have an avatar_url", () => {
expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
});
}); });
it('selection should be emptied when a project is deselected', () => { describe('on project click', () => {
selectDropdownItemAtIndex(0); // Select the item it('should emit the "selected" event with the selected project', () => {
selectDropdownItemAtIndex(0); // deselect it selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]);
});
it('should change selection when new project is clicked', () => {
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]);
});
it('selection should be emptied when a project is deselected', () => {
selectDropdownItemAtIndex(0); // Select the item
selectDropdownItemAtIndex(0); // deselect it
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
});
it('renders an avatar in the dropdown button when the project has an avatar_url', async () => {
selectDropdownItemAtIndex(0);
await wrapper.vm.$nextTick().then(() => {
expect(
findDropdownButton()
.find('img.gl-avatar')
.exists(),
).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
});
});
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]); it("renders an identicon in the dropdown button when the project doesn't have an avatar_url", async () => {
selectDropdownItemAtIndex(1);
await wrapper.vm.$nextTick().then(() => {
expect(
findDropdownButton()
.find('img.gl-avatar')
.exists(),
).toBe(false);
expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
});
});
}); });
});
describe('when using the GraphQl API', () => {
beforeEach(() => {
createComponent({ multiSelect: false, useGraphql: true });
});
describe('displays the correct information', () => {
it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders an avatar in the dropdown button when the project has an avatar_url', () => { it('renders an avatar when the project has an avatarUrl', () => {
selectDropdownItemAtIndex(0); expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
return wrapper.vm.$nextTick().then(() => { });
expect( it("renders an identicon when the project doesn't have an avatarUrl", () => {
findDropdownButton() expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
.find('img.gl-avatar') expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
.exists(),
).toBe(true);
expect(
findDropdownButton()
.find('.gl-avatar-identicon')
.exists(),
).toBe(false);
}); });
}); });
it("renders an identicon in the dropdown button when the project doesn't have an avatar_url", () => { describe('on project click', () => {
selectDropdownItemAtIndex(1); it('should emit the "selected" event with the selected project', () => {
selectDropdownItemAtIndex(0);
return wrapper.vm.$nextTick().then(() => {
expect( expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[0]]]]);
findDropdownButton() });
.find('img.gl-avatar')
.exists(), it('should change selection when new project is clicked', () => {
).toBe(false); selectDropdownItemAtIndex(1);
expect(
findDropdownButton() expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[1]]]]);
.find('.gl-avatar-identicon') });
.exists(),
).toBe(true); it('selection should be emptied when a project is deselected', () => {
selectDropdownItemAtIndex(0); // Select the item
selectDropdownItemAtIndex(0); // deselect it
expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[0]]], [[]]]);
});
it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
selectDropdownItemAtIndex(0);
await wrapper.vm.$nextTick().then(() => {
expect(
findDropdownButton()
.find('img.gl-avatar')
.exists(),
).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
});
});
it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
selectDropdownItemAtIndex(1);
await wrapper.vm.$nextTick().then(() => {
expect(
findDropdownButton()
.find('img.gl-avatar')
.exists(),
).toBe(false);
expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
});
}); });
}); });
}); });
}); });
describe('when multiSelect is true', () => { describe('when multiSelect is true', () => {
beforeEach(() => { describe('when using the RESTP API', () => {
createComponent({ multiSelect: true }); beforeEach(() => {
}); Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
describe('displays the correct information', () => { createComponent({ multiSelect: true });
it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
}); });
it('renders an avatar when the project has an avatar_url', () => { describe('displays the correct information', () => {
expect( it('contains 3 items', () => {
findDropdownAtIndex(0) expect(findDropdownItems()).toHaveLength(3);
.find('img.gl-avatar') });
.exists(),
).toBe(true); it('renders an avatar when the project has an avatar_url', () => {
expect( expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
findDropdownAtIndex(0) expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
.find('div.gl-avatar-identicon') });
.exists(),
).toBe(false); it("renders an identicon when the project doesn't have an avatar_url", () => {
expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
});
}); });
it("renders an identicon when the project doesn't have an avatar_url", () => { describe('on project click', () => {
expect( it('should add to selection when new project is clicked', () => {
findDropdownAtIndex(1) selectDropdownItemAtIndex(0);
.find('img.gl-avatar') selectDropdownItemAtIndex(1);
.exists(),
).toBe(false); expect(wrapper.emitted().selected).toEqual([
expect( [[projects[0]]],
findDropdownAtIndex(1) [[projects[0], projects[1]]],
.find('div.gl-avatar-identicon') ]);
.exists(), });
).toBe(true);
it('should remove from selection when clicked again', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
await wrapper.vm.$nextTick().then(() => {
expect(findDropdownButton().text()).toBe('2 projects selected');
});
});
}); });
}); });
describe('on project click', () => { describe('when using the GraphQl API', () => {
it('should add to selection when new project is clicked', () => { beforeEach(() => {
selectDropdownItemAtIndex(0); createComponent({ multiSelect: true, useGraphql: true });
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[projects[0], projects[1]]]]);
}); });
it('should remove from selection when clicked again', () => { describe('displays the correct information', () => {
selectDropdownItemAtIndex(0); it('contains 3 items', () => {
selectDropdownItemAtIndex(0); expect(findDropdownItems()).toHaveLength(3);
});
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]); it('renders an avatar when the project has an avatarUrl', () => {
expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
});
it("renders an identicon when the project doesn't have an avatarUrl", () => {
expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
});
}); });
it('renders the correct placeholder text when multiple projects are selected', () => { describe('on project click', () => {
selectDropdownItemAtIndex(0); it('should add to selection when new project is clicked', () => {
selectDropdownItemAtIndex(1); selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([
[[mockGraphqlProjects[0]]],
[[mockGraphqlProjects[0], mockGraphqlProjects[1]]],
]);
});
it('should remove from selection when clicked again', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[0]]], [[]]]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick().then(() => {
expect(findDropdownButton().text()).toBe('2 projects selected'); expect(findDropdownButton().text()).toBe('2 projects selected');
});
}); });
}); });
}); });
......
...@@ -22,6 +22,7 @@ const subGroupDataset = { ...@@ -22,6 +22,7 @@ const subGroupDataset = {
const projectDataset = { const projectDataset = {
projectId: '1', projectId: '1',
projectGid: 'gid://gitlab/Project/1',
projectName: 'My Project', projectName: 'My Project',
projectPathWithNamespace: 'my-group/my-project', projectPathWithNamespace: 'my-group/my-project',
}; };
...@@ -66,7 +67,7 @@ describe('buildProjectFromDataset', () => { ...@@ -66,7 +67,7 @@ describe('buildProjectFromDataset', () => {
it('returns a project object when the projectId is given', () => { it('returns a project object when the projectId is given', () => {
expect(buildProjectFromDataset(projectDataset)).toEqual({ expect(buildProjectFromDataset(projectDataset)).toEqual({
id: 1, id: 'gid://gitlab/Project/1',
name: 'My Project', name: 'My Project',
path_with_namespace: 'my-group/my-project', path_with_namespace: 'my-group/my-project',
avatar_url: undefined, avatar_url: undefined,
......
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