summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2019-07-03 22:39:10 +0100
committerFilipa Lacerda <filipa@gitlab.com>2019-07-03 22:39:10 +0100
commit50be7237f41b0ac44b9aaf8b73c57993548d4c35 (patch)
treeecfeeae58829dadbd90de4f834c730d1d8c55e74 /spec/frontend
parent35331c435196ea1155eb15161f3f9a481a01501d (diff)
parent2ad75a4f96c4d377e18788966e7eefee4d78b6d2 (diff)
downloadgitlab-ce-update-todo-in-ui.tar.gz
Merge branch 'master' into update-todo-in-uiupdate-todo-in-ui
* master: (435 commits) Change occurrence of Sidekiq::Testing.inline! Fix order-dependent spec failure in appearance_spec.rb Put a failed example from appearance_spec in quarantine Cache PerformanceBar.allowed_user_ids list locally and in Redis Add Grafana to Admin > Monitoring menu when enabled Add changelog entry Add salesforce logo Move error_tracking_frontend specs to Jest Only save Peek session in Redis when Peek is enabled Migrate markdown header_spec.js to Jest Fix golint command in Go guide doc to be recursive Move images to their own dirs Gitlab -> GitLab Re-align CE and EE API docs Rename Release groups in issue_workflow.md Update api docs to finish aligning EE and CE docs Update locale.pot Update TODO: allow_collaboration column renaming Show upcoming status for releases Rebased and squashed commits ...
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/boards/modal_store_spec.js2
-rw-r--r--spec/frontend/boards/services/board_service_spec.js390
-rw-r--r--spec/frontend/branches/divergence_graph_spec.js32
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js16
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js90
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js113
-rw-r--r--spec/frontend/diffs/mock_data/diff_discussions.js529
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js63
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js91
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js109
-rw-r--r--spec/frontend/error_tracking_settings/mock.js92
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js194
-rw-r--r--spec/frontend/error_tracking_settings/store/getters_spec.js93
-rw-r--r--spec/frontend/error_tracking_settings/store/mutation_spec.js82
-rw-r--r--spec/frontend/error_tracking_settings/utils_spec.js29
-rw-r--r--spec/frontend/helpers/vuex_action_helper.js7
-rw-r--r--spec/frontend/helpers/vuex_action_helper_spec.js166
-rw-r--r--spec/frontend/ide/utils_spec.js44
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js6
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js19
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js40
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js14
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap16
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js21
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap14
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/log_tree_spec.js129
-rw-r--r--spec/frontend/test_setup.js19
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js107
30 files changed, 2494 insertions, 41 deletions
diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
index 4dd27e94d97..5b5ae4b6556 100644
--- a/spec/frontend/boards/modal_store_spec.js
+++ b/spec/frontend/boards/modal_store_spec.js
@@ -25,7 +25,7 @@ describe('Modal store', () => {
});
issue2 = new ListIssue({
title: 'Testing',
- id: 1,
+ id: 2,
iid: 2,
confidential: false,
labels: [],
diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js
new file mode 100644
index 00000000000..de9fc998360
--- /dev/null
+++ b/spec/frontend/boards/services/board_service_spec.js
@@ -0,0 +1,390 @@
+import BoardService from '~/boards/services/board_service';
+import { TEST_HOST } from 'helpers/test_constants';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+
+describe('BoardService', () => {
+ const dummyResponse = "without type checking this doesn't matter";
+ const boardId = 'dummy-board-id';
+ const endpoints = {
+ boardsEndpoint: `${TEST_HOST}/boards`,
+ listsEndpoint: `${TEST_HOST}/lists`,
+ bulkUpdatePath: `${TEST_HOST}/bulk/update`,
+ recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
+ };
+
+ let service;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ service = new BoardService({
+ ...endpoints,
+ boardId,
+ });
+ });
+
+ describe('all', () => {
+ it('makes a request to fetch lists', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.all()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
+
+ return expect(service.all()).rejects.toThrow();
+ });
+ });
+
+ describe('generateDefaultLists', () => {
+ const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`;
+
+ it('makes a request to generate default lists', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.generateDefaultLists()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
+
+ return expect(service.generateDefaultLists()).rejects.toThrow();
+ });
+ });
+
+ describe('createList', () => {
+ const entityType = 'moorhen';
+ const entityId = 'quack';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { [entityType]: entityId } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.listsEndpoint).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.createList(entityId, entityType))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.createList(entityId, entityType))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('updateList', () => {
+ const id = 'David Webb';
+ const position = 'unknown';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { position } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to update a list position', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.updateList(id, position))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.updateList(id, position))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('destroyList', () => {
+ const id = '-42';
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onDelete(`${endpoints.listsEndpoint}/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to delete a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.destroyList(id))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.destroyList(id))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('getIssuesForList', () => {
+ const id = 'TOO-MUCH';
+ const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
+
+ it('makes a request to fetch list issues', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id)).resolves.toEqual(expectedResponse);
+ });
+
+ it('makes a request to fetch list issues with filter', () => {
+ const filter = { algal: 'scrubber' };
+ axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getIssuesForList(id)).rejects.toThrow();
+ });
+ });
+
+ describe('moveIssue', () => {
+ const urlRoot = 'potato';
+ const id = 'over 9000';
+ const fromListId = 'left';
+ const toListId = 'right';
+ const moveBeforeId = 'up';
+ const moveAfterId = 'down';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to move an issue between lists', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('newIssue', () => {
+ const id = 'not-creative';
+ const issue = { some: 'issue data' };
+ const url = `${endpoints.listsEndpoint}/${id}/issues`;
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ issue,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(url).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a new issue', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.newIssue(id, issue))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.newIssue(id, issue))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getBacklog', () => {
+ const urlRoot = 'deep';
+ const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
+ const requestParams = {
+ not: 'relevant',
+ };
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ it('makes a request to fetch backlog', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getBacklog(requestParams)).rejects.toThrow();
+ });
+ });
+
+ describe('bulkUpdate', () => {
+ const issueIds = [1, 2, 3];
+ const extraData = { moar: 'data' };
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ update: {
+ ...extraData,
+ issuable_ids: '1,2,3',
+ },
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getIssueInfo', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+
+ describe('toggleIssueSubscription', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
+ expectedResponse,
+ );
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+});
diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js
new file mode 100644
index 00000000000..4ed77c3a036
--- /dev/null
+++ b/spec/frontend/branches/divergence_graph_spec.js
@@ -0,0 +1,32 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import init from '~/branches/divergence_graph';
+
+describe('Divergence graph', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet('/-/diverging_counts').reply(200, {
+ master: { ahead: 1, behind: 1 },
+ });
+
+ jest.spyOn(axios, 'get');
+
+ document.body.innerHTML = `
+ <div class="js-branch-item" data-name="master"></div>
+ `;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls axos get with list of branch names', () =>
+ init('/-/diverging_counts').then(() => {
+ expect(axios.get).toHaveBeenCalledWith('/-/diverging_counts', {
+ params: { names: ['master'] },
+ });
+ }));
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index c146ef79be7..8632c5c4e26 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -72,9 +72,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
- expectedState | event | effects
- ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -108,9 +109,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLED}`, () => {
it.each`
- expectedState | event | effects
- ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
- ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -119,7 +121,7 @@ describe('applicationStateMachine', () => {
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
- ...effects,
+ ...noEffectsToEmptyObject(effects),
});
});
});
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
new file mode 100644
index 00000000000..28689ab07de
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -0,0 +1,90 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('DiffDiscussionReply', () => {
+ let wrapper;
+ let getters;
+ let store;
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMount(DiffDiscussionReply, {
+ store,
+ localVue,
+ sync: false,
+ propsData: {
+ ...props,
+ },
+ slots: {
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('if user can reply', () => {
+ beforeEach(() => {
+ getters = {
+ userCanReply: () => true,
+ getUserData: () => ({
+ path: 'test-path',
+ avatar_url: 'avatar_url',
+ name: 'John Doe',
+ }),
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+ });
+
+ it('should render a form if component has form', () => {
+ createComponent(
+ {
+ renderReplyPlaceholder: false,
+ hasForm: true,
+ },
+ {
+ form: `<div id="test-form"></div>`,
+ },
+ );
+
+ expect(wrapper.find('#test-form').exists()).toBe(true);
+ });
+
+ it('should render a reply placeholder if there is no form', () => {
+ createComponent({
+ renderReplyPlaceholder: true,
+ hasForm: false,
+ });
+
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ });
+ });
+
+ it('renders a signed out widget when user is not logged in', () => {
+ getters = {
+ userCanReply: () => false,
+ getUserData: () => null,
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+
+ createComponent({
+ renderReplyPlaceholder: false,
+ hasForm: false,
+ });
+
+ expect(wrapper.find(NoteSignedOutWidget).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
new file mode 100644
index 00000000000..48ee5c63f35
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+const localVue = createLocalVue();
+const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+describe('DiffGutterAvatars', () => {
+ let wrapper;
+
+ const findCollapseButton = () => wrapper.find('.diff-notes-collapse');
+ const findMoreCount = () => wrapper.find('.diff-comments-more-count');
+ const findUserAvatars = () => wrapper.findAll('.diff-comment-avatar');
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DiffGutterAvatars, {
+ localVue,
+ sync: false,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when expanded', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: true,
+ });
+ });
+
+ it('renders a collapse button when discussions are expanded', () => {
+ expect(findCollapseButton().exists()).toBe(true);
+ });
+
+ it('should emit toggleDiscussions event on button click', () => {
+ findCollapseButton().trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+ });
+
+ describe('when collapsed', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: false,
+ });
+ });
+
+ it('renders user avatars and moreCount text', () => {
+ expect(findUserAvatars().exists()).toBe(true);
+ expect(findMoreCount().exists()).toBe(true);
+ });
+
+ it('renders correct amount of user avatars', () => {
+ expect(findUserAvatars().length).toBe(3);
+ });
+
+ it('renders correct moreCount number', () => {
+ expect(findMoreCount().text()).toBe('+2');
+ });
+
+ it('should emit toggleDiscussions event on avatars click', () => {
+ findUserAvatars()
+ .at(0)
+ .trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+
+ it('should emit toggleDiscussions event on more count text click', () => {
+ findMoreCount().trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+ });
+
+ it('renders an empty more count string if there are no discussions', () => {
+ createComponent({
+ discussions: [],
+ discussionsExpanded: false,
+ });
+
+ expect(findMoreCount().exists()).toBe(false);
+ });
+
+ describe('tooltip text', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: false,
+ });
+ });
+
+ it('returns original comment if it is shorter than max length', () => {
+ const note = wrapper.vm.discussions[0].notes[0];
+
+ expect(wrapper.vm.getTooltipText(note)).toEqual('Administrator: comment 1');
+ });
+
+ it('returns truncated version of comment if it is longer than max length', () => {
+ const note = wrapper.vm.discussions[0].notes[1];
+
+ expect(wrapper.vm.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...');
+ });
+ });
+});
diff --git a/spec/frontend/diffs/mock_data/diff_discussions.js b/spec/frontend/diffs/mock_data/diff_discussions.js
new file mode 100644
index 00000000000..711ab543411
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/diff_discussions.js
@@ -0,0 +1,529 @@
+export default {
+ id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ position: {
+ old_line: null,
+ new_line: 2,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ expanded: true,
+ notes: [
+ {
+ id: '1749',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-03T21:06:21.521Z',
+ updated_at: '2018-04-08T08:50:41.762Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 1',
+ note_html: '<p dir="auto">comment 1</p>',
+ last_edited_at: '2018-04-08T08:50:41.762Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1749',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1753',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/fatihacevt',
+ },
+ created_at: '2018-04-08T08:49:35.804Z',
+ updated_at: '2018-04-08T08:50:45.915Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 2 is really long one',
+ note_html: '<p dir="auto">comment 2 is really long one</p>',
+ last_edited_at: '2018-04-08T08:50:45.915Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1753',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1754',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:48.294Z',
+ updated_at: '2018-04-08T08:50:48.294Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 3',
+ note_html: '<p dir="auto">comment 3</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1754',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1755',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:50.911Z',
+ updated_at: '2018-04-08T08:50:50.911Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 4',
+ note_html: '<p dir="auto">comment 4</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1755',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1756',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:53.895Z',
+ updated_at: '2018-04-08T08:50:53.895Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 5',
+ note_html: '<p dir="auto">comment 5</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1756',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ ],
+ individual_note: false,
+ resolvable: true,
+ resolved: false,
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ diff_file: {
+ submodule: false,
+ submodule_link: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readable_text: true,
+ icon: 'file-text-o',
+ },
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG.rb',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
+ text: true,
+ added_lines: 2,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: 'CHANGELOG_OLD',
+ new_path_html: 'CHANGELOG',
+ is_fully_expanded: true,
+ context_lines_path:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlighted_diff_lines: [
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ ],
+ parallel_diff_lines: [
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ right: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ },
+ ],
+ viewer: {
+ name: 'text',
+ error: null,
+ },
+ },
+ diff_discussion: true,
+ truncated_diff_lines: [
+ {
+ text: 'line',
+ rich_text:
+ '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
+ can_receive_suggestion: true,
+ line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ meta_data: null,
+ },
+ ],
+};
+
+export const imageDiffDiscussions = [
+ {
+ id: '1',
+ position: {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 200,
+ },
+ },
+ {
+ id: '2',
+ position: {
+ x: 5,
+ y: 5,
+ width: 100,
+ height: 200,
+ },
+ },
+];
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
new file mode 100644
index 00000000000..022f12ef191
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -0,0 +1,63 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
+import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
+import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
+import createStore from '~/error_tracking_settings/store';
+import { TEST_HOST } from 'helpers/test_constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings app', () => {
+ let store;
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ErrorTrackingSettings, {
+ localVue,
+ store, // Override the imported store
+ propsData: {
+ initialEnabled: 'true',
+ initialApiHost: TEST_HOST,
+ initialToken: 'someToken',
+ initialProject: null,
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ store = createStore();
+
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('section', () => {
+ it('renders the form and dropdown', () => {
+ expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy();
+ expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy();
+ });
+
+ it('renders the Save Changes button', () => {
+ expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy();
+ });
+
+ it('enables the button by default', () => {
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy();
+ });
+
+ it('disables the button when saving', () => {
+ store.state.settingsLoading = true;
+
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
new file mode 100644
index 00000000000..23e57c4bbf1
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlButton, GlFormInput } from '@gitlab/ui';
+import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
+import { defaultProps } from '../mock';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings form', () => {
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ErrorTrackingForm, {
+ localVue,
+ propsData: defaultProps,
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('an empty form', () => {
+ it('is rendered', () => {
+ expect(wrapper.findAll(GlFormInput).length).toBe(2);
+ expect(wrapper.find(GlFormInput).attributes('id')).toBe('error-tracking-api-host');
+ expect(
+ wrapper
+ .findAll(GlFormInput)
+ .at(1)
+ .attributes('id'),
+ ).toBe('error-tracking-token');
+
+ expect(wrapper.findAll(GlButton).exists()).toBe(true);
+ });
+
+ it('is rendered with labels and placeholders', () => {
+ const pageText = wrapper.text();
+
+ expect(pageText).toContain('Find your hostname in your Sentry account settings page');
+ expect(pageText).toContain(
+ "After adding your Auth Token, use the 'Connect' button to load projects",
+ );
+
+ expect(pageText).not.toContain('Connection has failed. Re-check Auth Token and try again');
+ expect(
+ wrapper
+ .findAll(GlFormInput)
+ .at(0)
+ .attributes('placeholder'),
+ ).toContain('https://mysentryserver.com');
+ });
+ });
+
+ describe('after a successful connection', () => {
+ beforeEach(() => {
+ wrapper.setProps({ connectSuccessful: true });
+ });
+
+ it('shows the success checkmark', () => {
+ expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(true);
+ });
+
+ it('does not show an error', () => {
+ expect(wrapper.text()).not.toContain(
+ 'Connection has failed. Re-check Auth Token and try again',
+ );
+ });
+ });
+
+ describe('after an unsuccessful connection', () => {
+ beforeEach(() => {
+ wrapper.setProps({ connectError: true });
+ });
+
+ it('does not show the check mark', () => {
+ expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(false);
+ });
+
+ it('shows an error', () => {
+ expect(wrapper.text()).toContain('Connection has failed. Re-check Auth Token and try again');
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
new file mode 100644
index 00000000000..8e5dbe28452
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -0,0 +1,109 @@
+import _ from 'underscore';
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
+import { defaultProps, projectList, staleProject } from '../mock';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings project dropdown', () => {
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ProjectDropdown, {
+ localVue,
+ propsData: {
+ ..._.pick(
+ defaultProps,
+ 'dropdownLabel',
+ 'invalidProjectLabel',
+ 'projects',
+ 'projectSelectionLabel',
+ 'selectedProject',
+ 'token',
+ ),
+ hasProjects: false,
+ isProjectInvalid: false,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('empty project list', () => {
+ it('renders the dropdown', () => {
+ expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
+ expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ });
+
+ it('shows helper text', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy();
+ expect(wrapper.find('.js-project-dropdown-label').text()).toContain(
+ 'To enable project selection',
+ );
+ });
+
+ it('does not show an error', () => {
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ });
+
+ it('does not contain any dropdown items', () => {
+ expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy();
+ expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
+ });
+ });
+
+ describe('populated project list', () => {
+ beforeEach(() => {
+ wrapper.setProps({ projects: _.clone(projectList), hasProjects: true });
+ });
+
+ it('renders the dropdown', () => {
+ expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
+ expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ });
+
+ it('contains a number of dropdown items', () => {
+ expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy();
+ expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
+ });
+ });
+
+ describe('selected project', () => {
+ const selectedProject = _.clone(projectList[0]);
+
+ beforeEach(() => {
+ wrapper.setProps({ projects: _.clone(projectList), selectedProject, hasProjects: true });
+ });
+
+ it('does not show helper text', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ });
+ });
+
+ describe('invalid project selected', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ projects: _.clone(projectList),
+ selectedProject: staleProject,
+ isProjectInvalid: true,
+ });
+ });
+
+ it('displays a error', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js
new file mode 100644
index 00000000000..42233f82d54
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/mock.js
@@ -0,0 +1,92 @@
+import createStore from '~/error_tracking_settings/store';
+import { TEST_HOST } from '../helpers/test_constants';
+
+const defaultStore = createStore();
+
+export const projectList = [
+ {
+ name: 'name',
+ slug: 'slug',
+ organizationName: 'organizationName',
+ organizationSlug: 'organizationSlug',
+ },
+ {
+ name: 'name2',
+ slug: 'slug2',
+ organizationName: 'organizationName2',
+ organizationSlug: 'organizationSlug2',
+ },
+];
+
+export const staleProject = {
+ name: 'staleName',
+ slug: 'staleSlug',
+ organizationName: 'staleOrganizationName',
+ organizationSlug: 'staleOrganizationSlug',
+};
+
+export const normalizedProject = {
+ name: 'name',
+ slug: 'slug',
+ organizationName: 'organization_name',
+ organizationSlug: 'organization_slug',
+};
+
+export const sampleBackendProject = {
+ name: normalizedProject.name,
+ slug: normalizedProject.slug,
+ organization_name: normalizedProject.organizationName,
+ organization_slug: normalizedProject.organizationSlug,
+};
+
+export const sampleFrontendSettings = {
+ apiHost: 'apiHost',
+ enabled: false,
+ token: 'token',
+ selectedProject: {
+ slug: normalizedProject.slug,
+ name: normalizedProject.name,
+ organizationName: normalizedProject.organizationName,
+ organizationSlug: normalizedProject.organizationSlug,
+ },
+};
+
+export const transformedSettings = {
+ api_host: 'apiHost',
+ enabled: false,
+ token: 'token',
+ project: {
+ slug: normalizedProject.slug,
+ name: normalizedProject.name,
+ organization_name: normalizedProject.organizationName,
+ organization_slug: normalizedProject.organizationSlug,
+ },
+};
+
+export const defaultProps = {
+ ...defaultStore.state,
+ ...defaultStore.getters,
+};
+
+export const initialEmptyState = {
+ apiHost: '',
+ enabled: false,
+ project: null,
+ token: '',
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+};
+
+export const initialPopulatedState = {
+ apiHost: 'apiHost',
+ enabled: true,
+ project: JSON.stringify(projectList[0]),
+ token: 'token',
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+};
+
+export const projectWithHtmlTemplate = {
+ ...projectList[0],
+ name: '<strong>bold</strong>',
+};
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
new file mode 100644
index 00000000000..1eab0f7470b
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -0,0 +1,194 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as actions from '~/error_tracking_settings/store/actions';
+import * as types from '~/error_tracking_settings/store/mutation_types';
+import defaultState from '~/error_tracking_settings/store/state';
+import { projectList } from '../mock';
+
+jest.mock('~/lib/utils/url_utility');
+
+describe('error tracking settings actions', () => {
+ let state;
+
+ describe('project list actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = { ...defaultState(), listProjectsEndpoint: TEST_HOST };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ refreshCurrentPage.mockClear();
+ });
+
+ it('should request and transform the project list', done => {
+ mock.onPost(TEST_HOST).reply(() => [200, { projects: projectList }]);
+ testAction(
+ actions.fetchProjects,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestProjects' },
+ {
+ type: 'receiveProjectsSuccess',
+ payload: projectList.map(convertObjectPropsToCamelCase),
+ },
+ ],
+ () => {
+ expect(mock.history.post.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should handle a server error', done => {
+ mock.onPost(`${TEST_HOST}.json`).reply(() => [400]);
+ testAction(
+ actions.fetchProjects,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestProjects' },
+ {
+ type: 'receiveProjectsError',
+ },
+ ],
+ () => {
+ expect(mock.history.post.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should request projects correctly', done => {
+ testAction(actions.requestProjects, null, state, [{ type: types.RESET_CONNECT }], [], done);
+ });
+
+ it('should receive projects correctly', done => {
+ const testPayload = [];
+ testAction(
+ actions.receiveProjectsSuccess,
+ testPayload,
+ state,
+ [
+ { type: types.UPDATE_CONNECT_SUCCESS },
+ { type: types.RECEIVE_PROJECTS, payload: testPayload },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should handle errors when receiving projects', done => {
+ const testPayload = [];
+ testAction(
+ actions.receiveProjectsError,
+ testPayload,
+ state,
+ [{ type: types.UPDATE_CONNECT_ERROR }, { type: types.CLEAR_PROJECTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('save changes actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = {
+ operationsSettingsEndpoint: TEST_HOST,
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should save the page', done => {
+ mock.onPatch(TEST_HOST).reply(200);
+ testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => {
+ expect(mock.history.patch.length).toBe(1);
+ expect(refreshCurrentPage).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('should handle a server error', done => {
+ mock.onPatch(TEST_HOST).reply(400);
+ testAction(
+ actions.updateSettings,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestSettings' },
+ {
+ type: 'receiveSettingsError',
+ payload: new Error('Request failed with status code 400'),
+ },
+ ],
+ () => {
+ expect(mock.history.patch.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should request to save the page', done => {
+ testAction(
+ actions.requestSettings,
+ null,
+ state,
+ [{ type: types.UPDATE_SETTINGS_LOADING, payload: true }],
+ [],
+ done,
+ );
+ });
+
+ it('should handle errors when requesting to save the page', done => {
+ testAction(
+ actions.receiveSettingsError,
+ {},
+ state,
+ [{ type: types.UPDATE_SETTINGS_LOADING, payload: false }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('generic actions to update the store', () => {
+ const testData = 'test';
+ it('should reset the `connect success` flag when updating the api host', done => {
+ testAction(
+ actions.updateApiHost,
+ testData,
+ state,
+ [{ type: types.UPDATE_API_HOST, payload: testData }, { type: types.RESET_CONNECT }],
+ [],
+ done,
+ );
+ });
+
+ it('should reset the `connect success` flag when updating the token', done => {
+ testAction(
+ actions.updateToken,
+ testData,
+ state,
+ [{ type: types.UPDATE_TOKEN, payload: testData }, { type: types.RESET_CONNECT }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/store/getters_spec.js b/spec/frontend/error_tracking_settings/store/getters_spec.js
new file mode 100644
index 00000000000..2c5ff084b8a
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/store/getters_spec.js
@@ -0,0 +1,93 @@
+import * as getters from '~/error_tracking_settings/store/getters';
+import defaultState from '~/error_tracking_settings/store/state';
+import { projectList, projectWithHtmlTemplate, staleProject } from '../mock';
+
+describe('Error Tracking Settings - Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = defaultState();
+ });
+
+ describe('hasProjects', () => {
+ it('should reflect when no projects exist', () => {
+ expect(getters.hasProjects(state)).toEqual(false);
+ });
+
+ it('should reflect when projects exist', () => {
+ state.projects = projectList;
+
+ expect(getters.hasProjects(state)).toEqual(true);
+ });
+ });
+
+ describe('isProjectInvalid', () => {
+ const mockGetters = { hasProjects: true };
+ it('should show when a project is valid', () => {
+ state.projects = projectList;
+ [state.selectedProject] = projectList;
+
+ expect(getters.isProjectInvalid(state, mockGetters)).toEqual(false);
+ });
+
+ it('should show when a project is invalid', () => {
+ state.projects = projectList;
+ state.selectedProject = staleProject;
+
+ expect(getters.isProjectInvalid(state, mockGetters)).toEqual(true);
+ });
+ });
+
+ describe('dropdownLabel', () => {
+ const mockGetters = { hasProjects: false };
+ it('should display correctly when there are no projects available', () => {
+ expect(getters.dropdownLabel(state, mockGetters)).toEqual('No projects available');
+ });
+
+ it('should display correctly when a project is selected', () => {
+ [state.selectedProject] = projectList;
+
+ expect(getters.dropdownLabel(state, mockGetters)).toEqual('organizationName | name');
+ });
+
+ it('should display correctly when no project is selected', () => {
+ state.projects = projectList;
+
+ expect(getters.dropdownLabel(state, { hasProjects: true })).toEqual('Select project');
+ });
+ });
+
+ describe('invalidProjectLabel', () => {
+ it('should display an error containing the project name', () => {
+ [state.selectedProject] = projectList;
+
+ expect(getters.invalidProjectLabel(state)).toEqual(
+ 'Project "name" is no longer available. Select another project to continue.',
+ );
+ });
+
+ it('should properly escape the label text', () => {
+ state.selectedProject = projectWithHtmlTemplate;
+
+ expect(getters.invalidProjectLabel(state)).toEqual(
+ 'Project "&lt;strong&gt;bold&lt;/strong&gt;" is no longer available. Select another project to continue.',
+ );
+ });
+ });
+
+ describe('projectSelectionLabel', () => {
+ it('should show the correct message when the token is empty', () => {
+ expect(getters.projectSelectionLabel(state)).toEqual(
+ 'To enable project selection, enter a valid Auth Token',
+ );
+ });
+
+ it('should show the correct message when token exists', () => {
+ state.token = 'test-token';
+
+ expect(getters.projectSelectionLabel(state)).toEqual(
+ "Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.",
+ );
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/store/mutation_spec.js b/spec/frontend/error_tracking_settings/store/mutation_spec.js
new file mode 100644
index 00000000000..fa188462c3f
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/store/mutation_spec.js
@@ -0,0 +1,82 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import mutations from '~/error_tracking_settings/store/mutations';
+import defaultState from '~/error_tracking_settings/store/state';
+import * as types from '~/error_tracking_settings/store/mutation_types';
+import {
+ initialEmptyState,
+ initialPopulatedState,
+ projectList,
+ sampleBackendProject,
+ normalizedProject,
+} from '../mock';
+
+describe('error tracking settings mutations', () => {
+ describe('mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = defaultState();
+ });
+
+ it('should create an empty initial state correctly', () => {
+ mutations[types.SET_INITIAL_STATE](state, {
+ ...initialEmptyState,
+ });
+
+ expect(state.apiHost).toEqual('');
+ expect(state.enabled).toEqual(false);
+ expect(state.selectedProject).toEqual(null);
+ expect(state.token).toEqual('');
+ expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
+ expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST);
+ });
+
+ it('should populate the initial state correctly', () => {
+ mutations[types.SET_INITIAL_STATE](state, {
+ ...initialPopulatedState,
+ });
+
+ expect(state.apiHost).toEqual('apiHost');
+ expect(state.enabled).toEqual(true);
+ expect(state.selectedProject).toEqual(projectList[0]);
+ expect(state.token).toEqual('token');
+ expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
+ expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST);
+ });
+
+ it('should receive projects successfully', () => {
+ mutations[types.RECEIVE_PROJECTS](state, [sampleBackendProject]);
+
+ expect(state.projects).toEqual([normalizedProject]);
+ });
+
+ it('should strip out unnecessary project properties', () => {
+ mutations[types.RECEIVE_PROJECTS](state, [
+ { ...sampleBackendProject, extra_property: 'extra_property' },
+ ]);
+
+ expect(state.projects).toEqual([normalizedProject]);
+ });
+
+ it('should update state when connect is successful', () => {
+ mutations[types.UPDATE_CONNECT_SUCCESS](state);
+
+ expect(state.connectSuccessful).toBe(true);
+ expect(state.connectError).toBe(false);
+ });
+
+ it('should update state when connect fails', () => {
+ mutations[types.UPDATE_CONNECT_ERROR](state);
+
+ expect(state.connectSuccessful).toBe(false);
+ expect(state.connectError).toBe(true);
+ });
+
+ it('should update state when connect is reset', () => {
+ mutations[types.RESET_CONNECT](state);
+
+ expect(state.connectSuccessful).toBe(false);
+ expect(state.connectError).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking_settings/utils_spec.js b/spec/frontend/error_tracking_settings/utils_spec.js
new file mode 100644
index 00000000000..4b144f7daf1
--- /dev/null
+++ b/spec/frontend/error_tracking_settings/utils_spec.js
@@ -0,0 +1,29 @@
+import { transformFrontendSettings } from '~/error_tracking_settings/utils';
+import { sampleFrontendSettings, transformedSettings } from './mock';
+
+describe('error tracking settings utils', () => {
+ describe('data transform functions', () => {
+ it('should transform settings successfully for the backend', () => {
+ expect(transformFrontendSettings(sampleFrontendSettings)).toEqual(transformedSettings);
+ });
+
+ it('should transform empty values in the settings object to null', () => {
+ const emptyFrontendSettingsObject = {
+ apiHost: '',
+ enabled: false,
+ token: '',
+ selectedProject: null,
+ };
+ const transformedEmptySettingsObject = {
+ api_host: null,
+ enabled: false,
+ token: null,
+ project: null,
+ };
+
+ expect(transformFrontendSettings(emptyFrontendSettingsObject)).toEqual(
+ transformedEmptySettingsObject,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js
index 88652202a8e..6c3569a2247 100644
--- a/spec/frontend/helpers/vuex_action_helper.js
+++ b/spec/frontend/helpers/vuex_action_helper.js
@@ -20,7 +20,7 @@ const noop = () => {};
* // expected mutations
* [
* { type: types.MUTATION}
- * { type: types.MUTATION_1, payload: jasmine.any(Number)}
+ * { type: types.MUTATION_1, payload: expect.any(Number)}
* ],
* // expected actions
* [
@@ -89,10 +89,7 @@ export default (
payload,
);
- return new Promise(resolve => {
- setImmediate(resolve);
- })
- .then(() => result)
+ return (result || new Promise(resolve => setImmediate(resolve)))
.catch(error => {
validateResults();
throw error;
diff --git a/spec/frontend/helpers/vuex_action_helper_spec.js b/spec/frontend/helpers/vuex_action_helper_spec.js
new file mode 100644
index 00000000000..61d05762a04
--- /dev/null
+++ b/spec/frontend/helpers/vuex_action_helper_spec.js
@@ -0,0 +1,166 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import testAction from './vuex_action_helper';
+
+describe('VueX test helper (testAction)', () => {
+ let originalExpect;
+ let assertion;
+ let mock;
+ const noop = () => {};
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ /**
+ * In order to test the helper properly, we need to overwrite the Jest
+ * `expect` helper. We test that the testAction helper properly passes the
+ * dispatched actions/committed mutations to the Jest helper.
+ */
+ originalExpect = expect;
+ assertion = null;
+ global.expect = actual => ({
+ toEqual: () => {
+ originalExpect(actual).toEqual(assertion);
+ },
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ global.expect = originalExpect;
+ });
+
+ it('properly passes state and payload to action', () => {
+ const exampleState = { FOO: 12, BAR: 3 };
+ const examplePayload = { BAZ: 73, BIZ: 55 };
+
+ const action = ({ state }, payload) => {
+ originalExpect(state).toEqual(exampleState);
+ originalExpect(payload).toEqual(examplePayload);
+ };
+
+ assertion = { mutations: [], actions: [] };
+
+ testAction(action, examplePayload, exampleState);
+ });
+
+ describe('given a sync action', () => {
+ it('mocks committing mutations', () => {
+ const action = ({ commit }) => {
+ commit('MUTATION');
+ };
+
+ assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('mocks dispatching actions', () => {
+ const action = ({ dispatch }) => {
+ dispatch('ACTION');
+ };
+
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('works with done callback once finished', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns a promise', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions)
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('given an async action (returning a promise)', () => {
+ let lastError;
+ const data = { FOO: 'BAR' };
+
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
+
+ return axios
+ .get(TEST_HOST)
+ .catch(error => {
+ commit('ERROR');
+ lastError = error;
+ throw error;
+ })
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ });
+ };
+
+ beforeEach(() => {
+ lastError = null;
+ });
+
+ it('works with done callback once finished', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns original data of successful promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(res => {
+ originalExpect(res).toEqual(data);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns original error of rejected promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(500, '');
+
+ assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(done.fail)
+ .catch(error => {
+ originalExpect(error).toBe(lastError);
+ done();
+ });
+ });
+ });
+
+ it('works with async actions not returning promises', done => {
+ const data = { FOO: 'BAR' };
+
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
+
+ axios
+ .get(TEST_HOST)
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ })
+ .catch(error => {
+ commit('ERROR');
+ throw error;
+ });
+ };
+
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
new file mode 100644
index 00000000000..2b7dffdcd88
--- /dev/null
+++ b/spec/frontend/ide/utils_spec.js
@@ -0,0 +1,44 @@
+import { commitItemIconMap } from '~/ide/constants';
+import { getCommitIconMap } from '~/ide/utils';
+import { decorateData } from '~/ide/stores/utils';
+
+describe('WebIDE utils', () => {
+ const createFile = (name = 'name', id = name, type = '', parent = null) =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
+ lastCommit: {},
+ });
+
+ describe('getCommitIconMap', () => {
+ let entry;
+
+ beforeEach(() => {
+ entry = createFile('Entry item');
+ });
+
+ it('renders "deleted" icon for deleted entries', () => {
+ entry.deleted = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted);
+ });
+ it('renders "addition" icon for temp entries', () => {
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition);
+ });
+ it('renders "modified" icon for newly-renamed entries', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = false;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ it('renders "modified" icon even for temp entries if they are newly-renamed', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 9f49e68cfe8..751fb5e1b94 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -334,6 +334,12 @@ describe('prettyTime methods', () => {
assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
});
+
+ it('should correctly parse values when limitedToHours is true', () => {
+ const twoDays = datetimeUtility.parseSeconds(173000, { limitToHours: true });
+
+ assertTimeUnits(twoDays, 3, 48, 0, 0);
+ });
});
describe('stringifyTime', () => {
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 9e920d59093..dc886d0db3b 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -55,9 +55,24 @@ describe('text_utility', () => {
});
});
- describe('slugifyWithHyphens', () => {
+ describe('slugify', () => {
+ it('should remove accents and convert to lower case', () => {
+ expect(textUtils.slugify('João')).toEqual('jo-o');
+ });
it('should replaces whitespaces with hyphens and convert to lower case', () => {
- expect(textUtils.slugifyWithHyphens('My Input String')).toEqual('my-input-string');
+ expect(textUtils.slugify('My Input String')).toEqual('my-input-string');
+ });
+ it('should remove trailing whitespace and replace whitespaces within string with a hyphen', () => {
+ expect(textUtils.slugify(' a new project ')).toEqual('a-new-project');
+ });
+ it('should only remove non-allowed special characters', () => {
+ expect(textUtils.slugify('test!_pro-ject~')).toEqual('test-_pro-ject-');
+ });
+ it('should squash multiple hypens', () => {
+ expect(textUtils.slugify('test!!!!_pro-ject~')).toEqual('test-_pro-ject-');
+ });
+ it('should return empty string if only non-allowed characters', () => {
+ expect(textUtils.slugify('здрасти')).toEqual('');
});
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index c3204b3aaa0..58d367077e8 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -87,7 +87,7 @@ describe('DiscussionNotes', () => {
discussion.notes[0],
];
discussion.notes = notesData;
- createComponent({ discussion });
+ createComponent({ discussion, shouldRenderDiffs: true });
const notes = wrapper.findAll('.notes > li');
expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
@@ -112,6 +112,44 @@ describe('DiscussionNotes', () => {
});
});
+ describe('events', () => {
+ describe('with groupped notes and replies expanded', () => {
+ const findNoteAtIndex = index => wrapper.find(`.note:nth-of-type(${index + 1}`);
+
+ beforeEach(() => {
+ createComponent({ shouldGroupReplies: true, isExpanded: true });
+ });
+
+ it('emits deleteNote when first note emits handleDeleteNote', () => {
+ findNoteAtIndex(0).vm.$emit('handleDeleteNote');
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
+ });
+
+ it('emits startReplying when first note emits startReplying', () => {
+ findNoteAtIndex(0).vm.$emit('startReplying');
+ expect(wrapper.emitted().startReplying).toBeTruthy();
+ });
+
+ it('emits deleteNote when second note emits handleDeleteNote', () => {
+ findNoteAtIndex(1).vm.$emit('handleDeleteNote');
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
+ });
+ });
+
+ describe('with ungroupped notes', () => {
+ let note;
+ beforeEach(() => {
+ createComponent();
+ note = wrapper.find('.note');
+ });
+
+ it('emits deleteNote when first note emits handleDeleteNote', () => {
+ note.vm.$emit('handleDeleteNote');
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
+ });
+ });
+ });
+
describe('componentData', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 07a366cf339..e008f4ed093 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -2,13 +2,19 @@ import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vu
import { shallowMount, createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
+const buttonText = 'Test Button Text';
describe('ReplyPlaceholder', () => {
let wrapper;
+ const findButton = () => wrapper.find({ ref: 'button' });
+
beforeEach(() => {
wrapper = shallowMount(ReplyPlaceholder, {
localVue,
+ propsData: {
+ buttonText,
+ },
});
});
@@ -17,9 +23,7 @@ describe('ReplyPlaceholder', () => {
});
it('emits onClick even on button click', () => {
- const button = wrapper.find({ ref: 'button' });
-
- button.trigger('click');
+ findButton().trigger('click');
expect(wrapper.emitted()).toEqual({
onClick: [[]],
@@ -27,8 +31,6 @@ describe('ReplyPlaceholder', () => {
});
it('should render reply button', () => {
- const button = wrapper.find({ ref: 'button' });
-
- expect(button.text()).toEqual('Reply...');
+ expect(findButton().text()).toEqual(buttonText);
});
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 3ad6bfa9e5f..cd8372a8800 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -27,8 +27,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
href="https://test.com/commit/123"
>
- Commit title
-
+ Commit title
+
</gllink-stub>
<!---->
@@ -41,12 +41,12 @@ exports[`Repository last commit component renders commit widget 1`] = `
href="https://test.com/test"
>
- Test
-
+ Test
+
</gllink-stub>
- authored
-
+ authored
+
<timeagotooltip-stub
cssclass=""
time="2019-01-01"
@@ -81,8 +81,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="label label-monospace monospace"
>
- 12345678
-
+ 12345678
+
</div>
<clipboardbutton-stub
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 972690a60f6..14479f3c3a4 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import LastCommit from '~/repository/components/last_commit.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -6,7 +7,7 @@ let vm;
function createCommitData(data = {}) {
return {
- id: '123456789',
+ sha: '123456789',
title: 'Commit title',
message: 'Commit message',
webUrl: 'https://test.com/commit/123',
@@ -16,7 +17,7 @@ function createCommitData(data = {}) {
avatarUrl: 'https://test.com',
webUrl: 'https://test.com/test',
},
- pipeline: {
+ latestPipeline: {
detailedStatus: {
detailsPath: 'https://test.com/pipeline',
icon: 'failed',
@@ -52,12 +53,12 @@ describe('Repository last commit component', () => {
it.each`
loading | label
- ${true} | ${'hides'}
- ${false} | ${'shows'}
- `('$label when $loading is true', ({ loading }) => {
+ ${true} | ${'shows'}
+ ${false} | ${'hides'}
+ `('$label when loading icon $loading is true', ({ loading }) => {
factory(createCommitData(), loading);
- expect(vm.isEmpty()).toBe(loading);
+ expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
});
it('renders commit widget', () => {
@@ -73,11 +74,17 @@ describe('Repository last commit component', () => {
});
it('hides pipeline components when pipeline does not exist', () => {
- factory(createCommitData({ pipeline: null }));
+ factory(createCommitData({ latestPipeline: null }));
expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
+ it('renders pipeline components', () => {
+ factory();
+
+ expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
+ });
+
it('hides author component when author does not exist', () => {
factory(createCommitData({ author: null }));
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 1f06d693411..d55dc553031 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -29,10 +29,20 @@ exports[`Repository table row component renders table row 1`] = `
<td
class="d-none d-sm-table-cell tree-commit"
- />
+ >
+ <glskeletonloading-stub
+ class="h-auto"
+ lines="1"
+ />
+ </td>
<td
class="tree-time-ago text-right"
- />
+ >
+ <glskeletonloading-stub
+ class="ml-auto h-auto w-50"
+ lines="1"
+ />
+ </td>
</tr>
`;
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 5a345ddeacd..c566057ad3f 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -16,6 +16,8 @@ function factory(propsData = {}) {
vm = shallowMount(TableRow, {
propsData: {
...propsData,
+ name: propsData.path,
+ projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
},
mocks: {
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
new file mode 100644
index 00000000000..a9499f7c61b
--- /dev/null
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -0,0 +1,129 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { normalizeData, resolveCommit, fetchLogsTree } from '~/repository/log_tree';
+
+const mockData = [
+ {
+ commit: {
+ id: '123',
+ message: 'testing message',
+ committed_date: '2019-01-01',
+ },
+ commit_path: `https://test.com`,
+ file_name: 'index.js',
+ type: 'blob',
+ },
+];
+
+describe('normalizeData', () => {
+ it('normalizes data into LogTreeCommit object', () => {
+ expect(normalizeData(mockData)).toEqual([
+ {
+ sha: '123',
+ message: 'testing message',
+ committedDate: '2019-01-01',
+ commitPath: 'https://test.com',
+ fileName: 'index.js',
+ type: 'blob',
+ __typename: 'LogTreeCommit',
+ },
+ ]);
+ });
+});
+
+describe('resolveCommit', () => {
+ it('calls resolve when commit found', () => {
+ const resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ const commits = [{ fileName: 'index.js', type: 'blob' }];
+
+ resolveCommit(commits, resolver);
+
+ expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', type: 'blob' });
+ });
+});
+
+describe('fetchLogsTree', () => {
+ let mock;
+ let client;
+ let resolver;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(/(.*)/).reply(200, mockData, {});
+
+ jest.spyOn(axios, 'get');
+
+ global.gon = { gitlab_url: 'https://test.com' };
+
+ client = {
+ readQuery: () => ({
+ projectPath: 'gitlab-org/gitlab-ce',
+ ref: 'master',
+ commits: [],
+ }),
+ writeQuery: jest.fn(),
+ };
+
+ resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls axios get', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ 'https://test.com/gitlab-org/gitlab-ce/refs/master/logs_tree',
+ { params: { format: 'json', offset: '0' } },
+ );
+ }));
+
+ it('calls axios get once', () =>
+ Promise.all([
+ fetchLogsTree(client, '', '0', resolver),
+ fetchLogsTree(client, '', '0', resolver),
+ ]).then(() => {
+ expect(axios.get.mock.calls.length).toEqual(1);
+ }));
+
+ it('calls entry resolver', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(resolver.resolve).toHaveBeenCalledWith({
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ });
+ }));
+
+ it('writes query to client', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: expect.anything(),
+ data: {
+ commits: [
+ {
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ },
+ ],
+ },
+ });
+ }));
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 7e7cc1488b8..15cf18700ed 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,10 +1,17 @@
import Vue from 'vue';
import * as jqueryMatchers from 'custom-jquery-matchers';
+import $ from 'jquery';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
+import { config as testUtilsConfig } from '@vue/test-utils';
import { initializeTestTimeout } from './helpers/timeout';
import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
+// Expose jQuery so specs using jQuery plugins can be imported nicely.
+// Here is an issue to explore better alternatives:
+// https://gitlab.com/gitlab-org/gitlab-ee/issues/12448
+window.jQuery = $;
+
process.on('unhandledRejection', global.promiseRejectionHandler);
afterEach(() =>
@@ -54,9 +61,21 @@ Object.assign(global, {
preloadFixtures() {},
});
+Object.assign(global, {
+ MutationObserver() {
+ return {
+ disconnect() {},
+ observe() {},
+ };
+ },
+});
+
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
expect.extend({
[matcherName]: matcherFactory().compare,
});
});
+
+// Tech debt issue TBD
+testUtilsConfig.logModifiedComponents = false;
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index e43d5301a50..b85e2673624 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -88,7 +88,7 @@ describe('RelatedIssuableItem', () => {
});
it('renders state title', () => {
- const stateTitle = tokenState.attributes('data-original-title');
+ const stateTitle = tokenState.attributes('title');
const formatedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
@@ -155,7 +155,9 @@ describe('RelatedIssuableItem', () => {
describe('token assignees', () => {
it('renders assignees avatars', () => {
- expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
+ // Expect 2 times 2 because assignees are rendered twice, due to layout issues
+ expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBeDefined();
+
expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
new file mode 100644
index 00000000000..aa0b544f948
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -0,0 +1,107 @@
+import Vue from 'vue';
+import $ from 'jquery';
+import headerComponent from '~/vue_shared/components/markdown/header.vue';
+
+describe('Markdown field header component', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(headerComponent);
+
+ vm = new Component({
+ propsData: {
+ previewMarkdown: false,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders markdown header buttons', () => {
+ const buttons = [
+ 'Add bold text',
+ 'Add italic text',
+ 'Insert a quote',
+ 'Insert suggestion',
+ 'Insert code',
+ 'Add a link',
+ 'Add a bullet list',
+ 'Add a numbered list',
+ 'Add a task list',
+ 'Add a table',
+ 'Go full screen',
+ ];
+ const elements = vm.$el.querySelectorAll('.toolbar-btn');
+
+ elements.forEach((buttonEl, index) => {
+ expect(buttonEl.getAttribute('data-original-title')).toBe(buttons[index]);
+ });
+ });
+
+ it('renders `write` link as active when previewMarkdown is false', () => {
+ expect(vm.$el.querySelector('li:nth-child(1)').classList.contains('active')).toBeTruthy();
+ });
+
+ it('renders `preview` link as active when previewMarkdown is true', done => {
+ vm.previewMarkdown = true;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('li:nth-child(2)').classList.contains('active')).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('emits toggle markdown event when clicking preview', () => {
+ jest.spyOn(vm, '$emit').mockImplementation();
+
+ vm.$el.querySelector('.js-preview-link').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('preview-markdown');
+
+ vm.$el.querySelector('.js-write-link').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('write-markdown');
+ });
+
+ it('does not emit toggle markdown event when triggered from another form', () => {
+ jest.spyOn(vm, '$emit').mockImplementation();
+
+ $(document).triggerHandler('markdown-preview:show', [
+ $(
+ '<form><div class="js-vue-markdown-field"><textarea class="markdown-area"></textarea></div></form>',
+ ),
+ ]);
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('blurs preview link after click', () => {
+ const link = vm.$el.querySelector('li:nth-child(2) button');
+ jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation();
+
+ link.click();
+
+ expect(link.blur).toHaveBeenCalled();
+ });
+
+ it('renders markdown table template', () => {
+ expect(vm.mdTable).toEqual(
+ '| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |',
+ );
+ });
+
+ it('renders suggestion template', () => {
+ vm.lineContent = 'Some content';
+
+ expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```');
+ });
+
+ it('does not render suggestion button if `canSuggest` is set to false', () => {
+ vm.canSuggest = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.qa-suggestion-btn')).toBe(null);
+ });
+ });
+});