Commit 6f1d482f authored by Simon Knox's avatar Simon Knox

Merge branch 'remove-boards-store-from-board-selector' into 'master'

Remove boardStore from GraphQL boards selector [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!53202
parents 99fa6ac7 6b3c37d8
......@@ -5,8 +5,8 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { getParameterByName } from '~/lib/utils/common_utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
import { fullLabelId, fullBoardId } from '../boards_util';
import { formType } from '../constants';
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
......@@ -26,12 +26,6 @@ const boardDefaults = {
hide_closed_list: false,
};
const formType = {
new: 'new',
delete: 'delete',
edit: 'edit',
};
export default {
i18n: {
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
......@@ -100,11 +94,14 @@ export default {
type: Object,
required: true,
},
currentPage: {
type: String,
required: true,
},
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
......@@ -256,7 +253,7 @@ export default {
}
},
cancel() {
boardsStore.showPage('');
this.$emit('cancel');
},
resetFormState() {
if (this.isNewForm) {
......
......@@ -12,11 +12,12 @@ import {
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectQuery from '../graphql/project_boards.query.graphql';
import groupQuery from '../graphql/group_boards.query.graphql';
import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
......@@ -35,6 +36,7 @@ export default {
directives: {
GlModalDirective,
},
inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
currentBoard: {
type: Object,
......@@ -99,12 +101,11 @@ export default {
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
store: boardsStore,
filterTerm: '',
currentPage: '',
};
},
computed: {
......@@ -114,16 +115,13 @@ export default {
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
currentPage() {
return this.state.currentPage;
},
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
return this.state.currentBoard;
return this.currentBoard;
},
showDelete() {
return this.boards.length > 1;
......@@ -148,11 +146,17 @@ export default {
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
eventHub.$on('showBoardModal', this.showPage);
},
beforeDestroy() {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
showPage(page) {
boardsStore.showPage(page);
this.currentPage = page;
},
cancel() {
this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
......@@ -161,7 +165,7 @@ export default {
this.$apollo.addSmartQuery('boards', {
variables() {
return { fullPath: this.state.endpoints.fullPath };
return { fullPath: this.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
......@@ -179,8 +183,10 @@ export default {
});
this.loadingRecentBoards = true;
boardsStore
.recentBoards()
// Follow up to fetch recent boards using GraphQL
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985
axios
.get(this.recentBoardsEndpoint)
.then((res) => {
this.recentBoards = res.data;
})
......@@ -346,6 +352,8 @@ export default {
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
:current-page="currentPage"
@cancel="cancel"
/>
</span>
</div>
......
<script>
import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectQuery from '../graphql/project_boards.query.graphql';
import groupQuery from '../graphql/group_boards.query.graphql';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
name: 'BoardsSelector',
components: {
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
},
directives: {
GlModalDirective,
},
props: {
currentBoard: {
type: Object,
required: true,
},
throttleDuration: {
type: Number,
default: 200,
required: false,
},
boardBaseUrl: {
type: String,
required: true,
},
hasMissingBoards: {
type: Boolean,
required: true,
},
canAdminBoard: {
type: Boolean,
required: true,
},
multipleIssueBoardsAvailable: {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
},
weights: {
type: Array,
required: true,
},
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
hasScrollFade: false,
loadingBoards: 0,
loadingRecentBoards: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
store: boardsStore,
filterTerm: '',
};
},
computed: {
parentType() {
return this.groupId ? 'group' : 'project';
},
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
currentPage() {
return this.state.currentPage;
},
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
return this.state.currentBoard;
},
showDelete() {
return this.boards.length > 1;
},
scrollFadeClass() {
return {
'fade-out': !this.hasScrollFade,
};
},
showRecentSection() {
return (
this.recentBoards.length &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
filteredBoards() {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
},
methods: {
showPage(page) {
boardsStore.showPage(page);
},
cancel() {
this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
}
this.$apollo.addSmartQuery('boards', {
variables() {
return { fullPath: this.state.endpoints.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
},
loadingKey: 'loadingBoards',
update(data) {
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
});
this.loadingRecentBoards = true;
boardsStore
.recentBoards()
.then((res) => {
this.recentBoards = res.data;
})
.catch((err) => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
this.recentBoards = []; // recent boards are empty
return;
}
throw err;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {})
.finally(() => {
this.loadingRecentBoards = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
if (!content) {
return false;
}
const currentPosition = this.contentClientHeight + content.scrollTop;
return currentPosition < this.maxPosition;
},
initScrollFade() {
const { content } = this.$refs;
if (!content) {
return;
}
this.scrollFadeInitialized = true;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
setScrollFade() {
if (!this.scrollFadeInitialized) this.initScrollFade();
this.hasScrollFade = this.isScrolledUp();
},
},
};
</script>
<template>
<div class="boards-switcher js-boards-selector gl-mr-3">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-dropdown
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
:text="board.name"
@show="loadBoards"
>
<p class="gl-new-dropdown-header-top" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</p>
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
<div
v-if="!loading"
ref="content"
data-qa-selector="boards_dropdown_content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
<gl-dropdown-item
v-show="filteredBoards.length === 0"
class="gl-pointer-events-none text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="showRecentSection">
{{ __('Recent') }}
</gl-dropdown-section-header>
<template v-if="showRecentSection">
<gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
</gl-dropdown-item>
</template>
<gl-dropdown-divider v-if="showRecentSection" />
<gl-dropdown-section-header v-if="showRecentSection">
{{ __('All') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
</gl-dropdown-item>
</div>
<div
v-show="filteredBoards.length > 0"
class="dropdown-content-faded-mask"
:class="scrollFadeClass"
></div>
<gl-loading-icon v-if="loading" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
<gl-dropdown-item
v-if="multipleIssueBoardsAvailable"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
>
{{ s__('IssueBoards|Create new board') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="showDelete"
v-gl-modal-directive="'board-config-modal'"
class="text-danger js-delete-board"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
</gl-dropdown-item>
</div>
</gl-dropdown>
<board-form
v-if="currentPage"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
:current-page="state.currentPage"
@cancel="cancel"
/>
</span>
</div>
</template>
......@@ -21,6 +21,12 @@ export const ListTypeTitles = {
label: __('Label'),
};
export const formType = {
new: 'new',
delete: 'delete',
edit: 'edit',
};
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
......
......@@ -358,5 +358,6 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
});
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapGetters } from 'vuex';
import store from '~/boards/stores';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
Vue.use(VueApollo);
......@@ -16,11 +20,15 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
BoardsSelectorDeprecated,
},
mixins: [glFeatureFlagMixin()],
apolloProvider,
store,
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint,
},
data() {
const { dataset } = boardsSwitcherElement;
......@@ -39,8 +47,16 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
computed: {
...mapGetters(['shouldUseGraphQL']),
},
render(createElement) {
return createElement(BoardsSelector, {
if (this.shouldUseGraphQL) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
}
return createElement(BoardsSelectorDeprecated, {
props: this.boardsSelectorProps,
});
},
......
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formType } from '~/boards/constants';
export default {
components: {
......@@ -38,8 +40,9 @@ export default {
},
},
methods: {
showPage(page) {
return this.boardsStore.showPage(page);
showPage() {
eventHub.$emit('showBoardModal', formType.edit);
return this.boardsStore.showPage(formType.edit);
},
},
};
......@@ -53,10 +56,9 @@ export default {
:title="tooltipTitle"
:class="{ 'dot-highlight': hasScope }"
data-qa-selector="boards_config_button"
@click.prevent="showPage('edit')"
@click.prevent="showPage"
>
{{ buttonText }}
</gl-button>
</div>
</template>
};
......@@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import { formType } from '~/boards/constants';
import BoardForm from '~/boards/components/board_form.vue';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
......@@ -35,6 +35,7 @@ const defaultProps = {
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
describe('BoardForm', () => {
......@@ -75,14 +76,12 @@ describe('BoardForm', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
boardsStore.state.currentPage = null;
mutate = null;
});
describe('when user can not admin the board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
createComponent();
createComponent({ currentPage: formType.new });
});
it('hides modal footer when user is not a board admin', () => {
......@@ -100,8 +99,7 @@ describe('BoardForm', () => {
describe('when user can admin the board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.new });
});
it('shows modal footer when user is a board admin', () => {
......@@ -118,13 +116,9 @@ describe('BoardForm', () => {
});
describe('when creating a new board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
});
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.new });
});
it('clears the form', () => {
......@@ -165,7 +159,7 @@ describe('BoardForm', () => {
});
it('does not call API if board name is empty', async () => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.new });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
......@@ -174,7 +168,7 @@ describe('BoardForm', () => {
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
......@@ -194,7 +188,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
......@@ -209,13 +203,9 @@ describe('BoardForm', () => {
});
describe('when editing a board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'edit';
});
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.edit });
});
it('clears the form', () => {
......@@ -247,7 +237,7 @@ describe('BoardForm', () => {
},
});
window.location = new URL('https://test/boards/1');
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
......@@ -273,7 +263,7 @@ describe('BoardForm', () => {
},
});
window.location = new URL('https://test/boards/1?group_by=epic');
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
......@@ -294,7 +284,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
......@@ -308,24 +298,20 @@ describe('BoardForm', () => {
});
describe('when deleting a board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'delete';
});
it('passes correct primary action text and variant', () => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findModalActionPrimary().text).toBe('Delete');
expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
});
it('renders delete confirmation message', () => {
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findDeleteConfirmation().exists()).toBe(true);
});
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
mutate = jest.fn().mockResolvedValue({});
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.delete });
findModal().vm.$emit('primary');
await waitForPromises();
......@@ -343,7 +329,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true });
createComponent({ canAdminBoard: true, currentPage: formType.delete });
findModal().vm.$emit('primary');
await waitForPromises();
......
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector_deprecated.vue';
import boardsStore from '~/boards/stores/boards_store';
const throttleDuration = 1;
function boardGenerator(n) {
return new Array(n).fill().map((board, index) => {
const id = `${index}`;
const name = `board${id}`;
return {
id,
name,
};
});
}
describe('BoardsSelector', () => {
let wrapper;
let allBoardsResponse;
let recentBoardsResponse;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
const fillSearchBox = (filterTerm) => {
const searchBox = wrapper.find({ ref: 'searchBox' });
const searchBoxInput = searchBox.find('input');
searchBoxInput.setValue(filterTerm);
searchBoxInput.trigger('input');
};
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findDropdown = () => wrapper.find(GlDropdown);
beforeEach(() => {
const $apollo = {
queries: {
boards: {
loading: false,
},
},
};
boardsStore.setEndpoints({
boardsEndpoint: '',
recentBoardsEndpoint: '',
listsEndpoint: '',
bulkUpdatePath: '',
boardId: '',
});
allBoardsResponse = Promise.resolve({
data: {
group: {
boards: {
edges: boards.map((board) => ({ node: board })),
},
},
},
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
});
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
wrapper = mount(BoardsSelector, {
propsData: {
throttleDuration,
currentBoard: {
id: 1,
name: 'Development',
milestone_id: null,
weight: null,
assignee_id: null,
labels: [],
},
boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false,
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
mocks: { $apollo },
attachTo: document.body,
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
wrapper.setData({
[options.loadingKey]: true,
});
});
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('loading', () => {
// we are testing loading state, so don't resolve responses until after the tests
afterEach(() => {
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
it('shows loading spinner', () => {
expect(getDropdownHeaders()).toHaveLength(0);
expect(getDropdownItems()).toHaveLength(0);
expect(getLoadingIcon().exists()).toBe(true);
});
});
describe('loaded', () => {
beforeEach(async () => {
await wrapper.setData({
loadingBoards: false,
});
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
it('hides loading spinner', () => {
expect(getLoadingIcon().exists()).toBe(false);
});
describe('filtering', () => {
beforeEach(() => {
wrapper.setData({
boards,
});
return nextTick();
});
it('shows all boards without filtering', () => {
expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
});
it('shows only matching boards when filtering', () => {
const filterTerm = 'board1';
const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
fillSearchBox(filterTerm);
return nextTick().then(() => {
expect(getDropdownItems()).toHaveLength(expectedCount);
});
});
it('shows message if there are no matching boards', () => {
fillSearchBox('does not exist');
return nextTick().then(() => {
expect(getDropdownItems()).toHaveLength(0);
expect(wrapper.text().includes('No matching boards found')).toBe(true);
});
});
});
describe('recent boards section', () => {
it('shows only when boards are greater than 10', () => {
wrapper.setData({
boards,
});
return nextTick().then(() => {
expect(getDropdownHeaders()).toHaveLength(2);
});
});
it('does not show when boards are less than 10', () => {
wrapper.setData({
boards: boards.slice(0, 5),
});
return nextTick().then(() => {
expect(getDropdownHeaders()).toHaveLength(0);
});
});
it('does not show when recentBoards api returns empty array', () => {
wrapper.setData({
recentBoards: [],
});
return nextTick().then(() => {
expect(getDropdownHeaders()).toHaveLength(0);
});
});
it('does not show when search is active', () => {
fillSearchBox('Random string');
return nextTick().then(() => {
expect(getDropdownHeaders()).toHaveLength(0);
});
});
});
});
});
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import boardsStore from '~/boards/stores/boards_store';
const throttleDuration = 1;
......@@ -23,6 +24,7 @@ describe('BoardsSelector', () => {
let wrapper;
let allBoardsResponse;
let recentBoardsResponse;
let mock;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
......@@ -39,6 +41,7 @@ describe('BoardsSelector', () => {
const findDropdown = () => wrapper.find(GlDropdown);
beforeEach(() => {
mock = new MockAdapter(axios);
const $apollo = {
queries: {
boards: {
......@@ -47,14 +50,6 @@ describe('BoardsSelector', () => {
},
};
boardsStore.setEndpoints({
boardsEndpoint: '',
recentBoardsEndpoint: '',
listsEndpoint: '',
bulkUpdatePath: '',
boardId: '',
});
allBoardsResponse = Promise.resolve({
data: {
group: {
......@@ -68,9 +63,6 @@ describe('BoardsSelector', () => {
data: recentBoards,
});
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
wrapper = mount(BoardsSelector, {
propsData: {
throttleDuration,
......@@ -95,6 +87,10 @@ describe('BoardsSelector', () => {
},
mocks: { $apollo },
attachTo: document.body,
provide: {
fullPath: '',
recentBoardsEndpoint: `${TEST_HOST}/recent`,
},
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
......@@ -103,6 +99,8 @@ describe('BoardsSelector', () => {
});
});
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
......@@ -110,6 +108,7 @@ describe('BoardsSelector', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
describe('loading', () => {
......@@ -133,7 +132,8 @@ describe('BoardsSelector', () => {
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
it('hides loading spinner', () => {
it('hides loading spinner', async () => {
await wrapper.vm.$nextTick();
expect(getLoadingIcon().exists()).toBe(false);
});
......
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