Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-11 12:09:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-11 12:09:45 +0300
commitc7ba7b997608a103a0a9165b2e5cef9530c4ef53 (patch)
tree3af88eaacba25539b97da4ad358418b6a69c11d7 /spec/frontend
parenta031b1f4f34470fba578856dc7ab735a6f54733a (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js42
-rw-r--r--spec/frontend/ci_variable_list/services/mock_data.js12
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js2
-rw-r--r--spec/frontend/ide/services/index_spec.js33
-rw-r--r--spec/frontend/image_diff/helpers/badge_helper_spec.js130
-rw-r--r--spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js144
-rw-r--r--spec/frontend/image_diff/helpers/dom_helper_spec.js120
-rw-r--r--spec/frontend/image_diff/helpers/utils_helper_spec.js152
-rw-r--r--spec/frontend/image_diff/image_badge_spec.js84
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js361
-rw-r--r--spec/frontend/image_diff/mock_data.js28
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js356
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js122
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js123
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/dropdown/mock_data.js11
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js140
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js83
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js109
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js57
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/tabs/tabs_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js27
40 files changed, 3218 insertions, 22 deletions
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index cb2531663bd..d7170e71a96 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -9,6 +9,7 @@ import {
GlIcon,
GlTab,
} from '@gitlab/ui';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants';
@@ -24,6 +25,7 @@ describe('AlertManagementList', () => {
const findStatusDropdown = () => wrapper.find(GlNewDropdown);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge);
+ const findDateFields = () => wrapper.findAll(TimeAgo);
function mountComponent({
props = {
@@ -198,5 +200,45 @@ describe('AlertManagementList', () => {
).toBe(true);
});
});
+
+ describe('handle date fields', () => {
+ it('should display time ago dates when values provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ startedAt: '2020-03-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().length).toBe(2);
+ });
+
+ it('should not display time ago dates when values not provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ startedAt: null,
+ endedAt: null,
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js
index 09c6cd9de21..7dab33050d9 100644
--- a/spec/frontend/ci_variable_list/services/mock_data.js
+++ b/spec/frontend/ci_variable_list/services/mock_data.js
@@ -8,7 +8,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
],
@@ -44,7 +44,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
{
environment_scope: 'All (default)',
@@ -104,7 +104,7 @@ export default {
id: 28,
key: 'goku_var',
value: 'goku_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: true,
masked: true,
environment_scope: 'staging',
@@ -114,7 +114,7 @@ export default {
id: 25,
key: 'test_var_4',
value: 'test_val_4',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
@@ -134,7 +134,7 @@ export default {
id: 24,
key: 'test_var_3',
value: 'test_val_3',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'All (default)',
@@ -144,7 +144,7 @@ export default {
id: 26,
key: 'test_var_5',
value: 'test_val_5',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index 8652359f3df..ce0792d0353 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -47,7 +47,7 @@ describe('CI variable list mutations', () => {
describe('CLEAR_MODAL', () => {
it('should clear modal state ', () => {
const modalState = {
- variable_type: 'Var',
+ variable_type: 'Variable',
key: '',
secret_value: '',
protected: false,
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 658ad37d7f2..f4d4122bd5a 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -221,4 +221,37 @@ describe('IDE services', () => {
});
});
});
+
+ describe('getFiles', () => {
+ let mock;
+ let relativeUrlRoot;
+ const TEST_RELATIVE_URL_ROOT = 'blah-blah';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ relativeUrlRoot = gon.relative_url_root;
+ gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
+
+ mock = new MockAdapter(axios);
+
+ mock
+ .onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`)
+ .reply(200, [TEST_FILE_PATH]);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ gon.relative_url_root = relativeUrlRoot;
+ });
+
+ it('initates the api call based on the passed path and commit hash', () => {
+ return services.getFiles(TEST_PROJECT_ID, TEST_COMMIT_SHA).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`,
+ expect.any(Object),
+ );
+ expect(data).toEqual([TEST_FILE_PATH]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js
new file mode 100644
index 00000000000..c970ccc535d
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js
@@ -0,0 +1,130 @@
+import * as badgeHelper from '~/image_diff/helpers/badge_helper';
+import * as mockData from '../mock_data';
+
+describe('badge helper', () => {
+ const { coordinate, noteId, badgeText, badgeNumber } = mockData;
+ let containerEl;
+ let buttonEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('createImageBadge', () => {
+ beforeEach(() => {
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate);
+ });
+
+ it('should create button', () => {
+ expect(buttonEl.tagName).toEqual('BUTTON');
+ expect(buttonEl.getAttribute('type')).toEqual('button');
+ });
+
+ it('should set disabled attribute', () => {
+ expect(buttonEl.hasAttribute('disabled')).toEqual(true);
+ });
+
+ it('should set noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ describe('classNames', () => {
+ it('should set .js-image-badge by default', () => {
+ expect(buttonEl.className).toEqual('js-image-badge');
+ });
+
+ it('should add additional class names if parameter is passed', () => {
+ const classNames = ['first-class', 'second-class'];
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames);
+
+ expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' '));
+ });
+ });
+ });
+
+ describe('addImageBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageBadge(containerEl, {
+ coordinate,
+ badgeText,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should appends button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should add badge classes', () => {
+ expect(buttonEl.className).toContain('badge badge-pill');
+ });
+
+ it('should set the badge text', () => {
+ expect(buttonEl.textContent).toEqual(badgeText);
+ });
+
+ it('should set the button coordinates', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should set the button noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+ });
+
+ describe('addImageCommentBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageCommentBadge(containerEl, {
+ coordinate,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append icon button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should create icon comment button', () => {
+ const iconEl = buttonEl.querySelector('svg');
+
+ expect(iconEl).toBeDefined();
+ });
+ });
+
+ describe('addAvatarBadge', () => {
+ let avatarBadgeEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div id="${noteId}">
+ <div class="badge hidden">
+ </div>
+ </div>
+ `;
+
+ badgeHelper.addAvatarBadge(containerEl, {
+ detail: {
+ noteId,
+ badgeNumber,
+ },
+ });
+ avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
+ });
+
+ it('should update badge number', () => {
+ expect(avatarBadgeEl.textContent).toEqual(badgeNumber.toString());
+ });
+
+ it('should remove hidden class', () => {
+ expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
new file mode 100644
index 00000000000..395bb7de362
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
@@ -0,0 +1,144 @@
+import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
+import * as mockData from '../mock_data';
+
+describe('commentIndicatorHelper', () => {
+ const { coordinate } = mockData;
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('addCommentIndicator', () => {
+ let buttonEl;
+
+ beforeEach(() => {
+ commentIndicatorHelper.addCommentIndicator(containerEl, coordinate);
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ describe('button', () => {
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should contain image-comment-dark svg', () => {
+ const svgEl = buttonEl.querySelector('svg');
+
+ expect(svgEl).toBeDefined();
+
+ const svgLink = svgEl.querySelector('use').getAttribute('xlink:href');
+
+ expect(svgLink.indexOf('image-comment-dark')).not.toBe(-1);
+ });
+ });
+ });
+
+ describe('removeCommentIndicator', () => {
+ it('should return removed false if there is no comment-indicator', () => {
+ const result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+
+ expect(result.removed).toEqual(false);
+ });
+
+ describe('has comment indicator', () => {
+ let result;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ `;
+ result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ });
+
+ it('should remove comment indicator', () => {
+ expect(containerEl.querySelector('.comment-indicator')).toBeNull();
+ });
+
+ it('should return removed true', () => {
+ expect(result.removed).toEqual(true);
+ });
+
+ it('should return indicator meta', () => {
+ expect(result.x).toEqual(coordinate.x);
+ expect(result.y).toEqual(coordinate.y);
+ expect(result.image).toBeDefined();
+ expect(result.image.width).toBeDefined();
+ expect(result.image.height).toBeDefined();
+ });
+ });
+ });
+
+ describe('showCommentIndicator', () => {
+ describe('commentIndicator exists', () => {
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <button class="comment-indicator"></button>
+ `;
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should set commentIndicator coordinates', () => {
+ const commentIndicatorEl = containerEl.querySelector('.comment-indicator');
+
+ expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+
+ describe('commentIndicator does not exist', () => {
+ beforeEach(() => {
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should addCommentIndicator', () => {
+ const buttonEl = containerEl.querySelector('.comment-indicator');
+
+ expect(buttonEl).toBeDefined();
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+ });
+
+ describe('commentIndicatorOnClick', () => {
+ let event;
+ let textAreaEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="diff-viewer">
+ <button></button>
+ <div class="note-container">
+ <textarea class="note-textarea"></textarea>
+ </div>
+ </div>
+ `;
+ textAreaEl = containerEl.querySelector('textarea');
+
+ event = {
+ stopPropagation: () => {},
+ currentTarget: containerEl.querySelector('button'),
+ };
+
+ jest.spyOn(event, 'stopPropagation').mockImplementation(() => {});
+ jest.spyOn(textAreaEl, 'focus').mockImplementation(() => {});
+ commentIndicatorHelper.commentIndicatorOnClick(event);
+ });
+
+ it('should stopPropagation', () => {
+ expect(event.stopPropagation).toHaveBeenCalled();
+ });
+
+ it('should focus textAreaEl', () => {
+ expect(textAreaEl.focus).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js
new file mode 100644
index 00000000000..9357d626bbe
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js
@@ -0,0 +1,120 @@
+import * as domHelper from '~/image_diff/helpers/dom_helper';
+import * as mockData from '../mock_data';
+
+describe('domHelper', () => {
+ const { imageMeta, badgeNumber } = mockData;
+
+ describe('setPositionDataAttribute', () => {
+ let containerEl;
+ let attributeAfterCall;
+ const position = {
+ myProperty: 'myProperty',
+ };
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ containerEl.dataset.position = JSON.stringify(position);
+ domHelper.setPositionDataAttribute(containerEl, imageMeta);
+ attributeAfterCall = JSON.parse(containerEl.dataset.position);
+ });
+
+ it('should set x, y, width, height', () => {
+ expect(attributeAfterCall.x).toEqual(imageMeta.x);
+ expect(attributeAfterCall.y).toEqual(imageMeta.y);
+ expect(attributeAfterCall.width).toEqual(imageMeta.width);
+ expect(attributeAfterCall.height).toEqual(imageMeta.height);
+ });
+
+ it('should not override other properties', () => {
+ expect(attributeAfterCall.myProperty).toEqual('myProperty');
+ });
+ });
+
+ describe('updateDiscussionAvatarBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <a href="#" class="image-diff-avatar-link">
+ <div class="badge"></div>
+ </a>
+ `;
+ domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update avatar badge number', () => {
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('updateDiscussionBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <div class="badge"></div>
+ `;
+ domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update discussion badge number', () => {
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('toggleCollapsed', () => {
+ let element;
+ let discussionNotesEl;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.innerHTML = `
+ <div class="discussion-notes">
+ <button></button>
+ <form class="discussion-form"></form>
+ </div>
+ `;
+ discussionNotesEl = element.querySelector('.discussion-notes');
+ });
+
+ describe('not collapsed', () => {
+ beforeEach(() => {
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should add collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true);
+ });
+
+ it('should force formEl to display none', () => {
+ const formEl = element.querySelector('.discussion-form');
+
+ expect(formEl.style.display).toEqual('none');
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(() => {
+ discussionNotesEl.classList.add('collapsed');
+
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should remove collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false);
+ });
+
+ it('should force formEl to display block', () => {
+ const formEl = element.querySelector('.discussion-form');
+
+ expect(formEl.style.display).toEqual('block');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/helpers/utils_helper_spec.js b/spec/frontend/image_diff/helpers/utils_helper_spec.js
new file mode 100644
index 00000000000..3b6378be883
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/utils_helper_spec.js
@@ -0,0 +1,152 @@
+import * as utilsHelper from '~/image_diff/helpers/utils_helper';
+import ImageBadge from '~/image_diff/image_badge';
+import * as mockData from '../mock_data';
+
+describe('utilsHelper', () => {
+ const { noteId, discussionId, image, imageProperties, imageMeta } = mockData;
+
+ describe('resizeCoordinatesToImageElement', () => {
+ let result;
+
+ beforeEach(() => {
+ result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta);
+ });
+
+ it('should return x based on widthRatio', () => {
+ expect(result.x).toEqual(imageMeta.x * 0.5);
+ });
+
+ it('should return y based on heightRatio', () => {
+ expect(result.y).toEqual(imageMeta.y * 0.5);
+ });
+
+ it('should return image width', () => {
+ expect(result.width).toEqual(image.width);
+ });
+
+ it('should return image height', () => {
+ expect(result.height).toEqual(image.height);
+ });
+ });
+
+ describe('generateBadgeFromDiscussionDOM', () => {
+ let discussionEl;
+ let result;
+
+ beforeEach(() => {
+ const imageFrameEl = document.createElement('div');
+ imageFrameEl.innerHTML = `
+ <img src="${gl.TEST_HOST}/image.png">
+ `;
+ discussionEl = document.createElement('div');
+ discussionEl.dataset.discussionId = discussionId;
+ discussionEl.innerHTML = `
+ <div class="note" id="${noteId}"></div>
+ `;
+ discussionEl.dataset.position = JSON.stringify(imageMeta);
+ result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl);
+ });
+
+ it('should return actual image properties', () => {
+ const { actual } = result;
+
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should return browser image properties', () => {
+ const { browser } = result;
+
+ expect(browser.x).toBeDefined();
+ expect(browser.y).toBeDefined();
+ expect(browser.width).toBeDefined();
+ expect(browser.height).toBeDefined();
+ });
+
+ it('should return instance of ImageBadge', () => {
+ expect(result instanceof ImageBadge).toEqual(true);
+ });
+
+ it('should return noteId', () => {
+ expect(result.noteId).toEqual(noteId);
+ });
+
+ it('should return discussionId', () => {
+ expect(result.discussionId).toEqual(discussionId);
+ });
+ });
+
+ describe('getTargetSelection', () => {
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = {
+ querySelector: () => imageProperties,
+ };
+ });
+
+ function generateEvent(offsetX, offsetY) {
+ return {
+ currentTarget: containerEl,
+ offsetX,
+ offsetY,
+ };
+ }
+
+ it('should return browser properties', () => {
+ const event = generateEvent(25, 25);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { browser } = result;
+
+ expect(browser.x).toEqual(event.offsetX);
+ expect(browser.y).toEqual(event.offsetY);
+ expect(browser.width).toEqual(imageProperties.width);
+ expect(browser.height).toEqual(imageProperties.height);
+ });
+
+ it('should return resized actual image properties', () => {
+ const event = generateEvent(50, 50);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { actual } = result;
+
+ expect(actual.x).toEqual(100);
+ expect(actual.y).toEqual(100);
+ expect(actual.width).toEqual(imageProperties.naturalWidth);
+ expect(actual.height).toEqual(imageProperties.naturalHeight);
+ });
+
+ describe('normalize coordinates', () => {
+ it('should return x = 0 if x < 0', () => {
+ const event = generateEvent(-5, 50);
+ const result = utilsHelper.getTargetSelection(event);
+
+ expect(result.browser.x).toEqual(0);
+ });
+
+ it('should return x = width if x > width', () => {
+ const event = generateEvent(1000, 50);
+ const result = utilsHelper.getTargetSelection(event);
+
+ expect(result.browser.x).toEqual(imageProperties.width);
+ });
+
+ it('should return y = 0 if y < 0', () => {
+ const event = generateEvent(50, -10);
+ const result = utilsHelper.getTargetSelection(event);
+
+ expect(result.browser.y).toEqual(0);
+ });
+
+ it('should return y = height if y > height', () => {
+ const event = generateEvent(50, 1000);
+ const result = utilsHelper.getTargetSelection(event);
+
+ expect(result.browser.y).toEqual(imageProperties.height);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/image_badge_spec.js b/spec/frontend/image_diff/image_badge_spec.js
new file mode 100644
index 00000000000..a11b50ead47
--- /dev/null
+++ b/spec/frontend/image_diff/image_badge_spec.js
@@ -0,0 +1,84 @@
+import ImageBadge from '~/image_diff/image_badge';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageBadge', () => {
+ const { noteId, discussionId, imageMeta } = mockData;
+ const options = {
+ noteId,
+ discussionId,
+ };
+
+ it('should save actual property', () => {
+ const imageBadge = new ImageBadge({ ...options, actual: imageMeta });
+
+ const { actual } = imageBadge;
+
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should save browser property', () => {
+ const imageBadge = new ImageBadge({ ...options, browser: imageMeta });
+
+ const { browser } = imageBadge;
+
+ expect(browser.x).toEqual(imageMeta.x);
+ expect(browser.y).toEqual(imageMeta.y);
+ expect(browser.width).toEqual(imageMeta.width);
+ expect(browser.height).toEqual(imageMeta.height);
+ });
+
+ it('should save noteId', () => {
+ const imageBadge = new ImageBadge(options);
+
+ expect(imageBadge.noteId).toEqual(noteId);
+ });
+
+ it('should save discussionId', () => {
+ const imageBadge = new ImageBadge(options);
+
+ expect(imageBadge.discussionId).toEqual(discussionId);
+ });
+
+ describe('default values', () => {
+ let imageBadge;
+
+ beforeEach(() => {
+ imageBadge = new ImageBadge(options);
+ });
+
+ it('should return defaultimageMeta if actual property is not provided', () => {
+ const { actual } = imageBadge;
+
+ expect(actual.x).toEqual(0);
+ expect(actual.y).toEqual(0);
+ expect(actual.width).toEqual(0);
+ expect(actual.height).toEqual(0);
+ });
+
+ it('should return defaultimageMeta if browser property is not provided', () => {
+ const { browser } = imageBadge;
+
+ expect(browser.x).toEqual(0);
+ expect(browser.y).toEqual(0);
+ expect(browser.width).toEqual(0);
+ expect(browser.height).toEqual(0);
+ });
+ });
+
+ describe('imageEl property is provided and not browser property', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(true);
+ });
+
+ it('should generate browser property', () => {
+ const imageBadge = new ImageBadge({ ...options, imageEl: document.createElement('img') });
+
+ expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled();
+ expect(imageBadge.browser).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
new file mode 100644
index 00000000000..c15718b5106
--- /dev/null
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -0,0 +1,361 @@
+import ImageDiff from '~/image_diff/image_diff';
+import * as imageUtility from '~/lib/utils/image_utility';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageDiff', () => {
+ let element;
+ let imageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="diff-file">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ <div class="comment-indicator"></div>
+ <div id="badge-1" class="badge">1</div>
+ <div id="badge-2" class="badge">2</div>
+ <div id="badge-3" class="badge">3</div>
+ </div>
+ <div class="note-container">
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ describe('constructor', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ renderCommentBadge: true,
+ });
+ });
+
+ it('should set el', () => {
+ expect(imageDiff.el).toEqual(element);
+ });
+
+ it('should set canCreateNote', () => {
+ expect(imageDiff.canCreateNote).toEqual(true);
+ });
+
+ it('should set renderCommentBadge', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(true);
+ });
+
+ it('should set $noteContainer', () => {
+ expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container'));
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element);
+ });
+
+ it('should set canCreateNote as false', () => {
+ expect(imageDiff.canCreateNote).toEqual(false);
+ });
+
+ it('should set renderCommentBadge as false', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+ });
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.init();
+ });
+
+ it('should set imageFrameEl', () => {
+ expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame'));
+ });
+
+ it('should set imageEl', () => {
+ expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img'));
+ });
+
+ it('should call bindEvents', () => {
+ expect(imageDiff.bindEvents).toHaveBeenCalled();
+ });
+ });
+
+ describe('bindEvents', () => {
+ let imageEl;
+
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'toggleCollapsed').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'commentIndicatorOnClick').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'imageClicked').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'addBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'removeBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
+ imageEl = element.querySelector('.diff-file .js-image-frame img');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click event delegation to js-diff-notes-toggle', () => {
+ element.querySelector('.js-diff-notes-toggle').click();
+
+ expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled();
+ });
+
+ it('should register click event delegation to comment-indicator', () => {
+ element.querySelector('.comment-indicator').click();
+
+ expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled();
+ });
+ });
+
+ describe('image not loaded', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should registers load eventListener', () => {
+ const loadEvent = new Event('load');
+ imageEl.dispatchEvent(loadEvent);
+
+ expect(imageDiff.renderBadges).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ });
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.imageClicked).toHaveBeenCalled();
+ });
+
+ it('should register blur.imageDiff event', () => {
+ const event = new CustomEvent('blur.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should register addBadge.imageDiff event', () => {
+ const event = new CustomEvent('addBadge.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.addBadge).toHaveBeenCalled();
+ });
+
+ it('should register removeBadge.imageDiff event', () => {
+ const event = new CustomEvent('removeBadge.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.removeBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote is false', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should not register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.imageClicked).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('imageClicked', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'getTargetSelection').mockReturnValue({
+ actual: {},
+ browser: {},
+ });
+ jest.spyOn(imageDiffHelper, 'setPositionDataAttribute').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageClicked({
+ detail: {
+ currentTarget: {},
+ },
+ });
+ });
+
+ it('should call getTargetSelection', () => {
+ expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled();
+ });
+
+ it('should call setPositionDataAttribute', () => {
+ expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled();
+ });
+
+ it('should call showCommentIndicator', () => {
+ expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderBadges', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'renderBadge').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadges();
+ });
+
+ it('should call renderBadge for each discussionEl', () => {
+ const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+
+ expect(imageDiff.renderBadge.mock.calls.length).toEqual(discussionEls.length);
+ });
+ });
+
+ describe('renderBadge', () => {
+ let discussionEls;
+
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addImageCommentBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').mockReturnValue({
+ browser: {},
+ noteId: 'noteId',
+ });
+ discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should populate imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ describe('renderCommentBadge', () => {
+ beforeEach(() => {
+ imageDiff.renderCommentBadge = true;
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should call addImageCommentBadge', () => {
+ expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderCommentBadge is false', () => {
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addBadge', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addAvatarBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.addBadge({
+ detail: {
+ x: 0,
+ y: 1,
+ width: 25,
+ height: 50,
+ noteId: 'noteId',
+ discussionId: 'discussionId',
+ },
+ });
+ });
+
+ it('should add imageBadge to imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+
+ it('should call addAvatarBadge', () => {
+ expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ describe('removeBadge', () => {
+ beforeEach(() => {
+ const { imageMeta } = mockData;
+
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.removeBadge({
+ detail: {
+ badgeNumber: 2,
+ },
+ });
+ });
+
+ describe('cascade badge count', () => {
+ it('should update next imageBadgeEl value', () => {
+ const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
+
+ expect(imageBadgeEls[0].textContent).toEqual('1');
+ expect(imageBadgeEls[1].textContent).toEqual('2');
+ expect(imageBadgeEls.length).toEqual(2);
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionAvatarBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ it('should remove badge from imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(2);
+ });
+
+ it('should remove imageBadgeEl', () => {
+ expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/mock_data.js b/spec/frontend/image_diff/mock_data.js
new file mode 100644
index 00000000000..a0d1732dd0a
--- /dev/null
+++ b/spec/frontend/image_diff/mock_data.js
@@ -0,0 +1,28 @@
+export const noteId = 'noteId';
+export const discussionId = 'discussionId';
+export const badgeText = 'badgeText';
+export const badgeNumber = 5;
+
+export const coordinate = {
+ x: 100,
+ y: 100,
+};
+
+export const image = {
+ width: 100,
+ height: 100,
+};
+
+export const imageProperties = {
+ width: image.width,
+ height: image.height,
+ naturalWidth: image.width * 2,
+ naturalHeight: image.height * 2,
+};
+
+export const imageMeta = {
+ x: coordinate.x,
+ y: coordinate.y,
+ width: imageProperties.naturalWidth,
+ height: imageProperties.naturalHeight,
+};
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
new file mode 100644
index 00000000000..f2a7b7f8406
--- /dev/null
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -0,0 +1,356 @@
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageDiff from '~/image_diff/image_diff';
+import { viewTypes } from '~/image_diff/view_types';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('ReplacedImageDiff', () => {
+ let element;
+ let replacedImageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="two-up">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="swipe">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="onion-skin">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="view-modes-menu">
+ <div class="two-up">2-up</div>
+ <div class="swipe">Swipe</div>
+ <div class="onion-skin">Onion skin</div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ function setupImageFrameEls() {
+ replacedImageDiff.imageFrameEls = [];
+ replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector(
+ '.two-up .js-image-frame',
+ );
+ replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector(
+ '.swipe .js-image-frame',
+ );
+ replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector(
+ '.onion-skin .js-image-frame',
+ );
+ }
+
+ function setupViewModesEls() {
+ replacedImageDiff.viewModesEls = [];
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector(
+ '.view-modes-menu .two-up',
+ );
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector(
+ '.view-modes-menu .swipe',
+ );
+ replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector(
+ '.view-modes-menu .onion-skin',
+ );
+ }
+
+ function setupImageEls() {
+ replacedImageDiff.imageEls = [];
+ replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img');
+ replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img');
+ replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img');
+ }
+
+ it('should extend ImageDiff', () => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ expect(replacedImageDiff instanceof ImageDiff).toEqual(true);
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'generateImageEls').mockImplementation(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.init();
+ });
+
+ it('should set imageFrameEls', () => {
+ const { imageFrameEls } = replacedImageDiff;
+
+ expect(imageFrameEls).toBeDefined();
+ expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(
+ element.querySelector('.two-up .js-image-frame'),
+ );
+
+ expect(imageFrameEls[viewTypes.SWIPE]).toEqual(
+ element.querySelector('.swipe .js-image-frame'),
+ );
+
+ expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(
+ element.querySelector('.onion-skin .js-image-frame'),
+ );
+ });
+
+ it('should set viewModesEls', () => {
+ const { viewModesEls } = replacedImageDiff;
+
+ expect(viewModesEls).toBeDefined();
+ expect(viewModesEls[viewTypes.TWO_UP]).toEqual(
+ element.querySelector('.view-modes-menu .two-up'),
+ );
+
+ expect(viewModesEls[viewTypes.SWIPE]).toEqual(
+ element.querySelector('.view-modes-menu .swipe'),
+ );
+
+ expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(
+ element.querySelector('.view-modes-menu .onion-skin'),
+ );
+ });
+
+ it('should generateImageEls', () => {
+ expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled();
+ });
+
+ it('should bindEvents', () => {
+ expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ describe('currentView', () => {
+ it('should set currentView', () => {
+ replacedImageDiff.init(viewTypes.ONION_SKIN);
+
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should default to viewTypes.TWO_UP', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP);
+ });
+ });
+ });
+
+ describe('generateImageEls', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element, {
+ canCreateNote: false,
+ renderCommentBadge: false,
+ });
+
+ setupImageFrameEls();
+ });
+
+ it('should set imageEls', () => {
+ replacedImageDiff.generateImageEls();
+ const { imageEls } = replacedImageDiff;
+
+ expect(imageEls).toBeDefined();
+ expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img'));
+ expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img'));
+ expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img'));
+ });
+ });
+
+ describe('bindEvents', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ setupViewModesEls();
+ });
+
+ it('should call super.bindEvents', () => {
+ replacedImageDiff.bindEvents();
+
+ expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ it('should register click eventlistener to 2-up view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.TWO_UP);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
+ });
+
+ it('should register click eventlistener to swipe view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+
+ it('should register click eventlistener to onion skin view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+ });
+
+ describe('getters', () => {
+ describe('imageEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageEls();
+ });
+
+ it('should return imageEl based on currentView', () => {
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img'));
+
+ replacedImageDiff.currentView = viewTypes.SWIPE;
+
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img'));
+ });
+ });
+
+ describe('imageFrameEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageFrameEls();
+ });
+
+ it('should return imageFrameEl based on currentView', () => {
+ expect(replacedImageDiff.imageFrameEl).toEqual(
+ element.querySelector('.two-up .js-image-frame'),
+ );
+
+ replacedImageDiff.currentView = viewTypes.ONION_SKIN;
+
+ expect(replacedImageDiff.imageFrameEl).toEqual(
+ element.querySelector('.onion-skin .js-image-frame'),
+ );
+ });
+ });
+ });
+
+ describe('changeView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockReturnValue({
+ removed: false,
+ });
+ setupImageFrameEls();
+ });
+
+ describe('invalid viewType', () => {
+ beforeEach(() => {
+ replacedImageDiff.changeView('some-view-name');
+ });
+
+ it('should not call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('valid viewType', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderNewView').mockImplementation(() => {});
+ replacedImageDiff.changeView(viewTypes.ONION_SKIN);
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('should call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should update currentView to newView', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should clear imageBadges', () => {
+ expect(replacedImageDiff.imageBadges.length).toEqual(0);
+ });
+
+ it('should call renderNewView', () => {
+ jest.advanceTimersByTime(251);
+
+ expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('renderNewView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ });
+
+ it('should call renderBadges', () => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
+
+ replacedImageDiff.renderNewView({
+ removed: false,
+ });
+
+ expect(replacedImageDiff.renderBadges).toHaveBeenCalled();
+ });
+
+ describe('removeIndicator', () => {
+ const indicator = {
+ removed: true,
+ x: 0,
+ y: 1,
+ image: {
+ width: 50,
+ height: 100,
+ },
+ };
+
+ beforeEach(() => {
+ setupImageEls();
+ setupImageFrameEls();
+ });
+
+ it('should pass showCommentIndicator normalized indicator values', done => {
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
+ jest
+ .spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement')
+ .mockImplementation((imageEl, meta) => {
+ expect(meta.x).toEqual(indicator.x);
+ expect(meta.y).toEqual(indicator.y);
+ expect(meta.width).toEqual(indicator.image.width);
+ expect(meta.height).toEqual(indicator.image.height);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+
+ it('should call showCommentIndicator', done => {
+ const normalized = {
+ normalized: true,
+ };
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(normalized);
+ jest
+ .spyOn(imageDiffHelper, 'showCommentIndicator')
+ .mockImplementation((imageFrameEl, normalizedIndicator) => {
+ expect(normalizedIndicator).toEqual(normalized);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
new file mode 100644
index 00000000000..f656bb0b60d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+describe('CI Badge Link Component', () => {
+ let CIBadge;
+ let vm;
+
+ const statuses = {
+ canceled: {
+ text: 'canceled',
+ label: 'canceled',
+ group: 'canceled',
+ icon: 'status_canceled',
+ details_path: 'status/canceled',
+ },
+ created: {
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ icon: 'status_created',
+ details_path: 'status/created',
+ },
+ failed: {
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ icon: 'status_failed',
+ details_path: 'status/failed',
+ },
+ manual: {
+ text: 'manual',
+ label: 'manual action',
+ group: 'manual',
+ icon: 'status_manual',
+ details_path: 'status/manual',
+ },
+ pending: {
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ icon: 'status_pending',
+ details_path: 'status/pending',
+ },
+ running: {
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ icon: 'status_running',
+ details_path: 'status/running',
+ },
+ skipped: {
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ icon: 'status_skipped',
+ details_path: 'status/skipped',
+ },
+ success_warining: {
+ text: 'passed',
+ label: 'passed',
+ group: 'success-with-warnings',
+ icon: 'status_warning',
+ details_path: 'status/warning',
+ },
+ success: {
+ text: 'passed',
+ label: 'passed',
+ group: 'passed',
+ icon: 'status_success',
+ details_path: 'status/passed',
+ },
+ };
+
+ beforeEach(() => {
+ CIBadge = Vue.extend(ciBadge);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render each status badge', () => {
+ Object.keys(statuses).map(status => {
+ vm = mountComponent(CIBadge, { status: statuses[status] });
+
+ expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
+ expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
+ expect(vm.$el.getAttribute('class')).toContain(`ci-status ci-${statuses[status].group}`);
+ expect(vm.$el.querySelector('svg')).toBeDefined();
+ return vm;
+ });
+ });
+
+ it('should not render label', () => {
+ vm = mountComponent(CIBadge, { status: statuses.canceled, showText: false });
+
+ expect(vm.$el.textContent.trim()).toEqual('');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
new file mode 100644
index 00000000000..63afe631063
--- /dev/null
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
+
+describe('CI Icon component', () => {
+ const Component = Vue.extend(ciIcon);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a span element with an svg', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_success',
+ },
+ });
+
+ expect(vm.$el.tagName).toEqual('SPAN');
+ expect(vm.$el.querySelector('span > svg')).toBeDefined();
+ });
+
+ it('should render a success status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_success',
+ group: 'success',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
+ });
+
+ it('should render a failed status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
+ });
+
+ it('should render success with warnings status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_warning',
+ group: 'warning',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
+ });
+
+ it('should render pending status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_pending',
+ group: 'pending',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
+ });
+
+ it('should render running status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_running',
+ group: 'running',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
+ });
+
+ it('should render created status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_created',
+ group: 'created',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
+ });
+
+ it('should render skipped status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_skipped',
+ group: 'skipped',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
+ });
+
+ it('should render canceled status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_canceled',
+ group: 'canceled',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
+ });
+
+ it('should render status for manual action', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_manual',
+ group: 'manual',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
new file mode 100644
index 00000000000..b0563f2f6de
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -0,0 +1,123 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import '~/behaviors/markdown/render_gfm';
+
+describe('ContentViewer', () => {
+ let vm;
+ let mock;
+
+ function createComponent(props) {
+ const ContentViewer = Vue.extend(contentViewer);
+ vm = mountComponent(ContentViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ if (mock) mock.restore();
+ });
+
+ it('markdown preview renders + loads rendered markdown from server', done => {
+ mock = new MockAdapter(axios);
+ mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
+ body: '<b>testing</b>',
+ });
+
+ createComponent({
+ path: 'test.md',
+ content: '* Test',
+ projectPath: 'testproject',
+ type: 'markdown',
+ });
+
+ waitForPromises()
+ .then(() => {
+ expect(vm.$el.querySelector('.md-previewer').textContent).toContain('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders image preview', done => {
+ createComponent({
+ path: GREEN_BOX_IMAGE_URL,
+ fileSize: 1024,
+ type: 'image',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders fallback download control', done => {
+ createComponent({
+ path: 'somepath/test.abc',
+ fileSize: 1024,
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(
+ vm.$el
+ .querySelector('.file-info')
+ .textContent.trim()
+ .replace(/\s+/, ' '),
+ ).toEqual('test.abc (1.00 KiB)');
+
+ expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders fallback download control for file with a data URL path properly', done => {
+ createComponent({
+ path: 'data:application/octet-stream;base64,U0VMRUNUICfEhHNnc2cnIGZyb20gVGFibGVuYW1lOwoK',
+ filePath: 'somepath/test.abc',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toEqual('test.abc');
+ expect(vm.$el.querySelector('.btn.btn-default')).toHaveAttr('download', 'test.abc');
+ expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('markdown preview receives the file path as a parameter', done => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(axios, 'post');
+ mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, {
+ body: '<b>testing</b>',
+ });
+
+ createComponent({
+ path: 'test.md',
+ content: '* Test',
+ projectPath: 'testproject',
+ type: 'markdown',
+ filePath: 'foo/test.md',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(axios.post).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/testproject/preview_markdown`,
+ { path: 'foo/test.md', text: '* Test' },
+ expect.any(Object),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
new file mode 100644
index 00000000000..636508be6b6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+
+describe('DiffViewer', () => {
+ const requiredProps = {
+ diffMode: 'replaced',
+ diffViewerMode: 'image',
+ newPath: GREEN_BOX_IMAGE_URL,
+ newSha: 'ABC',
+ oldPath: RED_BOX_IMAGE_URL,
+ oldSha: 'DEF',
+ };
+ let vm;
+
+ function createComponent(props) {
+ const DiffViewer = Vue.extend(diffViewer);
+
+ vm = mountComponent(DiffViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff', done => {
+ window.gon = {
+ relative_url_root: '',
+ };
+
+ createComponent({ ...requiredProps, projectPath: '' });
+
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
+ `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
+ );
+
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
+ `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
+ );
+
+ done();
+ });
+ });
+
+ it('renders fallback download diff display', done => {
+ createComponent({
+ ...requiredProps,
+ diffViewerMode: 'added',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ });
+
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
+ 'testold.abc',
+ );
+
+ expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+
+ it('renders renamed component', () => {
+ createComponent({
+ ...requiredProps,
+ diffMode: 'renamed',
+ diffViewerMode: 'renamed',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ });
+
+ expect(vm.$el.textContent).toContain('File moved');
+ });
+
+ it('renders mode changed component', () => {
+ createComponent({
+ ...requiredProps,
+ diffMode: 'mode_changed',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ aMode: '123',
+ bMode: '321',
+ });
+
+ expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
new file mode 100644
index 00000000000..892a96b76fd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+
+import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
+import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+const defaultLabel = 'Select';
+const customLabel = 'Select project';
+
+const createComponent = (props, slots = {}) => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponentWithSlots(Component, { props, slots });
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(defaultLabel);
+ });
+
+ it('returns custom toggle text when provided via props', () => {
+ const vmEmptyLabels = createComponent({ toggleText: customLabel });
+
+ expect(vmEmptyLabels.toggleText).toBe(customLabel);
+ vmEmptyLabels.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
+
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+
+ it('renders slot, if default slot exists', () => {
+ vm = createComponent(
+ {},
+ {
+ default: ['Lorem Ipsum Dolar'],
+ },
+ );
+
+ expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull();
+ expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
new file mode 100644
index 00000000000..30b8e869aab
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+
+import { mockLabels } from './mock_data';
+
+const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
+ const Component = Vue.extend(dropdownHiddenInputComponent);
+
+ return mountComponent(Component, {
+ name,
+ value,
+ });
+};
+
+describe('DropdownHiddenInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element of type `hidden`', () => {
+ expect(vm.$el.nodeName).toBe('INPUT');
+ expect(vm.$el.getAttribute('type')).toBe('hidden');
+ expect(vm.$el.getAttribute('name')).toBe(vm.name);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dropdown/mock_data.js b/spec/frontend/vue_shared/components/dropdown/mock_data.js
new file mode 100644
index 00000000000..b09d42da401
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/mock_data.js
@@ -0,0 +1,11 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+export default mockLabels;
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
new file mode 100644
index 00000000000..63f2614106d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import { file } from 'jest/ide/helpers';
+import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
+import createComponent from 'helpers/vue_mount_component_helper';
+
+describe('File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
new file mode 100644
index 00000000000..87cafa0bb8c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
@@ -0,0 +1,190 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import component from '~/vue_shared/components/filtered_search_dropdown.vue';
+
+describe('Filtered search dropdown', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with an empty array of items', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [],
+ filterKey: '',
+ });
+ });
+
+ it('renders empty list', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
+ });
+
+ it('renders filter input', () => {
+ expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
+ });
+ });
+
+ describe('when visible numbers is less than the items length', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
+ visibleItems: 2,
+ filterKey: 'title',
+ });
+ });
+
+ it('it renders only the maximum number provided', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
+ });
+ });
+
+ describe('when visible number is bigger than the items length', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
+ filterKey: 'title',
+ });
+ });
+
+ it('it renders the full list of items the maximum number provided', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
+ });
+ });
+
+ describe('while filtering', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ });
+ });
+
+ it('updates the results to match the typed value', done => {
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
+ done();
+ });
+ });
+
+ describe('when no value matches the typed one', () => {
+ it('does not render any result', done => {
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('with create mode enabled', () => {
+ describe('when there are no matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ showCreateMode: true,
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('renders a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
+ done();
+ });
+ });
+
+ it('renders computed button text', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
+ 'Create eleven',
+ );
+ done();
+ });
+ });
+
+ describe('on click create button', () => {
+ it('emits createItem event with the filter', done => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-dropdown-create-button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('when there are matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ showCreateMode: true,
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('does not render a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('with create mode disabled', () => {
+ describe('when there are no matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('does not render a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
new file mode 100644
index 00000000000..365c9fad478
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -0,0 +1,83 @@
+import mountComponent from 'helpers/vue_mount_component_helper';
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+describe('GlCountdown', () => {
+ const Component = Vue.extend(GlCountdown);
+ let vm;
+ let now = '2000-01-01T00:00:00Z';
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime());
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jest.clearAllTimers();
+ });
+
+ describe('when there is time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '2000-01-01T01:02:03Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays remaining time', () => {
+ expect(vm.$el.textContent).toContain('01:02:03');
+ });
+
+ it('updates remaining time', done => {
+ now = '2000-01-01T00:00:01Z';
+ jest.advanceTimersByTime(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.textContent).toContain('01:02:02');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('when there is no time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '1900-01-01T00:00:00Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays 00:00:00', () => {
+ expect(vm.$el.textContent).toContain('00:00:00');
+ });
+ });
+
+ describe('when an invalid date is passed', () => {
+ beforeEach(() => {
+ Vue.config.warnHandler = jest.fn();
+ });
+
+ afterEach(() => {
+ Vue.config.warnHandler = null;
+ });
+
+ it('throws a validation error', () => {
+ vm = mountComponent(Component, {
+ endDateString: 'this is invalid',
+ });
+
+ expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
+ const [errorMessage] = Vue.config.warnHandler.mock.calls[0];
+
+ expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
new file mode 100644
index 00000000000..216563165d6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
+
+describe('Header CI Component', () => {
+ let HeaderCi;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderCi = Vue.extend(headerCi);
+ props = {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ hasSidebarButton: true,
+ };
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
+
+ describe('render', () => {
+ beforeEach(() => {
+ vm = mountComponent(HeaderCi, props);
+ });
+
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-failed').getAttribute('href')).toEqual(
+ props.status.details_path,
+ );
+ });
+
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
+
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
+
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
+ });
+
+ it('should render sidebar toggle button', () => {
+ expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
+ });
+
+ it('should not render header action buttons when empty', () => {
+ expect(findActionButtons()).toBeNull();
+ });
+ });
+
+ describe('slot', () => {
+ it('should render header action buttons', () => {
+ vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
+
+ const buttons = findActionButtons();
+
+ expect(buttons).not.toBeNull();
+ expect(buttons.textContent).toEqual('Test Actions');
+ });
+ });
+
+ describe('shouldRenderTriggeredLabel', () => {
+ it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
+ vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
+
+ expect(vm.$el.textContent).toContain('created');
+ expect(vm.$el.textContent).not.toContain('triggered');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
new file mode 100644
index 00000000000..e7c31014bfc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+
+describe('toolbar', () => {
+ let vm;
+ const Toolbar = Vue.extend(toolbar);
+ const props = {
+ markdownDocsPath: '',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user can attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, props);
+ });
+
+ it('should render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
+ });
+ });
+
+ describe('user cannot attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, { ...props, canAttachFile: false });
+ });
+
+ it('should not render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
new file mode 100644
index 00000000000..561456d614e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+
+describe('navigation tabs component', () => {
+ let vm;
+ let Component;
+ let data;
+
+ beforeEach(() => {
+ data = [
+ {
+ name: 'All',
+ scope: 'all',
+ count: 1,
+ isActive: true,
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: 0,
+ isActive: false,
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ isActive: false,
+ },
+ ];
+
+ Component = Vue.extend(navigationTabs);
+ vm = mountComponent(Component, { tabs: data, scope: 'pipelines' });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render tabs', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
+ });
+
+ it('should render active tab', () => {
+ expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
+ });
+
+ it('should render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
+ expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual(
+ '0',
+ );
+ });
+
+ it('should not render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
+ });
+
+ it('should trigger onTabClick', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.$el.querySelector('.js-pipelines-tab-pending').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
new file mode 100644
index 00000000000..867bf88ff50
--- /dev/null
+++ b/spec/frontend/vue_shared/components/pikaday_spec.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import datePicker from '~/vue_shared/components/pikaday.vue';
+
+describe('datePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const DatePicker = Vue.extend(datePicker);
+ vm = mountComponent(DatePicker, {
+ label: 'label',
+ });
+ });
+
+ it('should render label text', () => {
+ expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
+ });
+
+ it('should show calendar', () => {
+ expect(vm.$el.querySelector('.pika-single')).toBeDefined();
+ });
+
+ it('should toggle when dropdown is clicked', () => {
+ const hidePicker = jest.fn();
+ vm.$on('hidePicker', hidePicker);
+
+ vm.$el.querySelector('.dropdown-menu-toggle').click();
+
+ expect(hidePicker).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
new file mode 100644
index 00000000000..090f8b69213
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { projectData } from 'jest/ide/mock_data';
+import { TEST_HOST } from 'spec/test_constants';
+import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
+import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
+
+describe('ProjectAvatarDefault component', () => {
+ const Component = Vue.extend(ProjectAvatarDefault);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ project: projectData,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders identicon if project has no avatar_url', done => {
+ const expectedText = getFirstCharacterCapitalized(projectData.name);
+
+ vm.project = {
+ ...vm.project,
+ avatar_url: null,
+ };
+
+ vm.$nextTick()
+ .then(() => {
+ const identiconEl = vm.$el.querySelector('.identicon');
+
+ expect(identiconEl).not.toBe(null);
+ expect(identiconEl.textContent.trim()).toEqual(expectedText);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders avatar image if project has avatar_url', done => {
+ const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
+
+ vm.project = {
+ ...vm.project,
+ avatar_url: avatarUrl,
+ };
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.avatar')).not.toBeNull();
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
+ expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
new file mode 100644
index 00000000000..eb1d9e93634
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -0,0 +1,109 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+
+const localVue = createLocalVue();
+
+describe('ProjectListItem component', () => {
+ const Component = localVue.extend(ProjectListItem);
+ let wrapper;
+ let vm;
+ let options;
+
+ const project = getJSONFixture('static/projects.json')[0];
+
+ beforeEach(() => {
+ options = {
+ propsData: {
+ project,
+ selected: false,
+ },
+ localVue,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ });
+
+ it('does not render a check mark icon if selected === false', () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ });
+
+ it('renders a check mark icon if selected === true', () => {
+ options.propsData.selected = true;
+
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ });
+
+ it(`emits a "clicked" event when clicked`, () => {
+ wrapper = shallowMount(Component, options);
+ ({ vm } = wrapper);
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ wrapper.vm.onClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it(`renders the project avatar`, () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ });
+
+ it(`renders a simple namespace name with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a /');
+ });
+
+ it(`renders a properly truncated namespace with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a / ... / e /');
+ });
+
+ it(`renders the project name`, () => {
+ options.propsData.project.name = 'my-test-project';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').text());
+
+ expect(renderedName).toBe('my-test-project');
+ });
+
+ it(`renders the project name with highlighting in the case of a search query match`, () => {
+ options.propsData.project.name = 'my-test-project';
+ options.propsData.matcher = 'pro';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
+
+ expect(renderedName).toContain(expected);
+ });
+
+ it('prevents search query and project name XSS', () => {
+ const alertSpy = jest.spyOn(window, 'alert');
+ options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
+ options.propsData.matcher = "pro<script>alert('XSS');</script>";
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-xss-project';
+
+ expect(renderedName).toContain(expected);
+ expect(alertSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index d90fafb6bf7..9db86fa775f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -4,10 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index a4121448492..d02d924bd2b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -3,10 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const componentConfig = {
...mockConfig,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index d0299523137..edec3b138b3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockSuggestedColors } from './mock_data';
const createComponent = headerTitle => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 784bbaf8e6a..7e9e242a4f5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 887c04268d1..e09f0006359 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 06355c0dd65..c33cffb421d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -2,10 +2,7 @@ import { mount } from '@vue/test-utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { GlLabel } from '@gitlab/ui';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
new file mode 100644
index 00000000000..6564c012e67
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -0,0 +1,57 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#0033CC',
+ text_color: '#FFFFFF',
+ },
+];
+
+export const mockSuggestedColors = [
+ '#0033CC',
+ '#428BCA',
+ '#44AD8E',
+ '#A8D695',
+ '#5CB85C',
+ '#69D100',
+ '#004E00',
+ '#34495E',
+ '#7F8C8D',
+ '#A295D6',
+ '#5843AD',
+ '#8E44AD',
+ '#FFECDB',
+ '#AD4363',
+ '#D10069',
+ '#CC0033',
+ '#FF0000',
+ '#D9534F',
+ '#D1D100',
+ '#F0AD4E',
+ '#AD8D43',
+];
+
+export const mockConfig = {
+ showCreate: true,
+ isProject: true,
+ abilityName: 'issue',
+ context: {
+ labels: mockLabels,
+ },
+ namespace: 'gitlab-org',
+ updatePath: '/gitlab-org/my-project/issue/1',
+ labelsPath: '/gitlab-org/my-project/-/labels.json',
+ labelsWebUrl: '/gitlab-org/my-project/-/labels',
+ labelFilterBasePath: '/gitlab-org/my-project/issues',
+ canEdit: true,
+ suggestedColors: mockSuggestedColors,
+ emptyValueText: 'None',
+};
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
new file mode 100644
index 00000000000..bc86ee5a0c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
+
+const createComponent = config => {
+ const Component = Vue.extend(stackedProgressBarComponent);
+ const defaultConfig = {
+ successLabel: 'Synced',
+ failureLabel: 'Failed',
+ neutralLabel: 'Out of sync',
+ successCount: 25,
+ failureCount: 10,
+ totalCount: 5000,
+ ...config,
+ };
+
+ return mountComponent(Component, defaultConfig);
+};
+
+describe('StackedProgressBarComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('neutralCount', () => {
+ it('returns neutralCount based on totalCount, successCount and failureCount', () => {
+ expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getPercent', () => {
+ it('returns percentage from provided count based on `totalCount`', () => {
+ expect(vm.getPercent(500)).toBe(10);
+ });
+
+ it('returns percentage with decimal place from provided count based on `totalCount`', () => {
+ expect(vm.getPercent(67)).toBe(1.3);
+ });
+
+ it('returns percentage as `< 1` from provided count based on `totalCount` when evaluated value is less than 1', () => {
+ expect(vm.getPercent(10)).toBe('< 1');
+ });
+
+ it('returns 0 if totalCount is falsy', () => {
+ vm = createComponent({ totalCount: 0 });
+
+ expect(vm.getPercent(100)).toBe(0);
+ });
+ });
+
+ describe('barStyle', () => {
+ it('returns style string based on percentage provided', () => {
+ expect(vm.barStyle(50)).toBe('width: 50%;');
+ });
+ });
+
+ describe('getTooltip', () => {
+ describe('when hideTooltips is false', () => {
+ it('returns label string based on label and count provided', () => {
+ expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
+ });
+ });
+
+ describe('when hideTooltips is true', () => {
+ beforeEach(() => {
+ vm = createComponent({ hideTooltips: true });
+ });
+
+ it('returns an empty string', () => {
+ expect(vm.getTooltip('Synced', 10)).toBe('');
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders container element', () => {
+ expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
+ });
+
+ it('renders empty state when count is unavailable', () => {
+ const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
+
+ expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0);
+ vmX.$destroy();
+ });
+
+ it('renders bar elements when count is available', () => {
+ expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0);
+ expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0);
+ expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/tabs/tab_spec.js b/spec/frontend/vue_shared/components/tabs/tab_spec.js
new file mode 100644
index 00000000000..8cf07a9177c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tabs/tab_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tab component', () => {
+ const Component = Vue.extend(Tab);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('sets localActive to equal active', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.localActive).toBe(true);
+
+ done();
+ });
+ });
+
+ it('sets active class', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).toContain('active');
+
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
new file mode 100644
index 00000000000..49d92094b34
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tabs component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = new Vue({
+ components: {
+ Tabs,
+ Tab,
+ },
+ render(h) {
+ return h('div', [
+ h('tabs', [
+ h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'),
+ h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']),
+ ]),
+ ]);
+ },
+ }).$mount();
+
+ return vm.$nextTick();
+ });
+
+ describe('tab links', () => {
+ it('renders links for tabs', () => {
+ expect(vm.$el.querySelectorAll('a').length).toBe(2);
+ });
+
+ it('renders link titles from props', () => {
+ expect(vm.$el.querySelector('a').textContent).toContain('Testing');
+ });
+
+ it('renders link titles from slot', () => {
+ expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
+ });
+
+ it('renders active class', () => {
+ expect(vm.$el.querySelector('a').classList).toContain('active');
+ });
+
+ it('updates active class on click', () => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ return vm.$nextTick(() => {
+ expect(vm.$el.querySelector('a').classList).not.toContain('active');
+ expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
+ });
+ });
+ });
+
+ describe('content', () => {
+ it('renders content panes', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
+ expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
+ expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
new file mode 100644
index 00000000000..83bbb37a89a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/toggle_button_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+
+describe('Toggle Button', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(toggleButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('render output', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ name: 'foo',
+ });
+ });
+
+ it('renders input with provided name', () => {
+ expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ });
+
+ it('renders input with provided value', () => {
+ expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
+ });
+
+ it('renders input status icon', () => {
+ expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1);
+ });
+ });
+
+ describe('is-checked', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ });
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ it('renders is checked class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ });
+
+ it('sets aria-label representing toggle state', () => {
+ vm.value = true;
+
+ expect(vm.ariaLabel).toEqual('Toggle Status: ON');
+
+ vm.value = false;
+
+ expect(vm.ariaLabel).toEqual('Toggle Status: OFF');
+ });
+
+ it('emits change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ });
+ });
+
+ describe('is-disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ disabledInput: true,
+ });
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ });
+
+ it('does not emit change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('is-loading', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ isLoading: true,
+ });
+ });
+
+ it('renders loading class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js
new file mode 100644
index 00000000000..ee6c2e2cc46
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
+
+describe('User Avatar Svg Component', () => {
+ describe('Initialization', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarSvg, {
+ propsData: {
+ size: 99,
+ svg:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.707 15.707C1.077z"/></svg>',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should have <svg> as a child element', () => {
+ expect(wrapper.element.tagName).toEqual('svg');
+ expect(wrapper.html()).toContain('<path');
+ });
+ });
+});