Commit a65be6a1 authored by Martin Wortschack's avatar Martin Wortschack

Switch VSA to be using GraphQL for projects query

- This MR switches group-level VSA
to be using the GraphQL API
for querying projects
parent 1e5ef1a2
...@@ -3,7 +3,7 @@ import { GlEmptyState } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlEmptyState } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { PROJECTS_PER_PAGE } from '../constants'; import { PROJECTS_PER_PAGE } from '../constants';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
import { SIMILARITY_ORDER, LAST_ACTIVITY_AT, DATE_RANGE_LIMIT } from '../../shared/constants'; import { DATE_RANGE_LIMIT } from '../../shared/constants';
import DateRange from '../../shared/components/daterange.vue'; import DateRange from '../../shared/components/daterange.vue';
import StageTable from './stage_table.vue'; import StageTable from './stage_table.vue';
import DurationChart from './duration_chart.vue'; import DurationChart from './duration_chart.vue';
...@@ -116,12 +116,8 @@ export default { ...@@ -116,12 +116,8 @@ export default {
}, },
projectsQueryParams() { projectsQueryParams() {
return { return {
per_page: PROJECTS_PER_PAGE, first: PROJECTS_PER_PAGE,
with_shared: false, includeSubgroups: true,
order_by: this.featureFlags.hasAnalyticsSimilaritySearch
? SIMILARITY_ORDER
: LAST_ACTIVITY_AT,
include_subgroups: true,
}; };
}, },
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import CycleAnalytics from './components/base.vue'; import CycleAnalytics from './components/base.vue';
import createStore from './store'; import createStore from './store';
import { buildCycleAnalyticsInitialData } from '../shared/utils'; import { buildCycleAnalyticsInitialData } from '../shared/utils';
import createDefaultClient from '~/lib/graphql';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
Vue.use(GlToast); Vue.use(GlToast);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const el = document.querySelector('#js-cycle-analytics-app'); const el = document.querySelector('#js-cycle-analytics-app');
...@@ -43,6 +50,7 @@ export default () => { ...@@ -43,6 +50,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
name: 'CycleAnalyticsApp', name: 'CycleAnalyticsApp',
apolloProvider,
store, store,
render: createElement => render: createElement =>
createElement(CycleAnalytics, { createElement(CycleAnalytics, {
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { dateFormats } from '../../shared/constants'; import { dateFormats } from '../../shared/constants';
import { transformStagesForPathNavigation } from '../utils'; import { transformStagesForPathNavigation } from '../utils';
...@@ -14,7 +15,7 @@ export const currentValueStreamId = ({ selectedValueStream }) => ...@@ -14,7 +15,7 @@ export const currentValueStreamId = ({ selectedValueStream }) =>
export const currentGroupPath = ({ currentGroup }) => currentGroup?.fullPath || null; export const currentGroupPath = ({ currentGroup }) => currentGroup?.fullPath || null;
export const selectedProjectIds = ({ selectedProjects }) => export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects?.map(({ id }) => id) || []; selectedProjects?.map(({ id }) => getIdFromGraphQLId(id)) || [];
export const cycleAnalyticsRequestParams = (state, getters) => { export const cycleAnalyticsRequestParams = (state, getters) => {
const { const {
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { n__, s__, __ } from '~/locale'; import { n__, s__, __ } from '~/locale';
import Api from '~/api';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; 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';
...@@ -56,11 +55,6 @@ export default { ...@@ -56,11 +55,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
useGraphql: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -133,42 +127,33 @@ export default { ...@@ -133,42 +127,33 @@ export default {
fetchData() { fetchData() {
this.loading = true; this.loading = true;
if (this.useGraphql) { return this.$apollo
return this.$apollo .query({
.query({ query: getProjects,
query: getProjects, variables: {
variables: { groupFullPath: this.groupNamespace,
groupFullPath: this.groupNamespace, search: this.searchTerm,
search: this.searchTerm, ...this.queryParams,
...this.queryParams, },
}, })
}) .then(response => {
.then(response => { const {
const { data: {
data: { group: {
group: { projects: { nodes },
projects: { nodes },
},
}, },
} = response; },
} = response;
this.loading = false;
this.projects = nodes;
});
}
return Api.groupProjects(this.groupId, this.searchTerm, this.queryParams, projects => { this.loading = false;
this.projects = projects; this.projects = nodes;
this.loading = false; });
});
}, },
isProjectSelected(id) { isProjectSelected(id) {
return this.selectedProjects ? this.selectedProjectIds.includes(id) : false; return this.selectedProjects ? this.selectedProjectIds.includes(id) : false;
}, },
getEntityId(project) { getEntityId(project) {
if (this.useGraphql) return getIdFromGraphQLId(project.id); return getIdFromGraphQLId(project.id);
return project?.id || null;
}, },
}, },
}; };
...@@ -184,7 +169,7 @@ export default { ...@@ -184,7 +169,7 @@ 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="useGraphql ? selectedProjects[0].avatarUrl : selectedProjects[0].avatar_url" :src="selectedProjects[0].avatarUrl"
:entity-id="getEntityId(selectedProjects[0])" :entity-id="getEntityId(selectedProjects[0])"
:entity-name="selectedProjects[0].name" :entity-name="selectedProjects[0].name"
:size="16" :size="16"
...@@ -213,7 +198,7 @@ export default { ...@@ -213,7 +198,7 @@ export default {
:size="16" :size="16"
:entity-id="getEntityId(project)" :entity-id="getEntityId(project)"
:entity-name="project.name" :entity-name="project.name"
:src="useGraphql ? project.avatarUrl : project.avatar_url" :src="project.avatarUrl"
shape="rect" shape="rect"
/> />
{{ project.name }} {{ project.name }}
......
...@@ -14,10 +14,6 @@ export const scatterChartLineProps = { ...@@ -14,10 +14,6 @@ export const scatterChartLineProps = {
}, },
}; };
export const LAST_ACTIVITY_AT = 'last_activity_at';
export const SIMILARITY_ORDER = 'similarity';
export const DATE_RANGE_LIMIT = 180; export const DATE_RANGE_LIMIT = 180;
export const OFFSET_DATE_BY_ONE = 1; export const OFFSET_DATE_BY_ONE = 1;
......
...@@ -113,7 +113,7 @@ module Gitlab ...@@ -113,7 +113,7 @@ module Gitlab
def project_data_attributes(project) def project_data_attributes(project)
{ {
id: project.id, id: 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
......
...@@ -24,6 +24,7 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -24,6 +24,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import * as mockData from '../mock_data'; import * as mockData from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -62,6 +63,11 @@ const mocks = { ...@@ -62,6 +63,11 @@ const mocks = {
$toast: { $toast: {
show: jest.fn(), show: jest.fn(),
}, },
$apollo: {
query: jest.fn().mockResolvedValue({
data: { group: { projects: { nodes: [] } } },
}),
},
}; };
function mockRequiredRoutes(mockAdapter) { function mockRequiredRoutes(mockAdapter) {
...@@ -387,25 +393,6 @@ describe('Cycle Analytics component', () => { ...@@ -387,25 +393,6 @@ describe('Cycle Analytics component', () => {
}); });
}); });
describe('when analyticsSimilaritySearch feature flag is on', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasAnalyticsSimilaritySearch: true,
},
});
});
it('uses similarity as the order param', () => {
displaysProjectsDropdownFilter(true);
expect(wrapper.find(ProjectsDropdownFilter).props().queryParams.order_by).toEqual(
'similarity',
);
});
});
it('displays the date range picker', () => { it('displays the date range picker', () => {
displaysDateRangePicker(true); displaysDateRangePicker(true);
}); });
...@@ -632,7 +619,7 @@ describe('Cycle Analytics component', () => { ...@@ -632,7 +619,7 @@ describe('Cycle Analytics component', () => {
project_ids: null, project_ids: null,
}; };
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id); const selectedProjectIds = mockData.selectedProjects.map(({ id }) => getIdFromGraphQLId(id));
beforeEach(async () => { beforeEach(async () => {
commonUtils.historyPushState = jest.fn(); commonUtils.historyPushState = jest.fn();
......
...@@ -251,13 +251,13 @@ export const rawDurationMedianData = [ ...@@ -251,13 +251,13 @@ export const rawDurationMedianData = [
export const selectedProjects = [ export const selectedProjects = [
{ {
id: 1, id: 'gid://gitlab/Project/1',
name: 'cool project', name: 'cool project',
pathWithNamespace: 'group/cool-project', pathWithNamespace: 'group/cool-project',
avatarUrl: null, avatarUrl: null,
}, },
{ {
id: 2, id: 'gid://gitlab/Project/2',
name: 'another cool project', name: 'another cool project',
pathWithNamespace: 'group/another-cool-project', pathWithNamespace: 'group/another-cool-project',
avatarUrl: null, avatarUrl: null,
......
...@@ -2,15 +2,9 @@ import { mount } from '@vue/test-utils'; ...@@ -2,15 +2,9 @@ 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 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 { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import Api from '~/api';
jest.mock('~/api', () => ({ const projects = [
groupProjects: jest.fn(),
}));
const mockGraphqlProjects = [
{ {
id: 'gid://gitlab/Project/1', id: 'gid://gitlab/Project/1',
name: 'Gitlab Test', name: 'Gitlab Test',
...@@ -31,28 +25,10 @@ const mockGraphqlProjects = [ ...@@ -31,28 +25,10 @@ const mockGraphqlProjects = [
}, },
]; ];
const projects = [
{
id: 1,
name: 'foo',
avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
},
{
id: 2,
name: 'foobar',
avatar_url: null,
},
{
id: 3,
name: 'foooooooo',
avatar_url: null,
},
];
const defaultMocks = { const defaultMocks = {
$apollo: { $apollo: {
query: jest.fn().mockResolvedValue({ query: jest.fn().mockResolvedValue({
data: { group: { projects: { nodes: mockGraphqlProjects } } }, data: { group: { projects: { nodes: projects } } },
}), }),
}, },
}; };
...@@ -98,37 +74,9 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -98,37 +74,9 @@ describe('ProjectsDropdownFilter component', () => {
.find('button') .find('button')
.trigger('click'); .trigger('click');
describe('when using the REST API', () => { describe('queryParams are applied when fetching data', () => {
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: {
first: 50, first: 50,
includeSubgroups: true, includeSubgroups: true,
...@@ -156,295 +104,127 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -156,295 +104,127 @@ describe('ProjectsDropdownFilter component', () => {
}); });
describe('when passed a an array of defaultProject as prop', () => { describe('when passed a an array of defaultProject as prop', () => {
describe('when using the RESTP API', () => { beforeEach(() => {
beforeEach(() => { createComponent({
Api.groupProjects.mockImplementation((groupId, term, options, callback) => { defaultProjects: [projects[0]],
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 using the GraphQL API', () => { describe('when multiSelect is false', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({ multiSelect: false });
useGraphql: true, });
defaultProjects: [mockGraphqlProjects[0]],
});
});
it("displays the defaultProject's name", () => { describe('displays the correct information', () => {
expect(findDropdownButton().text()).toContain(mockGraphqlProjects[0].name); it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
}); });
it("renders the defaultProject's avatar", () => { it('renders an avatar when the project has an avatarUrl', () => {
expect(findDropdownButtonAvatar().exists()).toBe(true); expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
}); });
it("renders an identicon when the project doesn't have an avatarUrl", () => {
it('marks the defaultProject as selected', () => { expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
expect(findDropdownAtIndex(0).props('isChecked')).toBe(true); expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
}); });
}); });
});
describe('when multiSelect is false', () => { describe('on project click', () => {
describe('when using the RESTP API', () => { it('should emit the "selected" event with the selected project', () => {
beforeEach(() => { selectDropdownItemAtIndex(0);
Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
createComponent({ multiSelect: false }); expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]);
}); });
describe('displays the correct information', () => { it('should change selection when new project is clicked', () => {
it('contains 3 items', () => { selectDropdownItemAtIndex(1);
expect(findDropdownItems()).toHaveLength(3);
});
it('renders an avatar when the project has an avatar_url', () => { expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]);
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);
});
}); });
describe('on project click', () => { it('selection should be emptied when a project is deselected', () => {
it('should emit the "selected" event with the selected project', () => { selectDropdownItemAtIndex(0); // Select the item
selectDropdownItemAtIndex(0); selectDropdownItemAtIndex(0); // deselect it
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);
});
});
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', () => { expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
beforeEach(() => {
createComponent({ multiSelect: false, useGraphql: true });
}); });
describe('displays the correct information', () => { it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
it('contains 3 items', () => { selectDropdownItemAtIndex(0);
expect(findDropdownItems()).toHaveLength(3);
});
it('renders an avatar when the project has an avatarUrl', () => { await wrapper.vm.$nextTick().then(() => {
expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); 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);
});
}); });
describe('on project click', () => { it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
it('should emit the "selected" event with the selected project', () => { selectDropdownItemAtIndex(1);
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[0]]]]);
});
it('should change selection when new project is clicked', () => {
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[1]]]]);
});
it('selection should be emptied when a project is deselected', () => { await wrapper.vm.$nextTick().then(() => {
selectDropdownItemAtIndex(0); // Select the item expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
selectDropdownItemAtIndex(0); // deselect it expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
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', () => {
describe('when using the RESTP API', () => { beforeEach(() => {
beforeEach(() => { createComponent({ multiSelect: true });
Api.groupProjects.mockImplementation((groupId, term, options, callback) => { });
callback(projects);
});
createComponent({ multiSelect: true }); describe('displays the correct information', () => {
it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
}); });
describe('displays the correct information', () => { it('renders an avatar when the project has an avatarUrl', () => {
it('contains 3 items', () => { expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
expect(findDropdownItems()).toHaveLength(3); expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
});
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);
});
}); });
describe('on project click', () => { it("renders an identicon when the project doesn't have an avatarUrl", () => {
it('should add to selection when new project is clicked', () => { expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
selectDropdownItemAtIndex(0); expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([
[[projects[0]]],
[[projects[0], projects[1]]],
]);
});
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('when using the GraphQl API', () => { describe('on project click', () => {
beforeEach(() => { it('should add to selection when new project is clicked', () => {
createComponent({ multiSelect: true, useGraphql: true }); selectDropdownItemAtIndex(0);
}); selectDropdownItemAtIndex(1);
describe('displays the correct information', () => {
it('contains 3 items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders an avatar when the project has an avatarUrl', () => { expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[projects[0], projects[1]]]]);
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);
});
}); });
describe('on project click', () => { it('should remove from selection when clicked again', () => {
it('should add to selection when new project is clicked', () => { selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(0); selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([
[[mockGraphqlProjects[0]]],
[[mockGraphqlProjects[0], mockGraphqlProjects[1]]],
]);
});
it('should remove from selection when clicked again', () => { expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
selectDropdownItemAtIndex(0); });
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[mockGraphqlProjects[0]]], [[]]]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => { it('renders the correct placeholder text when multiple projects are selected', async () => {
selectDropdownItemAtIndex(0); selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1); selectDropdownItemAtIndex(1);
await wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick().then(() => {
expect(findDropdownButton().text()).toBe('2 projects selected'); expect(findDropdownButton().text()).toBe('2 projects selected');
});
}); });
}); });
}); });
......
...@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do ...@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do
describe 'optional `project_ids`' do describe 'optional `project_ids`' do
context 'when `project_ids` is not empty' do context 'when `project_ids` is not empty' do
def json_project(project) def json_project(project)
{ id: project.id, { id: 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 }.to_json avatar_url: project.avatar_url }.to_json
......
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