summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js361
-rw-r--r--spec/frontend/boards/boards_store_spec.js17
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js145
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js124
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js63
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js434
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap48
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js52
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js30
9 files changed, 1209 insertions, 65 deletions
diff --git a/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
new file mode 100644
index 00000000000..3b64e4910e2
--- /dev/null
+++ b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
@@ -0,0 +1,361 @@
+import sqljs from 'sql.js';
+import axios from '~/lib/utils/axios_utils';
+import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
+import ClassSpecHelper from '../../helpers/class_spec_helper';
+
+jest.mock('sql.js');
+
+describe('BalsamiqViewer', () => {
+ const mockArrayBuffer = new ArrayBuffer(10);
+ let balsamiqViewer;
+ let viewer;
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ viewer = {};
+
+ balsamiqViewer = new BalsamiqViewer(viewer);
+ });
+
+ it('should set .viewer', () => {
+ expect(balsamiqViewer.viewer).toBe(viewer);
+ });
+ });
+
+ describe('loadFile', () => {
+ let bv;
+ const endpoint = 'endpoint';
+ const requestSuccess = Promise.resolve({
+ data: mockArrayBuffer,
+ status: 200,
+ });
+
+ beforeEach(() => {
+ viewer = {};
+ bv = new BalsamiqViewer(viewer);
+ });
+
+ it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
+ jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
+ jest.spyOn(bv, 'renderFile').mockReturnValue();
+
+ bv.loadFile(endpoint);
+
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ expect.objectContaining({
+ responseType: 'arraybuffer',
+ }),
+ );
+ });
+
+ it('should call `renderFile` on request success', done => {
+ jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
+ jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
+
+ bv.loadFile(endpoint)
+ .then(() => {
+ expect(bv.renderFile).toHaveBeenCalledWith(mockArrayBuffer);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should not call `renderFile` on request failure', done => {
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.reject());
+ jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
+
+ bv.loadFile(endpoint)
+ .then(() => {
+ done.fail('Expected loadFile to throw error!');
+ })
+ .catch(() => {
+ expect(bv.renderFile).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('renderFile', () => {
+ let container;
+ let previews;
+
+ beforeEach(() => {
+ viewer = {
+ appendChild: jest.fn(),
+ };
+ previews = [document.createElement('ul'), document.createElement('ul')];
+
+ balsamiqViewer = {
+ initDatabase: jest.fn(),
+ getPreviews: jest.fn(),
+ renderPreview: jest.fn(),
+ };
+ balsamiqViewer.viewer = viewer;
+
+ balsamiqViewer.getPreviews.mockReturnValue(previews);
+ balsamiqViewer.renderPreview.mockImplementation(preview => preview);
+ viewer.appendChild.mockImplementation(containerElement => {
+ container = containerElement;
+ });
+
+ BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, mockArrayBuffer);
+ });
+
+ it('should call .initDatabase', () => {
+ expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(mockArrayBuffer);
+ });
+
+ it('should call .getPreviews', () => {
+ expect(balsamiqViewer.getPreviews).toHaveBeenCalled();
+ });
+
+ it('should call .renderPreview for each preview', () => {
+ const allArgs = balsamiqViewer.renderPreview.mock.calls;
+
+ expect(allArgs.length).toBe(2);
+
+ previews.forEach((preview, i) => {
+ expect(allArgs[i][0]).toBe(preview);
+ });
+ });
+
+ it('should set the container HTML', () => {
+ expect(container.innerHTML).toBe('<ul></ul><ul></ul>');
+ });
+
+ it('should add inline preview classes', () => {
+ expect(container.classList[0]).toBe('list-inline');
+ expect(container.classList[1]).toBe('previews');
+ });
+
+ it('should call viewer.appendChild', () => {
+ expect(viewer.appendChild).toHaveBeenCalledWith(container);
+ });
+ });
+
+ describe('initDatabase', () => {
+ let uint8Array;
+ let data;
+
+ beforeEach(() => {
+ uint8Array = {};
+ data = 'data';
+ balsamiqViewer = {};
+ window.Uint8Array = jest.fn();
+ window.Uint8Array.mockReturnValue(uint8Array);
+
+ BalsamiqViewer.prototype.initDatabase.call(balsamiqViewer, data);
+ });
+
+ it('should instantiate Uint8Array', () => {
+ expect(window.Uint8Array).toHaveBeenCalledWith(data);
+ });
+
+ it('should call sqljs.Database', () => {
+ expect(sqljs.Database).toHaveBeenCalledWith(uint8Array);
+ });
+
+ it('should set .database', () => {
+ expect(balsamiqViewer.database).not.toBe(null);
+ });
+ });
+
+ describe('getPreviews', () => {
+ let database;
+ let thumbnails;
+ let getPreviews;
+
+ beforeEach(() => {
+ database = {
+ exec: jest.fn(),
+ };
+ thumbnails = [{ values: [0, 1, 2] }];
+
+ balsamiqViewer = {
+ database,
+ };
+
+ jest.spyOn(BalsamiqViewer, 'parsePreview').mockImplementation(preview => preview.toString());
+ database.exec.mockReturnValue(thumbnails);
+
+ getPreviews = BalsamiqViewer.prototype.getPreviews.call(balsamiqViewer);
+ });
+
+ it('should call database.exec', () => {
+ expect(database.exec).toHaveBeenCalledWith('SELECT * FROM thumbnails');
+ });
+
+ it('should call .parsePreview for each value', () => {
+ const allArgs = BalsamiqViewer.parsePreview.mock.calls;
+
+ expect(allArgs.length).toBe(3);
+
+ thumbnails[0].values.forEach((value, i) => {
+ expect(allArgs[i][0]).toBe(value);
+ });
+ });
+
+ it('should return an array of parsed values', () => {
+ expect(getPreviews).toEqual(['0', '1', '2']);
+ });
+ });
+
+ describe('getResource', () => {
+ let database;
+ let resourceID;
+ let resource;
+ let getResource;
+
+ beforeEach(() => {
+ database = {
+ exec: jest.fn(),
+ };
+ resourceID = 4;
+ resource = ['resource'];
+
+ balsamiqViewer = {
+ database,
+ };
+
+ database.exec.mockReturnValue(resource);
+
+ getResource = BalsamiqViewer.prototype.getResource.call(balsamiqViewer, resourceID);
+ });
+
+ it('should call database.exec', () => {
+ expect(database.exec).toHaveBeenCalledWith(
+ `SELECT * FROM resources WHERE id = '${resourceID}'`,
+ );
+ });
+
+ it('should return the selected resource', () => {
+ expect(getResource).toBe(resource[0]);
+ });
+ });
+
+ describe('renderPreview', () => {
+ let previewElement;
+ let innerHTML;
+ let preview;
+ let renderPreview;
+
+ beforeEach(() => {
+ innerHTML = '<a>innerHTML</a>';
+ previewElement = {
+ outerHTML: '<p>outerHTML</p>',
+ classList: {
+ add: jest.fn(),
+ },
+ };
+ preview = {};
+
+ balsamiqViewer = {
+ renderTemplate: jest.fn(),
+ };
+
+ jest.spyOn(document, 'createElement').mockReturnValue(previewElement);
+ balsamiqViewer.renderTemplate.mockReturnValue(innerHTML);
+
+ renderPreview = BalsamiqViewer.prototype.renderPreview.call(balsamiqViewer, preview);
+ });
+
+ it('should call classList.add', () => {
+ expect(previewElement.classList.add).toHaveBeenCalledWith('preview');
+ });
+
+ it('should call .renderTemplate', () => {
+ expect(balsamiqViewer.renderTemplate).toHaveBeenCalledWith(preview);
+ });
+
+ it('should set .innerHTML', () => {
+ expect(previewElement.innerHTML).toBe(innerHTML);
+ });
+
+ it('should return element', () => {
+ expect(renderPreview).toBe(previewElement);
+ });
+ });
+
+ describe('renderTemplate', () => {
+ let preview;
+ let name;
+ let resource;
+ let template;
+ let renderTemplate;
+
+ beforeEach(() => {
+ preview = { resourceID: 1, image: 'image' };
+ name = 'name';
+ resource = 'resource';
+ template = `
+ <div class="card">
+ <div class="card-header">name</div>
+ <div class="card-body">
+ <img class="img-thumbnail" src="data:image/png;base64,image"/>
+ </div>
+ </div>
+ `;
+
+ balsamiqViewer = {
+ getResource: jest.fn(),
+ };
+
+ jest.spyOn(BalsamiqViewer, 'parseTitle').mockReturnValue(name);
+ balsamiqViewer.getResource.mockReturnValue(resource);
+
+ renderTemplate = BalsamiqViewer.prototype.renderTemplate.call(balsamiqViewer, preview);
+ });
+
+ it('should call .getResource', () => {
+ expect(balsamiqViewer.getResource).toHaveBeenCalledWith(preview.resourceID);
+ });
+
+ it('should call .parseTitle', () => {
+ expect(BalsamiqViewer.parseTitle).toHaveBeenCalledWith(resource);
+ });
+
+ it('should return the template string', () => {
+ expect(renderTemplate.replace(/\s/g, '')).toEqual(template.replace(/\s/g, ''));
+ });
+ });
+
+ describe('parsePreview', () => {
+ let preview;
+ let parsePreview;
+
+ beforeEach(() => {
+ preview = ['{}', '{ "id": 1 }'];
+
+ jest.spyOn(JSON, 'parse');
+
+ parsePreview = BalsamiqViewer.parsePreview(preview);
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
+
+ it('should return the parsed JSON', () => {
+ expect(parsePreview).toEqual(JSON.parse('{ "id": 1 }'));
+ });
+ });
+
+ describe('parseTitle', () => {
+ let title;
+ let parseTitle;
+
+ beforeEach(() => {
+ title = { values: [['{}', '{}', '{"name":"name"}']] };
+
+ jest.spyOn(JSON, 'parse');
+
+ parseTitle = BalsamiqViewer.parseTitle(title);
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
+
+ it('should return the name value', () => {
+ expect(parseTitle).toBe('name');
+ });
+ });
+});
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 2dc9039bc9d..5c5315fd465 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -440,23 +440,6 @@ describe('boardsStore', () => {
});
});
- describe('allBoards', () => {
- const url = `${endpoints.boardsEndpoint}.json`;
-
- it('makes a request to fetch all boards', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.allBoards()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.allBoards()).rejects.toThrow();
- });
- });
-
describe('recentBoards', () => {
const url = `${endpoints.recentBoardsEndpoint}.json`;
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 7723af07d8c..b1ae86c2d3f 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import boardsStore from '~/boards/stores/boards_store';
@@ -8,7 +8,8 @@ import boardsStore from '~/boards/stores/boards_store';
const throttleDuration = 1;
function boardGenerator(n) {
- return new Array(n).fill().map((board, id) => {
+ return new Array(n).fill().map((board, index) => {
+ const id = `${index}`;
const name = `board${id}`;
return {
@@ -34,8 +35,17 @@ describe('BoardsSelector', () => {
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header');
+ const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
+ const $apollo = {
+ queries: {
+ boards: {
+ loading: false,
+ },
+ },
+ };
+
boardsStore.setEndpoints({
boardsEndpoint: '',
recentBoardsEndpoint: '',
@@ -45,7 +55,13 @@ describe('BoardsSelector', () => {
});
allBoardsResponse = Promise.resolve({
- data: boards,
+ data: {
+ group: {
+ boards: {
+ edges: boards.map(board => ({ node: board })),
+ },
+ },
+ },
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
@@ -54,8 +70,7 @@ describe('BoardsSelector', () => {
boardsStore.allBoards = jest.fn(() => allBoardsResponse);
boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
- const Component = Vue.extend(BoardsSelector);
- wrapper = mount(Component, {
+ wrapper = mount(BoardsSelector, {
propsData: {
throttleDuration,
currentBoard: {
@@ -77,13 +92,18 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
+ mocks: { $apollo },
attachToDocument: true,
});
+ wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
+ wrapper.setData({
+ [options.loadingKey]: true,
+ });
+ });
+
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
wrapper.find(GlDropdown).vm.$emit('show');
-
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => Vue.nextTick());
});
afterEach(() => {
@@ -91,64 +111,99 @@ describe('BoardsSelector', () => {
wrapper = null;
});
- describe('filtering', () => {
- it('shows all boards without filtering', () => {
- expect(getDropdownItems().length).toBe(boards.length + recentBoards.length);
+ describe('loading', () => {
+ // we are testing loading state, so don't resolve responses until after the tests
+ afterEach(() => {
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
- it('shows only matching boards when filtering', () => {
- const filterTerm = 'board1';
- const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
+ it('shows loading spinner', () => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(getLoadingIcon().exists()).toBe(true);
+ });
+ });
- fillSearchBox(filterTerm);
+ describe('loaded', () => {
+ beforeEach(() => {
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
+ });
- return Vue.nextTick().then(() => {
- expect(getDropdownItems().length).toBe(expectedCount);
- });
+ it('hides loading spinner', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
});
- it('shows message if there are no matching boards', () => {
- fillSearchBox('does not exist');
+ describe('filtering', () => {
+ beforeEach(() => {
+ wrapper.setData({
+ boards,
+ });
- return Vue.nextTick().then(() => {
- expect(getDropdownItems().length).toBe(0);
- expect(wrapper.text().includes('No matching boards found')).toBe(true);
+ return nextTick();
});
- });
- });
- describe('recent boards section', () => {
- it('shows only when boards are greater than 10', () => {
- const expectedCount = 2; // Recent + All
+ it('shows all boards without filtering', () => {
+ expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ });
- expect(getDropdownHeaders().length).toBe(expectedCount);
- });
+ it('shows only matching boards when filtering', () => {
+ const filterTerm = 'board1';
+ const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
- it('does not show when boards are less than 10', () => {
- wrapper.setData({
- boards: boards.slice(0, 5),
+ fillSearchBox(filterTerm);
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(expectedCount);
+ });
});
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ it('shows message if there are no matching boards', () => {
+ fillSearchBox('does not exist');
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(wrapper.text().includes('No matching boards found')).toBe(true);
+ });
});
});
- it('does not show when recentBoards api returns empty array', () => {
- wrapper.setData({
- recentBoards: [],
+ describe('recent boards section', () => {
+ it('shows only when boards are greater than 10', () => {
+ wrapper.setData({
+ boards,
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(2);
+ });
});
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ it('does not show when boards are less than 10', () => {
+ wrapper.setData({
+ boards: boards.slice(0, 5),
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
+ });
+
+ it('does not show when recentBoards api returns empty array', () => {
+ wrapper.setData({
+ recentBoards: [],
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
});
- });
- it('does not show when search is active', () => {
- fillSearchBox('Random string');
+ it('does not show when search is active', () => {
+ fillSearchBox('Random string');
- return Vue.nextTick().then(() => {
- expect(getDropdownHeaders().length).toBe(0);
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
});
});
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
new file mode 100644
index 00000000000..8ab5426a005
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -0,0 +1,124 @@
+import { mount, shallowMount } from '@vue/test-utils';
+
+import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
+import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
+
+describe('Project Feature Settings', () => {
+ const defaultProps = {
+ name: 'Test',
+ options: [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]],
+ value: 1,
+ disabledInput: false,
+ };
+ let wrapper;
+
+ const mountComponent = customProps => {
+ const propsData = { ...defaultProps, ...customProps };
+ return shallowMount(projectFeatureSetting, { propsData });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Hidden name input', () => {
+ it('should set the hidden name input if the name exists', () => {
+ expect(wrapper.find({ name: 'Test' }).props().value).toBe(1);
+ });
+
+ it('should not set the hidden name input if the name does not exist', () => {
+ wrapper.setProps({ name: null });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ name: 'Test' }).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Feature toggle', () => {
+ it('should enable the feature toggle if the value is not 0', () => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
+ });
+
+ it('should enable the feature toggle if the value is less than 0', () => {
+ wrapper.setProps({ value: -1 });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
+ });
+ });
+
+ it('should disable the feature toggle if the value is 0', () => {
+ wrapper.setProps({ value: 0 });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().value).toBe(false);
+ });
+ });
+
+ it('should disable the feature toggle if disabledInput is set', () => {
+ wrapper.setProps({ disabledInput: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(projectFeatureToggle).props().disabledInput).toBe(true);
+ });
+ });
+
+ it('should emit a change event when the feature toggle changes', () => {
+ // Needs to be fully mounted to be able to trigger the click event on the internal button
+ wrapper = mount(projectFeatureSetting, { propsData: defaultProps });
+
+ expect(wrapper.emitted().change).toBeUndefined();
+ wrapper
+ .find(projectFeatureToggle)
+ .find('button')
+ .trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted().change.length).toBe(1);
+ expect(wrapper.emitted().change[0]).toEqual([0]);
+ });
+ });
+ });
+
+ describe('Project repo select', () => {
+ it.each`
+ disabledInput | value | options | isDisabled
+ ${true} | ${0} | ${[[1, 1]]} | ${true}
+ ${true} | ${1} | ${[[1, 1], [2, 2], [3, 3]]} | ${true}
+ ${false} | ${0} | ${[[1, 1], [2, 2], [3, 3]]} | ${true}
+ ${false} | ${1} | ${[[1, 1]]} | ${true}
+ ${false} | ${1} | ${[[1, 1], [2, 2], [3, 3]]} | ${false}
+ `(
+ 'should set disabled to $isDisabled when disabledInput is $disabledInput, the value is $value and options are $options',
+ ({ disabledInput, value, options, isDisabled }) => {
+ wrapper.setProps({ disabledInput, value, options });
+
+ return wrapper.vm.$nextTick(() => {
+ if (isDisabled) {
+ expect(wrapper.find('select').attributes().disabled).toEqual('disabled');
+ } else {
+ expect(wrapper.find('select').attributes().disabled).toBeUndefined();
+ }
+ });
+ },
+ );
+
+ it('should emit the change when a new option is selected', () => {
+ expect(wrapper.emitted().change).toBeUndefined();
+ wrapper
+ .findAll('option')
+ .at(1)
+ .trigger('change');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted().change.length).toBe(1);
+ expect(wrapper.emitted().change[0]).toEqual([2]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
new file mode 100644
index 00000000000..7cbcbdcdd1f
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+
+import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue';
+
+describe('Project Setting Row', () => {
+ let wrapper;
+
+ const mountComponent = (customProps = {}) => {
+ const propsData = { ...customProps };
+ return shallowMount(projectSettingRow, { propsData });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should show the label if it is set', () => {
+ wrapper.setProps({ label: 'Test label' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('label').text()).toEqual('Test label');
+ });
+ });
+
+ it('should hide the label if it is not set', () => {
+ expect(wrapper.find('label').exists()).toBe(false);
+ });
+
+ it('should show the help icon with the correct help path if it is set', () => {
+ wrapper.setProps({ label: 'Test label', helpPath: '/123' });
+
+ return wrapper.vm.$nextTick(() => {
+ const link = wrapper.find('a');
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toEqual('/123');
+ });
+ });
+
+ it('should hide the help icon if no help path is set', () => {
+ wrapper.setProps({ label: 'Test label' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('a').exists()).toBe(false);
+ });
+ });
+
+ it('should show the help text if it is set', () => {
+ wrapper.setProps({ helpText: 'Test text' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('span').text()).toEqual('Test text');
+ });
+ });
+
+ it('should hide the help text if it is set', () => {
+ expect(wrapper.find('span').exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
new file mode 100644
index 00000000000..c304dfd2048
--- /dev/null
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -0,0 +1,434 @@
+import { shallowMount } from '@vue/test-utils';
+
+import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
+import {
+ featureAccessLevel,
+ visibilityLevelDescriptions,
+ visibilityOptions,
+} from '~/pages/projects/shared/permissions/constants';
+
+const defaultProps = {
+ currentSettings: {
+ visibilityLevel: 10,
+ requestAccessEnabled: true,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ forkingAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ pagesAccessLevel: 10,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ emailsDisabled: false,
+ packagesEnabled: true,
+ },
+ canDisableEmails: true,
+ canChangeVisibilityLevel: true,
+ allowedVisibilityOptions: [0, 10, 20],
+ visibilityHelpPath: '/help/public_access/public_access',
+ registryAvailable: false,
+ registryHelpPath: '/help/user/packages/container_registry/index',
+ lfsAvailable: true,
+ lfsHelpPath: '/help/workflow/lfs/manage_large_binaries_with_git_lfs',
+ pagesAvailable: true,
+ pagesAccessControlEnabled: false,
+ pagesAccessControlForced: false,
+ pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control-core',
+ packagesAvailable: false,
+ packagesHelpPath: '/help/user/packages/index',
+};
+
+describe('Settings Panel', () => {
+ let wrapper;
+
+ const mountComponent = customProps => {
+ const propsData = { ...defaultProps, ...customProps };
+ return shallowMount(settingsPanel, { propsData });
+ };
+
+ const overrideCurrentSettings = (currentSettingsProps, extraProps = {}) => {
+ return mountComponent({
+ ...extraProps,
+ currentSettings: {
+ ...defaultProps.currentSettings,
+ ...currentSettingsProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Project Visibility', () => {
+ it('should set the project visibility help path', () => {
+ expect(wrapper.find({ ref: 'project-visibility-settings' }).props().helpPath).toBe(
+ defaultProps.visibilityHelpPath,
+ );
+ });
+
+ it('should not disable the visibility level dropdown', () => {
+ wrapper.setProps({ canChangeVisibilityLevel: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper.find('[name="project[visibility_level]"]').attributes().disabled,
+ ).toBeUndefined();
+ });
+ });
+
+ it('should disable the visibility level dropdown', () => {
+ wrapper.setProps({ canChangeVisibilityLevel: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('[name="project[visibility_level]"]').attributes().disabled).toBe(
+ 'disabled',
+ );
+ });
+ });
+
+ it.each`
+ option | allowedOptions | disabled
+ ${visibilityOptions.PRIVATE} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.PRIVATE} | ${[visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${true}
+ ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.PUBLIC]} | ${true}
+ ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
+ ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL]} | ${true}
+ `(
+ 'sets disabled to $disabled for the visibility option $option when given $allowedOptions',
+ ({ option, allowedOptions, disabled }) => {
+ wrapper.setProps({ allowedVisibilityOptions: allowedOptions });
+
+ return wrapper.vm.$nextTick(() => {
+ const attributeValue = wrapper
+ .find(`[name="project[visibility_level]"] option[value="${option}"]`)
+ .attributes().disabled;
+
+ if (disabled) {
+ expect(attributeValue).toBe('disabled');
+ } else {
+ expect(attributeValue).toBeUndefined();
+ }
+ });
+ },
+ );
+
+ it('should set the visibility level description based upon the selected visibility level', () => {
+ wrapper.find('[name="project[visibility_level]"]').setValue(visibilityOptions.INTERNAL);
+
+ expect(wrapper.find({ ref: 'project-visibility-settings' }).text()).toContain(
+ visibilityLevelDescriptions[visibilityOptions.INTERNAL],
+ );
+ });
+
+ it('should show the request access checkbox if the visibility level is not private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.INTERNAL });
+
+ expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(true);
+ });
+
+ it('should not show the request access checkbox if the visibility level is private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(false);
+ });
+ });
+
+ describe('Repository', () => {
+ it('should set the repository help text when the visibility level is set to private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ 'View and edit files in this project',
+ );
+ });
+
+ it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC });
+
+ expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ 'View and edit files in this project. Non-project members will only have read access',
+ );
+ });
+ });
+
+ describe('Merge requests', () => {
+ it('should enable the merge requests access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper
+ .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
+ .props().disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the merge requests access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper
+ .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
+ .props().disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Forks', () => {
+ it('should enable the forking access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the forking access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Pipelines', () => {
+ it('should enable the builds access level input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the builds access level input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+
+ expect(
+ wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
+ .disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Container registry', () => {
+ it('should show the container registry settings if the registry is available', () => {
+ wrapper.setProps({ registryAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(true);
+ });
+ });
+
+ it('should hide the container registry settings if the registry is not available', () => {
+ wrapper.setProps({ registryAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(false);
+ });
+ });
+
+ it('should set the container registry settings help path', () => {
+ wrapper.setProps({ registryAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'container-registry-settings' }).props().helpPath).toBe(
+ defaultProps.registryHelpPath,
+ );
+ });
+ });
+
+ it('should show the container registry public note if the visibility level is public and the registry is available', () => {
+ wrapper = overrideCurrentSettings(
+ { visibilityLevel: visibilityOptions.PUBLIC },
+ { registryAvailable: true },
+ );
+
+ expect(wrapper.find({ ref: 'container-registry-settings' }).text()).toContain(
+ 'Note: the container registry is always visible when a project is public',
+ );
+ });
+
+ it('should hide the container registry public note if the visibility level is private and the registry is available', () => {
+ wrapper = overrideCurrentSettings(
+ { visibilityLevel: visibilityOptions.PRIVATE },
+ { registryAvailable: true },
+ );
+
+ expect(wrapper.find({ ref: 'container-registry-settings' }).text()).not.toContain(
+ 'Note: the container registry is always visible when a project is public',
+ );
+ });
+
+ it('should enable the container registry input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { registryAvailable: true },
+ );
+
+ expect(
+ wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
+ ).toEqual(false);
+ });
+
+ it('should disable the container registry input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { registryAvailable: true },
+ );
+
+ expect(
+ wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
+ ).toEqual(true);
+ });
+ });
+
+ describe('Git Large File Storage', () => {
+ it('should show the LFS settings if LFS is available', () => {
+ wrapper.setProps({ lfsAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(true);
+ });
+ });
+
+ it('should hide the LFS settings if LFS is not available', () => {
+ wrapper.setProps({ lfsAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(false);
+ });
+ });
+
+ it('should set the LFS settings help path', () => {
+ expect(wrapper.find({ ref: 'git-lfs-settings' }).props().helpPath).toBe(
+ defaultProps.lfsHelpPath,
+ );
+ });
+
+ it('should enable the LFS input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { lfsAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(false);
+ });
+
+ it('should disable the LFS input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { lfsAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(true);
+ });
+ });
+
+ describe('Packages', () => {
+ it('should show the packages settings if packages are available', () => {
+ wrapper.setProps({ packagesAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(true);
+ });
+ });
+
+ it('should hide the packages settings if packages are not available', () => {
+ wrapper.setProps({ packagesAvailable: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(false);
+ });
+ });
+
+ it('should set the package settings help path', () => {
+ wrapper.setProps({ packagesAvailable: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'package-settings' }).props().helpPath).toBe(
+ defaultProps.packagesHelpPath,
+ );
+ });
+ });
+
+ it('should enable the packages input when the repository is enabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ { packagesAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
+ false,
+ );
+ });
+
+ it('should disable the packages input when the repository is disabled', () => {
+ wrapper = overrideCurrentSettings(
+ { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ { packagesAvailable: true },
+ );
+
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
+ true,
+ );
+ });
+ });
+
+ describe('Pages', () => {
+ it.each`
+ pagesAvailable | pagesAccessControlEnabled | visibility
+ ${true} | ${true} | ${'show'}
+ ${true} | ${false} | ${'hide'}
+ ${false} | ${true} | ${'hide'}
+ ${false} | ${false} | ${'hide'}
+ `(
+ 'should $visibility the page settings if pagesAvailable is $pagesAvailable and pagesAccessControlEnabled is $pagesAccessControlEnabled',
+ ({ pagesAvailable, pagesAccessControlEnabled, visibility }) => {
+ wrapper.setProps({ pagesAvailable, pagesAccessControlEnabled });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'pages-settings' }).exists()).toBe(visibility === 'show');
+ });
+ },
+ );
+
+ it('should set the pages settings help path', () => {
+ wrapper.setProps({ pagesAvailable: true, pagesAccessControlEnabled: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'pages-settings' }).props().helpPath).toBe(
+ defaultProps.pagesHelpPath,
+ );
+ });
+ });
+ });
+
+ describe('Email notifications', () => {
+ it('should show the disable email notifications input if emails an be disabled', () => {
+ wrapper.setProps({ canDisableEmails: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(true);
+ });
+ });
+
+ it('should hide the disable email notifications input if emails cannot be disabled', () => {
+ wrapper.setProps({ canDisableEmails: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
new file mode 100644
index 00000000000..3c3f9764f64
--- /dev/null
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Snippet Description Edit component rendering matches the snapshot 1`] = `
+<div
+ class="form-group js-description-input"
+>
+ <label>
+ Description (optional)
+ </label>
+
+ <div
+ class="js-collapsible-input"
+ >
+ <div
+ class="js-collapsed d-none"
+ >
+ <gl-form-input-stub
+ class="form-control"
+ data-qa-selector="description_placeholder"
+ placeholder="Optionally add a description about what your snippet does or how to use it…"
+ />
+ </div>
+
+ <markdown-field-stub
+ addspacingclasses="true"
+ canattachfile="true"
+ class="js-expanded"
+ enableautocomplete="true"
+ helppagepath=""
+ markdowndocspath="help/"
+ markdownpreviewpath="foo/"
+ note="[object Object]"
+ quickactionsdocspath=""
+ textareavalue=""
+ >
+ <textarea
+ aria-label="Description"
+ class="note-textarea js-gfm-input js-autosize markdown-area
+ qa-description-textarea"
+ data-supports-quick-actions="false"
+ dir="auto"
+ id="snippet-description"
+ placeholder="Write a comment or drag your files here…"
+ />
+ </markdown-field-stub>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
new file mode 100644
index 00000000000..167489dc004
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -0,0 +1,52 @@
+import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('Snippet Description Edit component', () => {
+ let wrapper;
+ const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
+ const markdownPreviewPath = 'foo/';
+ const markdownDocsPath = 'help/';
+
+ function createComponent(description = defaultDescription) {
+ wrapper = shallowMount(SnippetDescriptionEdit, {
+ propsData: {
+ description,
+ markdownPreviewPath,
+ markdownDocsPath,
+ },
+ });
+ }
+
+ function isHidden(sel) {
+ return wrapper.find(sel).classes('d-none');
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders the field expanded when description exists', () => {
+ expect(wrapper.find('.js-collapsed').classes('d-none')).toBe(true);
+ expect(wrapper.find('.js-expanded').classes('d-none')).toBe(false);
+
+ expect(isHidden('.js-collapsed')).toBe(true);
+ expect(isHidden('.js-expanded')).toBe(false);
+ });
+
+ it('renders the field collapsed if there is no description yet', () => {
+ createComponent('');
+
+ expect(isHidden('.js-collapsed')).toBe(false);
+ expect(isHidden('.js-expanded')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 5edf41b1ec6..ef95cb1b8f2 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -259,16 +259,40 @@ describe('mrWidgetOptions', () => {
describe('methods', () => {
describe('checkStatus', () => {
- it('should tell service to check status', () => {
+ let cb;
+ let isCbExecuted;
+
+ beforeEach(() => {
jest.spyOn(vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
jest.spyOn(vm.mr, 'setData').mockImplementation(() => {});
jest.spyOn(vm, 'handleNotification').mockImplementation(() => {});
- let isCbExecuted = false;
- const cb = () => {
+ isCbExecuted = false;
+ cb = () => {
isCbExecuted = true;
};
+ });
+
+ it('should not tell service to check status if document is not visible', () => {
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ configurable: true,
+ });
+ vm.checkStatus(cb);
+
+ return vm.$nextTick().then(() => {
+ expect(vm.service.checkStatus).not.toHaveBeenCalled();
+ expect(vm.mr.setData).not.toHaveBeenCalled();
+ expect(vm.handleNotification).not.toHaveBeenCalled();
+ expect(isCbExecuted).toBeFalsy();
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ configurable: true,
+ });
+ });
+ });
+ it('should tell service to check status if document is visible', () => {
vm.checkStatus(cb);
return vm.$nextTick().then(() => {