Commit 2e479619 authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Nicolò Maria Mezzopera

Added test spec for board form

- removed unused specs
- removed unused utilities
- added new cases to the form spec
parent c02edde6
import { sortBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
......@@ -108,10 +109,20 @@ export function moveIssueListHelper(issue, fromList, toList) {
return updatedIssue;
}
export function getBoardsPath(endpoint, board) {
const path = `${endpoint}${board.id ? `/${board.id}` : ''}.json`;
if (board.id) {
return axios.put(path, { board });
}
return axios.post(path, { board });
}
export default {
getMilestone,
formatIssue,
formatListIssues,
fullBoardId,
fullLabelId,
getBoardsPath,
};
<script>
import { GlModal } from '@gitlab/ui';
import { pick } from 'lodash';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import { fullBoardId, getBoardsPath } from '../boards_util';
import BoardConfigurationOptions from './board_configuration_options.vue';
import createBoardMutation from '../graphql/board.mutation.graphql';
const boardDefaults = {
id: false,
......@@ -81,11 +84,19 @@ export default {
required: false,
default: false,
},
currentBoard: {
type: Object,
required: true,
},
},
inject: {
endpoints: {
default: {},
},
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
currentBoard: boardsStore.state.currentBoard,
currentPage: boardsStore.state.currentPage,
isLoading: false,
};
......@@ -143,6 +154,15 @@ export default {
text: this.$options.i18n.cancelButtonText,
};
},
boardPayload() {
const { assignee, milestone, labels } = this.board;
return {
...this.board,
assignee_id: assignee?.id,
milestone_id: milestone?.id,
label_ids: labels.length ? labels.map(b => b.id) : [''],
};
},
},
mounted() {
this.resetFormState();
......@@ -151,6 +171,31 @@ export default {
}
},
methods: {
callBoardMutation(id) {
return this.$apollo.mutate({
mutation: createBoardMutation,
variables: {
...pick(this.boardPayload, ['hideClosedList', 'hideBacklogList']),
id,
},
});
},
async updateBoard() {
const responses = await Promise.all([
// Remove unnecessary REST API call when https://gitlab.com/gitlab-org/gitlab/-/issues/282299#note_462996301 is resolved
getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload),
this.callBoardMutation(fullBoardId(this.boardPayload.id)),
]);
return responses[0].data;
},
async createBoard() {
// TODO: change this to use `createBoard` mutation https://gitlab.com/gitlab-org/gitlab/-/issues/292466 is resolved
const boardData = await getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload);
await this.callBoardMutation(fullBoardId(boardData.data.id));
return boardData.data || boardData;
},
submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
......@@ -166,21 +211,9 @@ export default {
this.isLoading = false;
});
} else {
boardsStore
.createBoard(this.board)
.then(resp => {
// This handles 2 use cases
// - In create call we only get one parameter, the new board
// - In update call, due to Promise.all, we get REST response in
// array index 0
if (Array.isArray(resp)) {
return resp[0].data;
}
return resp.data ? resp.data : resp;
})
const boardAction = this.boardPayload.id ? this.updateBoard : this.createBoard;
boardAction()
.then(data => {
this.isLoading = false;
visitUrl(data.board_path);
})
.catch(() => {
......@@ -219,9 +252,11 @@ export default {
@close="cancel"
@hide.prevent
>
<p v-if="isDeleteForm">{{ $options.i18n.deleteConfirmationMessage }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="gl-mb-5">
<p v-if="isDeleteForm" data-testid="delete-confirmation-message">
{{ $options.i18n.deleteConfirmationMessage }}
</p>
<form v-else class="js-board-config-modal" data-testid="board-form-wrapper" @submit.prevent>
<div v-if="!readonly" class="gl-mb-5" data-testid="board-form">
<label class="gl-font-weight-bold gl-font-lg" for="board-new-name">
{{ $options.i18n.titleFieldLabel }}
</label>
......
......@@ -345,6 +345,7 @@ export default {
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
/>
</span>
</div>
......
......@@ -349,5 +349,8 @@ export default () => {
toggleEpicsSwimlanes();
}
mountMultipleBoardsSwitcher();
mountMultipleBoardsSwitcher({
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
});
};
......@@ -10,7 +10,7 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
export default (endpoints = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
el: boardsSwitcherElement,
......@@ -35,6 +35,9 @@ export default () => {
return { boardsSelectorProps };
},
provide: {
endpoints,
},
render(createElement) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
......
/* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */
/* global ListIssue */
import { sortBy, pick } from 'lodash';
import { sortBy } from 'lodash';
import Vue from 'vue';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import {
......@@ -21,8 +21,6 @@ import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone';
import createBoardMutation from '../graphql/board.mutation.graphql';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
......@@ -759,52 +757,6 @@ const boardsStore = {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
createBoard(board) {
const boardPayload = { ...board };
boardPayload.label_ids = (board.labels || []).map(b => b.id);
if (boardPayload.label_ids.length === 0) {
boardPayload.label_ids = [''];
}
if (boardPayload.assignee) {
boardPayload.assignee_id = boardPayload.assignee.id;
}
if (boardPayload.milestone) {
boardPayload.milestone_id = boardPayload.milestone.id;
}
if (boardPayload.id) {
const input = {
...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
id: this.generateBoardGid(boardPayload.id),
};
return Promise.all([
axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }),
gqlClient.mutate({
mutation: createBoardMutation,
variables: input,
}),
]);
}
return axios
.post(this.generateBoardsPath(), { board: boardPayload })
.then(resp => resp.data)
.then(data => {
gqlClient.mutate({
mutation: createBoardMutation,
variables: {
...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
id: this.generateBoardGid(data.id),
},
});
return data;
});
},
deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id));
},
......
---
title: Migrate `createBoard` away from boardStore
merge_request: 49450
author:
type: changed
......@@ -234,10 +234,6 @@ export default {
notImplemented();
},
createBoard: () => {
notImplemented();
},
deleteBoard: () => {
notImplemented();
},
......
......@@ -4,9 +4,6 @@ export const RECEIVE_AVAILABLE_BOARDS_ERROR = 'RECEIVE_AVAILABLE_BOARDS_ERROR';
export const REQUEST_RECENT_BOARDS = 'REQUEST_RECENT_BOARDS';
export const RECEIVE_RECENT_BOARDS_SUCCESS = 'RECEIVE_RECENT_BOARDS_SUCCESS';
export const RECEIVE_RECENT_BOARDS_ERROR = 'RECEIVE_RECENT_BOARDS_ERROR';
export const REQUEST_ADD_BOARD = 'REQUEST_ADD_BOARD';
export const RECEIVE_ADD_BOARD_SUCCESS = 'RECEIVE_ADD_BOARD_SUCCESS';
export const RECEIVE_ADD_BOARD_ERROR = 'RECEIVE_ADD_BOARD_ERROR';
export const REQUEST_REMOVE_BOARD = 'REQUEST_REMOVE_BOARD';
export const RECEIVE_REMOVE_BOARD_SUCCESS = 'RECEIVE_REMOVE_BOARD_SUCCESS';
export const RECEIVE_REMOVE_BOARD_ERROR = 'RECEIVE_REMOVE_BOARD_ERROR';
......
......@@ -40,18 +40,6 @@ export default {
notImplemented();
},
[mutationTypes.REQUEST_ADD_BOARD]: () => {
notImplemented();
},
[mutationTypes.RECEIVE_ADD_BOARD_SUCCESS]: () => {
notImplemented();
},
[mutationTypes.RECEIVE_ADD_BOARD_ERROR]: () => {
notImplemented();
},
[mutationTypes.REQUEST_REMOVE_BOARD]: () => {
notImplemented();
},
......
......@@ -472,9 +472,8 @@ RSpec.describe 'Scoped issue boards', :js do
end
def expect_dot_highlight(button_title)
button = first('.filter-dropdown-container .btn.gl-button')
button = first('.filter-dropdown-container .btn.gl-button.dot-highlight')
expect(button.text).to include(button_title)
expect(button[:class]).to include('dot-highlight')
expect(button['title']).to include('This board\'s scope is reduced')
end
......
......@@ -351,10 +351,6 @@ describe('fetchRecentBoards', () => {
expectNotImplemented(actions.fetchRecentBoards);
});
describe('createBoard', () => {
expectNotImplemented(actions.createBoard);
});
describe('deleteBoard', () => {
expectNotImplemented(actions.deleteBoard);
});
......
......@@ -60,18 +60,6 @@ describe('RECEIVE_RECENT_BOARDS_ERROR', () => {
expectNotImplemented(mutations.RECEIVE_RECENT_BOARDS_ERROR);
});
describe('REQUEST_ADD_BOARD', () => {
expectNotImplemented(mutations.REQUEST_ADD_BOARD);
});
describe('RECEIVE_ADD_BOARD_SUCCESS', () => {
expectNotImplemented(mutations.RECEIVE_ADD_BOARD_SUCCESS);
});
describe('RECEIVE_ADD_BOARD_ERROR', () => {
expectNotImplemented(mutations.RECEIVE_ADD_BOARD_ERROR);
});
describe('REQUEST_REMOVE_BOARD', () => {
expectNotImplemented(mutations.REQUEST_REMOVE_BOARD);
});
......
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import boardsStore, { gqlClient } from '~/boards/stores/boards_store';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate } from './mock_data';
......@@ -456,118 +456,6 @@ describe('boardsStore', () => {
});
});
describe('createBoard', () => {
const labelIds = ['first label', 'second label'];
const assigneeId = 'as sign ee';
const milestoneId = 'vegetable soup';
const board = {
labels: labelIds.map(id => ({ id })),
assignee: { id: assigneeId },
milestone: { id: milestoneId },
};
describe('for existing board', () => {
const id = 'skate-board';
const url = `${endpoints.boardsEndpoint}/${id}.json`;
const expectedRequest = expect.objectContaining({
data: JSON.stringify({
board: {
...board,
id,
label_ids: labelIds,
assignee_id: assigneeId,
milestone_id: milestoneId,
},
}),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPut(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
});
it('makes a request to update the board', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = [
expect.objectContaining({ data: dummyResponse }),
expect.objectContaining({}),
];
return expect(
boardsStore.createBoard({
...board,
id,
}),
)
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(
boardsStore.createBoard({
...board,
id,
}),
)
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('for new board', () => {
const url = `${endpoints.boardsEndpoint}.json`;
const expectedRequest = expect.objectContaining({
data: JSON.stringify({
board: {
...board,
label_ids: labelIds,
assignee_id: assigneeId,
milestone_id: milestoneId,
},
}),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPost(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
});
it('makes a request to create a new board', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = dummyResponse;
return expect(boardsStore.createBoard(board))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.createBoard(board))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
});
describe('deleteBoard', () => {
const id = 'capsized';
const url = `${endpoints.boardsEndpoint}/${id}.json`;
......
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'jest/helpers/test_constants';
import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import BoardScope from 'ee_component/boards/components/board_scope.vue';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import boardForm from '~/boards/components/board_form.vue';
import BoardForm from '~/boards/components/board_form.vue';
import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue';
import createBoardMutation from '~/boards/graphql/board.mutation.graphql';
describe('board_form.vue', () => {
let wrapper;
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
const propsData = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
};
const currentBoard = {
id: 1,
name: 'test',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
};
const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
};
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
};
const endpoints = {
boardsEndpoint: 'test-endpoint',
};
const mutate = jest.fn().mockResolvedValue({});
describe('BoardForm', () => {
let wrapper;
let axiosMock;
const findModal = () => wrapper.find(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findForm = () => wrapper.find('[data-testid="board-form"]');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findConfigurationOptions = () => wrapper.find(BoardConfigurationOptions);
const findBoardScope = () => wrapper.find(BoardScope);
const findInput = () => wrapper.find('#board-new-name');
const createComponent = (props, data) => {
wrapper = shallowMount(BoardForm, {
propsData: { ...defaultProps, ...props },
data() {
return {
...data,
};
},
provide: {
endpoints,
},
mocks: {
$apollo: {
mutate,
},
},
attachToDocument: true,
});
};
beforeEach(() => {
boardsStore.state.currentPage = 'edit';
wrapper = mount(boardForm, { propsData });
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
axiosMock.restore();
boardsStore.state.currentPage = null;
});
describe('when user can not admin the board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
createComponent();
});
it('hides modal footer when user is not a board admin', () => {
expect(findModal().attributes('hide-footer')).toBeDefined();
});
it('displays board scope title', () => {
expect(findModal().attributes('title')).toBe('Board scope');
});
it('does not display a form', () => {
expect(findForm().exists()).toBe(false);
});
});
describe('methods', () => {
describe('cancel', () => {
it('resets currentPage', () => {
wrapper.vm.cancel();
expect(boardsStore.state.currentPage).toBe('');
describe('when user can admin the board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
createComponent({ canAdminBoard: true });
});
it('shows modal footer when user is a board admin', () => {
expect(findModal().attributes('hide-footer')).toBeUndefined();
});
it('displays a form', () => {
expect(findForm().exists()).toBe(true);
});
it('focuses an input field', async () => {
expect(document.activeElement).toBe(wrapper.vm.$refs.name);
});
});
describe('when creating a new board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'new';
});
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true });
});
it('clears the form', () => {
expect(findConfigurationOptions().props('board')).toEqual(boardDefaults);
});
it('shows a correct title about creating a board', () => {
expect(findModal().attributes('title')).toBe('Create new board');
});
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Create board');
expect(findModalActionPrimary().attributes[0].variant).toBe('success');
});
it('does not render delete confirmation message', () => {
expect(findDeleteConfirmation().exists()).toBe(false);
});
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
it('passes a true isNewForm prop to BoardConfigurationOptions component', () => {
expect(findConfigurationOptions().props('isNewForm')).toBe(true);
});
});
it('passes a correct collapseScope property to BoardScope component on scoped board', async () => {
createComponent({ canAdminBoard: true, scopedIssueBoardFeatureEnabled: true });
await waitForPromises();
expect(findBoardScope().props('collapseScope')).toBe(true);
});
describe('when submitting a create event', () => {
beforeEach(() => {
const url = `${endpoints.boardsEndpoint}.json`;
axiosMock.onPost(url).reply(200, { id: '2', board_path: 'new path' });
});
it('does not call API if board name is empty', async () => {
createComponent({ canAdminBoard: true });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(mutate).not.toHaveBeenCalled();
});
it('calls REST and GraphQL API and redirects to correct page', async () => {
createComponent({ canAdminBoard: true });
findInput().value = 'Test name';
findInput().trigger('input');
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(axiosMock.history.post[0].data).toBe(
JSON.stringify({ board: { ...boardDefaults, name: 'test', label_ids: [''] } }),
);
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
variables: {
id: 'gid://gitlab/Board/2',
},
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('new path');
});
});
});
describe('buttons', () => {
it('cancel button triggers cancel()', () => {
wrapper.setMethods({ cancel: jest.fn() });
findModal().vm.$emit('cancel');
describe('when editing a board', () => {
beforeEach(() => {
boardsStore.state.currentPage = 'edit';
});
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true });
});
it('clears the form', () => {
expect(findConfigurationOptions().props('board')).toEqual(currentBoard);
});
it('shows a correct title about creating a board', () => {
expect(findModal().attributes('title')).toBe('Edit board');
});
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Save changes');
expect(findModalActionPrimary().attributes[0].variant).toBe('info');
});
it('does not render delete confirmation message', () => {
expect(findDeleteConfirmation().exists()).toBe(false);
});
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
it('passes a false isNewForm prop to BoardConfigurationOptions component', () => {
expect(findConfigurationOptions().props('isNewForm')).toBe(false);
});
});
it('passes a correct collapseScope property to BoardScope component on scoped board', async () => {
createComponent({ canAdminBoard: true, scopedIssueBoardFeatureEnabled: true });
await waitForPromises();
expect(findBoardScope().props('collapseScope')).toBe(false);
});
describe('when submitting an update event', () => {
beforeEach(() => {
const url = endpoints.boardsEndpoint;
axiosMock.onPut(url).reply(200, { board_path: 'new path' });
});
it('calls REST and GraphQL API with correct parameters', async () => {
createComponent({ canAdminBoard: true });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(axiosMock.history.put[0].data).toBe(
JSON.stringify({ board: { ...currentBoard, label_ids: [''] } }),
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.cancel).toHaveBeenCalled();
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
variables: {
id: `gid://gitlab/Board/${currentBoard.id}`,
},
});
});
});
});
......
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