diff options
Diffstat (limited to 'spec/frontend')
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(() => { |