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
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-10 12:08:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-10 12:08:10 +0300
commit82fa8a3d1e8466ef36b58604d20fcc145ea12118 (patch)
treec5c0286537405c2fa7719ecce3ed0d73d947c555 /spec
parent232655bf32cd474d54de357b65ef43d77271117c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_token.json6
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json10
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json1
-rw-r--r--spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js (renamed from spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js)90
-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
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb53
14 files changed, 967 insertions, 108 deletions
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
index c8a8b8d1e7d..7cb9f136b0d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
@@ -25,7 +25,9 @@
"items": {
"type": "string"
}
+ },
+ "token": {
+ "type": "string"
}
- },
- "additionalProperties": false
+ }
} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json
new file mode 100644
index 00000000000..ed8fa58393f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json
@@ -0,0 +1,10 @@
+{
+ "type": "object",
+ "required": [],
+ "properties": {
+ "name": { "type": "string" },
+ "precision": { "type": "number" },
+ "format": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
index a16f1ef592f..9f39e9c77cb 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -9,6 +9,7 @@
"title": { "type": "string" },
"type": { "type": "string" },
"y_label": { "type": "string" },
+ "y_axis": { "$ref": "axis.json" },
"weight": { "type": "number" },
"metrics": {
"type": "array",
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
index d175c8ba853..3b64e4910e2 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
@@ -3,6 +3,8 @@ 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;
@@ -34,22 +36,22 @@ describe('BalsamiqViewer', () => {
});
it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
- spyOn(axios, 'get').and.returnValue(requestSuccess);
- spyOn(bv, 'renderFile').and.stub();
+ jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
+ jest.spyOn(bv, 'renderFile').mockReturnValue();
bv.loadFile(endpoint);
expect(axios.get).toHaveBeenCalledWith(
endpoint,
- jasmine.objectContaining({
+ expect.objectContaining({
responseType: 'arraybuffer',
}),
);
});
it('should call `renderFile` on request success', done => {
- spyOn(axios, 'get').and.returnValue(requestSuccess);
- spyOn(bv, 'renderFile').and.callFake(() => {});
+ jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
+ jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
bv.loadFile(endpoint)
.then(() => {
@@ -60,8 +62,8 @@ describe('BalsamiqViewer', () => {
});
it('should not call `renderFile` on request failure', done => {
- spyOn(axios, 'get').and.returnValue(Promise.reject());
- spyOn(bv, 'renderFile');
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.reject());
+ jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
bv.loadFile(endpoint)
.then(() => {
@@ -80,19 +82,21 @@ describe('BalsamiqViewer', () => {
let previews;
beforeEach(() => {
- viewer = jasmine.createSpyObj('viewer', ['appendChild']);
+ viewer = {
+ appendChild: jest.fn(),
+ };
previews = [document.createElement('ul'), document.createElement('ul')];
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', [
- 'initDatabase',
- 'getPreviews',
- 'renderPreview',
- ]);
+ balsamiqViewer = {
+ initDatabase: jest.fn(),
+ getPreviews: jest.fn(),
+ renderPreview: jest.fn(),
+ };
balsamiqViewer.viewer = viewer;
- balsamiqViewer.getPreviews.and.returnValue(previews);
- balsamiqViewer.renderPreview.and.callFake(preview => preview);
- viewer.appendChild.and.callFake(containerElement => {
+ balsamiqViewer.getPreviews.mockReturnValue(previews);
+ balsamiqViewer.renderPreview.mockImplementation(preview => preview);
+ viewer.appendChild.mockImplementation(containerElement => {
container = containerElement;
});
@@ -108,7 +112,7 @@ describe('BalsamiqViewer', () => {
});
it('should call .renderPreview for each preview', () => {
- const allArgs = balsamiqViewer.renderPreview.calls.allArgs();
+ const allArgs = balsamiqViewer.renderPreview.mock.calls;
expect(allArgs.length).toBe(2);
@@ -132,19 +136,15 @@ describe('BalsamiqViewer', () => {
});
describe('initDatabase', () => {
- let database;
let uint8Array;
let data;
beforeEach(() => {
uint8Array = {};
- database = {};
data = 'data';
-
balsamiqViewer = {};
-
- spyOn(window, 'Uint8Array').and.returnValue(uint8Array);
- spyOn(sqljs, 'Database').and.returnValue(database);
+ window.Uint8Array = jest.fn();
+ window.Uint8Array.mockReturnValue(uint8Array);
BalsamiqViewer.prototype.initDatabase.call(balsamiqViewer, data);
});
@@ -158,7 +158,7 @@ describe('BalsamiqViewer', () => {
});
it('should set .database', () => {
- expect(balsamiqViewer.database).toBe(database);
+ expect(balsamiqViewer.database).not.toBe(null);
});
});
@@ -168,15 +168,17 @@ describe('BalsamiqViewer', () => {
let getPreviews;
beforeEach(() => {
- database = jasmine.createSpyObj('database', ['exec']);
+ database = {
+ exec: jest.fn(),
+ };
thumbnails = [{ values: [0, 1, 2] }];
balsamiqViewer = {
database,
};
- spyOn(BalsamiqViewer, 'parsePreview').and.callFake(preview => preview.toString());
- database.exec.and.returnValue(thumbnails);
+ jest.spyOn(BalsamiqViewer, 'parsePreview').mockImplementation(preview => preview.toString());
+ database.exec.mockReturnValue(thumbnails);
getPreviews = BalsamiqViewer.prototype.getPreviews.call(balsamiqViewer);
});
@@ -186,7 +188,7 @@ describe('BalsamiqViewer', () => {
});
it('should call .parsePreview for each value', () => {
- const allArgs = BalsamiqViewer.parsePreview.calls.allArgs();
+ const allArgs = BalsamiqViewer.parsePreview.mock.calls;
expect(allArgs.length).toBe(3);
@@ -207,7 +209,9 @@ describe('BalsamiqViewer', () => {
let getResource;
beforeEach(() => {
- database = jasmine.createSpyObj('database', ['exec']);
+ database = {
+ exec: jest.fn(),
+ };
resourceID = 4;
resource = ['resource'];
@@ -215,7 +219,7 @@ describe('BalsamiqViewer', () => {
database,
};
- database.exec.and.returnValue(resource);
+ database.exec.mockReturnValue(resource);
getResource = BalsamiqViewer.prototype.getResource.call(balsamiqViewer, resourceID);
});
@@ -241,14 +245,18 @@ describe('BalsamiqViewer', () => {
innerHTML = '<a>innerHTML</a>';
previewElement = {
outerHTML: '<p>outerHTML</p>',
- classList: jasmine.createSpyObj('classList', ['add']),
+ classList: {
+ add: jest.fn(),
+ },
};
preview = {};
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderTemplate']);
+ balsamiqViewer = {
+ renderTemplate: jest.fn(),
+ };
- spyOn(document, 'createElement').and.returnValue(previewElement);
- balsamiqViewer.renderTemplate.and.returnValue(innerHTML);
+ jest.spyOn(document, 'createElement').mockReturnValue(previewElement);
+ balsamiqViewer.renderTemplate.mockReturnValue(innerHTML);
renderPreview = BalsamiqViewer.prototype.renderPreview.call(balsamiqViewer, preview);
});
@@ -290,10 +298,12 @@ describe('BalsamiqViewer', () => {
</div>
`;
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['getResource']);
+ balsamiqViewer = {
+ getResource: jest.fn(),
+ };
- spyOn(BalsamiqViewer, 'parseTitle').and.returnValue(name);
- balsamiqViewer.getResource.and.returnValue(resource);
+ jest.spyOn(BalsamiqViewer, 'parseTitle').mockReturnValue(name);
+ balsamiqViewer.getResource.mockReturnValue(resource);
renderTemplate = BalsamiqViewer.prototype.renderTemplate.call(balsamiqViewer, preview);
});
@@ -306,7 +316,7 @@ describe('BalsamiqViewer', () => {
expect(BalsamiqViewer.parseTitle).toHaveBeenCalledWith(resource);
});
- it('should return the template string', function() {
+ it('should return the template string', () => {
expect(renderTemplate.replace(/\s/g, '')).toEqual(template.replace(/\s/g, ''));
});
});
@@ -318,7 +328,7 @@ describe('BalsamiqViewer', () => {
beforeEach(() => {
preview = ['{}', '{ "id": 1 }'];
- spyOn(JSON, 'parse').and.callThrough();
+ jest.spyOn(JSON, 'parse');
parsePreview = BalsamiqViewer.parsePreview(preview);
});
@@ -337,7 +347,7 @@ describe('BalsamiqViewer', () => {
beforeEach(() => {
title = { values: [['{}', '{}', '{"name":"name"}']] };
- spyOn(JSON, 'parse').and.callThrough();
+ jest.spyOn(JSON, 'parse');
parseTitle = BalsamiqViewer.parseTitle(title);
});
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(() => {
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 635349955b1..5f22208a3ac 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -52,7 +52,7 @@ describe ProjectPolicy do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics read_deploy_token
+ daily_statistics read_deploy_token create_deploy_token
]
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 14153fae42f..8076b0958a4 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -133,4 +133,57 @@ describe API::DeployTokens do
end
end
end
+
+ describe 'POST /projects/:id/deploy_tokens' do
+ let(:params) do
+ {
+ name: 'Foo',
+ expires_at: 1.year.from_now,
+ scopes: [
+ 'read_repository'
+ ],
+ username: 'Bar'
+ }
+ end
+
+ subject do
+ post api("/projects/#{project.id}/deploy_tokens", user), params: params
+ response
+ end
+
+ context 'when unauthenticated' do
+ let(:user) { nil }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when authenticated as non-admin user' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+
+ context 'when authenticated as maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates the deploy token' do
+ expect { subject }.to change { DeployToken.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ end
+
+ context 'with an invalid scope' do
+ before do
+ params[:scopes] = %w[read_repository all_access]
+ end
+
+ it { is_expected.to have_gitlab_http_status(:bad_request) }
+ end
+ end
+ end
end