diff options
Diffstat (limited to 'spec/frontend')
107 files changed, 4645 insertions, 236 deletions
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 7004373be0e..62ba0d36982 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -151,6 +151,28 @@ describe('Api', () => { }); }); + describe('projectUsers', () => { + it('fetches all users of a particular project', done => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const projectPath = 'gitlab-org%2Fgitlab-ce'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/users`; + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); + + Api.projectUsers('gitlab-org/gitlab-ce', query, options) + .then(response => { + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('projectMergeRequests', () => { const projectPath = 'abc'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`; diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js index 4d9c8f96d62..33d402388c9 100644 --- a/spec/frontend/autosave_spec.js +++ b/spec/frontend/autosave_spec.js @@ -63,12 +63,15 @@ describe('Autosave', () => { expect(field.trigger).toHaveBeenCalled(); }); - it('triggers native event', done => { - autosave.field.get(0).addEventListener('change', () => { - done(); - }); + it('triggers native event', () => { + const fieldElement = autosave.field.get(0); + const eventHandler = jest.fn(); + fieldElement.addEventListener('change', eventHandler); Autosave.prototype.restore.call(autosave); + + expect(eventHandler).toHaveBeenCalledTimes(1); + fieldElement.removeEventListener('change', eventHandler); }); }); diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js new file mode 100644 index 00000000000..6db0eabc16b --- /dev/null +++ b/spec/frontend/behaviors/markdown/render_metrics_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import renderMetrics from '~/behaviors/markdown/render_metrics'; +import { TEST_HOST } from 'helpers/test_constants'; + +const originalExtend = Vue.extend; + +describe('Render metrics for Gitlab Flavoured Markdown', () => { + const container = { + Metrics() {}, + }; + + let spyExtend; + + beforeEach(() => { + Vue.extend = () => container.Metrics; + spyExtend = jest.spyOn(Vue, 'extend'); + }); + + afterEach(() => { + Vue.extend = originalExtend; + }); + + it('does nothing when no elements are found', () => { + renderMetrics([]); + + expect(spyExtend).not.toHaveBeenCalled(); + }); + + it('renders a vue component when elements are found', () => { + const element = document.createElement('div'); + element.setAttribute('data-dashboard-url', TEST_HOST); + + renderMetrics([element]); + + expect(spyExtend).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js index a8a322e7237..e106c2bf1f1 100644 --- a/spec/frontend/boards/services/board_service_spec.js +++ b/spec/frontend/boards/services/board_service_spec.js @@ -389,4 +389,163 @@ describe('BoardService', () => { return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow(); }); }); + + 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(service.allBoards()).resolves.toEqual(expectedResponse); + }); + + it('fails for error response', () => { + axiosMock.onGet(url).replyOnce(500); + + return expect(service.allBoards()).rejects.toThrow(); + }); + }); + + describe('recentBoards', () => { + const url = `${endpoints.recentBoardsEndpoint}.json`; + + it('makes a request to fetch all boards', () => { + axiosMock.onGet(url).replyOnce(200, dummyResponse); + const expectedResponse = expect.objectContaining({ data: dummyResponse }); + + return expect(service.recentBoards()).resolves.toEqual(expectedResponse); + }); + + it('fails for error response', () => { + axiosMock.onGet(url).replyOnce(500); + + return expect(service.recentBoards()).rejects.toThrow(); + }); + }); + + describe('createBoard', () => { + const labelIds = ['first label', 'second label']; + const assigneeId = 'as sign ee'; + const milestoneId = 'vegetable soup'; + const board = { + labels: labelIds.map(id => ({ id })), + assignee: { id: assigneeId }, + milestone: { id: milestoneId }, + }; + + describe('for existing board', () => { + const id = 'skate-board'; + const url = `${endpoints.boardsEndpoint}/${id}.json`; + const expectedRequest = expect.objectContaining({ + data: JSON.stringify({ + board: { + ...board, + id, + label_ids: labelIds, + assignee_id: assigneeId, + milestone_id: milestoneId, + }, + }), + }); + + let requestSpy; + + beforeEach(() => { + requestSpy = jest.fn(); + axiosMock.onPut(url).replyOnce(config => requestSpy(config)); + }); + + it('makes a request to update the board', () => { + requestSpy.mockReturnValue([200, dummyResponse]); + const expectedResponse = expect.objectContaining({ data: dummyResponse }); + + return expect( + service.createBoard({ + ...board, + id, + }), + ) + .resolves.toEqual(expectedResponse) + .then(() => { + expect(requestSpy).toHaveBeenCalledWith(expectedRequest); + }); + }); + + it('fails for error response', () => { + requestSpy.mockReturnValue([500]); + + return expect( + service.createBoard({ + ...board, + id, + }), + ) + .rejects.toThrow() + .then(() => { + expect(requestSpy).toHaveBeenCalledWith(expectedRequest); + }); + }); + }); + + describe('for new board', () => { + const url = `${endpoints.boardsEndpoint}.json`; + const expectedRequest = expect.objectContaining({ + data: JSON.stringify({ + board: { + ...board, + label_ids: labelIds, + assignee_id: assigneeId, + milestone_id: milestoneId, + }, + }), + }); + + let requestSpy; + + beforeEach(() => { + requestSpy = jest.fn(); + axiosMock.onPost(url).replyOnce(config => requestSpy(config)); + }); + + it('makes a request to create a new board', () => { + requestSpy.mockReturnValue([200, dummyResponse]); + const expectedResponse = expect.objectContaining({ data: dummyResponse }); + + return expect(service.createBoard(board)) + .resolves.toEqual(expectedResponse) + .then(() => { + expect(requestSpy).toHaveBeenCalledWith(expectedRequest); + }); + }); + + it('fails for error response', () => { + requestSpy.mockReturnValue([500]); + + return expect(service.createBoard(board)) + .rejects.toThrow() + .then(() => { + expect(requestSpy).toHaveBeenCalledWith(expectedRequest); + }); + }); + }); + }); + + describe('deleteBoard', () => { + const id = 'capsized'; + const url = `${endpoints.boardsEndpoint}/${id}.json`; + + it('makes a request to delete a boards', () => { + axiosMock.onDelete(url).replyOnce(200, dummyResponse); + const expectedResponse = expect.objectContaining({ data: dummyResponse }); + + return expect(service.deleteBoard({ id })).resolves.toEqual(expectedResponse); + }); + + it('fails for error response', () => { + axiosMock.onDelete(url).replyOnce(500); + + return expect(service.deleteBoard({ id })).rejects.toThrow(); + }); + }); }); diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 6de06a9e2d5..80816faa5fc 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -147,47 +147,80 @@ describe('Clusters', () => { }); describe('updateContainer', () => { + const { location } = window; + + beforeEach(() => { + delete window.location; + window.location = { + reload: jest.fn(), + hash: location.hash, + }; + }); + + afterEach(() => { + window.location = location; + }); + describe('when creating cluster', () => { it('should show the creating container', () => { cluster.updateContainer(null, 'creating'); expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(window.location.reload).not.toHaveBeenCalled(); }); it('should continue to show `creating` banner with subsequent updates of the same status', () => { + cluster.updateContainer(null, 'creating'); cluster.updateContainer('creating', 'creating'); expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(window.location.reload).not.toHaveBeenCalled(); }); }); describe('when cluster is created', () => { - it('should show the success container and fresh the page', () => { - cluster.updateContainer(null, 'created'); + it('should hide the "creating" banner and refresh the page', () => { + jest.spyOn(cluster, 'setClusterNewlyCreated'); + cluster.updateContainer(null, 'creating'); + cluster.updateContainer('creating', 'created'); expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(window.location.reload).toHaveBeenCalled(); + expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(true); + }); - expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy(); + it('when the page is refreshed, it should show the "success" banner', () => { + jest.spyOn(cluster, 'setClusterNewlyCreated'); + jest.spyOn(cluster, 'isClusterNewlyCreated').mockReturnValue(true); + + cluster.updateContainer(null, 'created'); + cluster.updateContainer('created', 'created'); + expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy(); expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(window.location.reload).not.toHaveBeenCalled(); + expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(false); }); it('should not show a banner when status is already `created`', () => { + jest.spyOn(cluster, 'setClusterNewlyCreated'); + jest.spyOn(cluster, 'isClusterNewlyCreated').mockReturnValue(false); + + cluster.updateContainer(null, 'created'); cluster.updateContainer('created', 'created'); expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(window.location.reload).not.toHaveBeenCalled(); + expect(cluster.setClusterNewlyCreated).not.toHaveBeenCalled(); }); }); diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap index fd307ce5ab3..47bdc677068 100644 --- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap +++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap @@ -2,7 +2,7 @@ exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = ` <div - class="form-group" + class="confidential-merge-request-fork-group form-group" > <label> Project @@ -28,6 +28,23 @@ exports[`Confidential merge request project form group component renders empty s </a> and set the forks visiblity to private. </span> + + <gllink-stub + class="w-auto p-0 d-inline-block text-primary bg-transparent" + href="/help" + target="_blank" + > + <span + class="sr-only" + > + Read more + </span> + + <i + aria-hidden="true" + class="fa fa-question-circle" + /> + </gllink-stub> </p> </div> </div> @@ -35,7 +52,7 @@ exports[`Confidential merge request project form group component renders empty s exports[`Confidential merge request project form group component renders fork dropdown 1`] = ` <div - class="form-group" + class="confidential-merge-request-fork-group form-group" > <label> Project @@ -61,6 +78,23 @@ exports[`Confidential merge request project form group component renders fork dr </a> and set the forks visiblity to private. </span> + + <gllink-stub + class="w-auto p-0 d-inline-block text-primary bg-transparent" + href="/help" + target="_blank" + > + <span + class="sr-only" + > + Read more + </span> + + <i + aria-hidden="true" + class="fa fa-question-circle" + /> + </gllink-stub> </p> </div> </div> diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js index 6e41fdabdce..dcc6fa96d18 100644 --- a/spec/frontend/create_merge_request_dropdown_spec.js +++ b/spec/frontend/create_merge_request_dropdown_spec.js @@ -1,6 +1,7 @@ import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; +import confidentialState from '~/confidential_merge_request/state'; import { TEST_HOST } from './helpers/test_constants'; describe('CreateMergeRequestDropdown', () => { @@ -66,4 +67,37 @@ describe('CreateMergeRequestDropdown', () => { ); }); }); + + describe('enable', () => { + beforeEach(() => { + dropdown.createMergeRequestButton.classList.add('disabled'); + }); + + afterEach(() => { + confidentialState.selectedProject = {}; + }); + + it('enables button when not confidential issue', () => { + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled'); + }); + + it('enables when can create confidential issue', () => { + document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true'); + confidentialState.selectedProject = { name: 'test' }; + + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled'); + }); + + it('does not enable when can not create confidential issue', () => { + document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true'); + + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).toContain('disabled'); + }); + }); }); diff --git a/spec/frontend/cycle_analytics/stage_nav_item_spec.js b/spec/frontend/cycle_analytics/stage_nav_item_spec.js new file mode 100644 index 00000000000..ff079082ca7 --- /dev/null +++ b/spec/frontend/cycle_analytics/stage_nav_item_spec.js @@ -0,0 +1,177 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import StageNavItem from '~/cycle_analytics/components/stage_nav_item.vue'; + +describe('StageNavItem', () => { + let wrapper = null; + const title = 'Cool stage'; + const value = '1 day'; + + function createComponent(props, shallow = true) { + const func = shallow ? shallowMount : mount; + return func(StageNavItem, { + propsData: { + canEdit: false, + isActive: false, + isUserAllowed: false, + isDefaultStage: true, + title, + value, + ...props, + }, + }); + } + + function hasStageName() { + const stageName = wrapper.find('.stage-name'); + expect(stageName.exists()).toBe(true); + expect(stageName.text()).toEqual(title); + } + + it('renders stage name', () => { + wrapper = createComponent({ isUserAllowed: true }); + hasStageName(); + wrapper.destroy(); + }); + + describe('User has access', () => { + describe('with a value', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: true }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders the value for median value', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(false); + expect(wrapper.find('.not-available').exists()).toBe(false); + expect(wrapper.find('.stage-median').text()).toEqual(value); + }); + }); + + describe('without a value', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: true, value: null }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('has the stage-empty class', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(true); + }); + + it('renders Not enough data for the median value', () => { + expect(wrapper.find('.stage-median').text()).toEqual('Not enough data'); + }); + }); + }); + + describe('is active', () => { + beforeEach(() => { + wrapper = createComponent({ isActive: true }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('has the active class', () => { + expect(wrapper.find('.stage-nav-item').classes('active')).toBe(true); + }); + }); + + describe('is not active', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('emits the `select` event when clicked', () => { + expect(wrapper.emitted().select).toBeUndefined(); + wrapper.trigger('click'); + expect(wrapper.emitted().select.length).toBe(1); + }); + }); + + describe('User does not have access', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: false }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders stage name', () => { + hasStageName(); + }); + + it('has class not-available', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(false); + expect(wrapper.find('.not-available').exists()).toBe(true); + }); + + it('renders Not available for the median value', () => { + expect(wrapper.find('.stage-median').text()).toBe('Not available'); + }); + it('does not render options menu', () => { + expect(wrapper.find('.more-actions-toggle').exists()).toBe(false); + }); + }); + + describe('User can edit stages', () => { + beforeEach(() => { + wrapper = createComponent({ canEdit: true, isUserAllowed: true }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders stage name', () => { + hasStageName(); + }); + + it('renders options menu', () => { + expect(wrapper.find('.more-actions-toggle').exists()).toBe(true); + }); + + describe('Default stages', () => { + beforeEach(() => { + wrapper = createComponent( + { canEdit: true, isUserAllowed: true, isDefaultStage: true }, + false, + ); + }); + it('can hide the stage', () => { + expect(wrapper.text()).toContain('Hide stage'); + }); + it('can not edit the stage', () => { + expect(wrapper.text()).not.toContain('Edit stage'); + }); + it('can not remove the stage', () => { + expect(wrapper.text()).not.toContain('Remove stage'); + }); + }); + + describe('Custom stages', () => { + beforeEach(() => { + wrapper = createComponent( + { canEdit: true, isUserAllowed: true, isDefaultStage: false }, + false, + ); + }); + it('can edit the stage', () => { + expect(wrapper.text()).toContain('Edit stage'); + }); + it('can remove the stage', () => { + expect(wrapper.text()).toContain('Remove stage'); + }); + + it('can not hide the stage', () => { + expect(wrapper.text()).not.toContain('Hide stage'); + }); + }); + }); +}); diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index a8c8688441d..290c0e797cb 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -1,8 +1,11 @@ /* eslint-disable import/no-commonjs */ +const path = require('path'); const { ErrorWithStack } = require('jest-util'); const JSDOMEnvironment = require('jest-environment-jsdom'); +const ROOT_PATH = path.resolve(__dirname, '../..'); + class CustomEnvironment extends JSDOMEnvironment { constructor(config, context) { super(config, context); @@ -35,9 +38,8 @@ class CustomEnvironment extends JSDOMEnvironment { this.rejectedPromises.push(error); }; - this.global.fixturesBasePath = `${process.cwd()}/${ - IS_EE ? 'ee/' : '' - }spec/javascripts/fixtures`; + this.global.fixturesBasePath = `${ROOT_PATH}/tmp/tests/frontend/fixtures${IS_EE ? '-ee' : ''}`; + this.global.staticFixturesBasePath = `${ROOT_PATH}/spec/frontend/fixtures`; // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317 this.global.document.createRange = () => ({ diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js new file mode 100644 index 00000000000..a1a22274e8f --- /dev/null +++ b/spec/frontend/environments/confirm_rollback_modal_spec.js @@ -0,0 +1,70 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue'; +import eventHub from '~/environments/event_hub'; + +describe('Confirm Rollback Modal Component', () => { + let environment; + + beforeEach(() => { + environment = { + name: 'test', + last_deployment: { + commit: { + short_id: 'abc0123', + }, + }, + modalId: 'test', + }; + }); + + it('should show "Rollback" when isLastDeployment is false', () => { + const component = shallowMount(ConfirmRollbackModal, { + propsData: { + environment: { + ...environment, + isLastDeployment: false, + }, + }, + }); + const modal = component.find(GlModal); + + expect(modal.attributes('title')).toContain('Rollback'); + expect(modal.attributes('title')).toContain('test'); + expect(modal.attributes('ok-title')).toBe('Rollback'); + expect(modal.text()).toContain('commit abc0123'); + expect(modal.text()).toContain('Are you sure you want to continue?'); + }); + + it('should show "Re-deploy" when isLastDeployment is true', () => { + const component = shallowMount(ConfirmRollbackModal, { + propsData: { + environment: { + ...environment, + isLastDeployment: true, + }, + }, + }); + const modal = component.find(GlModal); + + expect(modal.attributes('title')).toContain('Re-deploy'); + expect(modal.attributes('title')).toContain('test'); + expect(modal.attributes('ok-title')).toBe('Re-deploy'); + expect(modal.text()).toContain('commit abc0123'); + expect(modal.text()).toContain('Are you sure you want to continue?'); + }); + + it('should emit the "rollback" event when "ok" is clicked', () => { + environment = { ...environment, isLastDeployment: true }; + const component = shallowMount(ConfirmRollbackModal, { + propsData: { + environment, + }, + }); + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const modal = component.find(GlModal); + modal.vm.$emit('ok'); + + expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', environment); + }); +}); diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js new file mode 100644 index 00000000000..fb62a096c3d --- /dev/null +++ b/spec/frontend/environments/environment_rollback_spec.js @@ -0,0 +1,53 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import eventHub from '~/environments/event_hub'; +import RollbackComponent from '~/environments/components/environment_rollback.vue'; + +describe('Rollback Component', () => { + const retryUrl = 'https://gitlab.com/retry'; + + it('Should render Re-deploy label when isLastDeployment is true', () => { + const wrapper = mount(RollbackComponent, { + propsData: { + retryUrl, + isLastDeployment: true, + environment: {}, + }, + }); + + expect(wrapper.element).toHaveSpriteIcon('repeat'); + }); + + it('Should render Rollback label when isLastDeployment is false', () => { + const wrapper = mount(RollbackComponent, { + propsData: { + retryUrl, + isLastDeployment: false, + environment: {}, + }, + }); + + expect(wrapper.element).toHaveSpriteIcon('redo'); + }); + + it('should emit a "rollback" event on button click', () => { + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const wrapper = shallowMount(RollbackComponent, { + propsData: { + retryUrl, + environment: { + name: 'test', + }, + }, + }); + const button = wrapper.find(GlButton); + + button.vm.$emit('click'); + + expect(eventHubSpy).toHaveBeenCalledWith('requestRollbackEnvironment', { + retryUrl, + isLastDeployment: true, + name: 'test', + }); + }); +}); diff --git a/spec/frontend/filterable_list_spec.js b/spec/frontend/filterable_list_spec.js new file mode 100644 index 00000000000..67d18611661 --- /dev/null +++ b/spec/frontend/filterable_list_spec.js @@ -0,0 +1,53 @@ +import FilterableList from '~/filterable_list'; +import { getJSONFixture, setHTMLFixture } from './helpers/fixtures'; + +describe('FilterableList', () => { + let List; + let form; + let filter; + let holder; + + beforeEach(() => { + setHTMLFixture(` + <form id="project-filter-form"> + <input name="name" class="js-projects-list-filter" /> + </div> + <div class="js-projects-list-holder"></div> + `); + getJSONFixture('static/projects.json'); + form = document.querySelector('form#project-filter-form'); + filter = document.querySelector('.js-projects-list-filter'); + holder = document.querySelector('.js-projects-list-holder'); + List = new FilterableList(form, filter, holder); + }); + + it('processes input parameters', () => { + expect(List.filterForm).toEqual(form); + expect(List.listFilterElement).toEqual(filter); + expect(List.listHolderElement).toEqual(holder); + }); + + describe('getPagePath', () => { + it('returns properly constructed base endpoint', () => { + List.filterForm.action = '/foo/bar/'; + List.listFilterElement.value = 'blah'; + + expect(List.getPagePath()).toEqual('/foo/bar/?name=blah'); + }); + + it('properly appends custom parameters to existing URL', () => { + List.filterForm.action = '/foo/bar?alpha=beta'; + List.listFilterElement.value = 'blah'; + + expect(List.getPagePath()).toEqual('/foo/bar?alpha=beta&name=blah'); + }); + }); + + describe('getFilterEndpoint', () => { + it('returns getPagePath by default', () => { + jest.spyOn(List, 'getPagePath').mockReturnValue('blah/blah/foo'); + + expect(List.getFilterEndpoint()).toEqual(List.getPagePath()); + }); + }); +}); diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb new file mode 100644 index 00000000000..21356390cae --- /dev/null +++ b/spec/frontend/fixtures/abuse_reports.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let!(:abuse_report) { create(:abuse_report) } + let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } + let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } + + render_views + + before(:all) do + clean_frontend_fixtures('abuse_reports/') + end + + before do + sign_in(admin) + end + + it 'abuse_reports/abuse_reports_list.html' do + get :index + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/admin_users.rb b/spec/frontend/fixtures/admin_users.rb new file mode 100644 index 00000000000..0209594dadc --- /dev/null +++ b/spec/frontend/fixtures/admin_users.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('admin/users') + end + + it 'admin/users/new_with_internal_user_regex.html' do + stub_application_setting(user_default_external: true) + stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') + + get :new + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb new file mode 100644 index 00000000000..38a060580c1 --- /dev/null +++ b/spec/frontend/fixtures/application_settings.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('application_settings/') + end + + after do + remove_repository(project) + end + + it 'application_settings/accounts_and_limit.html' do + stub_application_setting(user_default_external: false) + + get :show + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/autocomplete_sources.rb b/spec/frontend/fixtures/autocomplete_sources.rb new file mode 100644 index 00000000000..9e04328e2b9 --- /dev/null +++ b/spec/frontend/fixtures/autocomplete_sources.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:admin) { create(:admin) } + set(:group) { create(:group, name: 'frontend-fixtures') } + set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } + set(:issue) { create(:issue, project: project) } + + before(:all) do + clean_frontend_fixtures('autocomplete_sources/') + end + + before do + sign_in(admin) + end + + it 'autocomplete_sources/labels.json' do + issue.labels << create(:label, project: project, title: 'bug') + issue.labels << create(:label, project: project, title: 'critical') + + create(:label, project: project, title: 'feature') + create(:label, project: project, title: 'documentation') + + get :labels, + format: :json, + params: { + namespace_id: group.path, + project_id: project.path, + type: issue.class.name, + type_id: issue.id + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb new file mode 100644 index 00000000000..ce5030efbf8 --- /dev/null +++ b/spec/frontend/fixtures/blob.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('blob/') + end + + before do + sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') + end + + after do + remove_repository(project) + end + + it 'blob/show.html' do + get(:show, params: { + namespace_id: project.namespace, + project_id: project, + id: 'add-ipython-files/files/ipython/basic.ipynb' + }) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/boards.rb b/spec/frontend/fixtures/boards.rb new file mode 100644 index 00000000000..f257d80390f --- /dev/null +++ b/spec/frontend/fixtures/boards.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('boards/') + end + + before do + sign_in(admin) + end + + it 'boards/show.html' do + get(:index, params: { + namespace_id: project.namespace, + project_id: project + }) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb new file mode 100644 index 00000000000..197fe42c52a --- /dev/null +++ b/spec/frontend/fixtures/branches.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('branches/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'branches/new_branch.html' do + get :new, params: { + namespace_id: project.namespace.to_param, + project_id: project + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb new file mode 100644 index 00000000000..f15ef010807 --- /dev/null +++ b/spec/frontend/fixtures/clusters.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace) } + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + + render_views + + before(:all) do + clean_frontend_fixtures('clusters/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'clusters/show_cluster.html' do + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: cluster + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb new file mode 100644 index 00000000000..a328c455356 --- /dev/null +++ b/spec/frontend/fixtures/commit.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + let(:commit) { project.commit("master") } + + render_views + + before(:all) do + clean_frontend_fixtures('commit/') + end + + before do + project.add_maintainer(user) + sign_in(user) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') + end + + it 'commit/show.html' do + params = { + namespace_id: project.namespace, + project_id: project, + id: commit.id + } + + get :show, params: params + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb new file mode 100644 index 00000000000..fca233c6f59 --- /dev/null +++ b/spec/frontend/fixtures/deploy_keys.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } + let(:project2) { create(:project, :internal)} + let(:project3) { create(:project, :internal)} + let(:project4) { create(:project, :internal)} + + before(:all) do + clean_frontend_fixtures('deploy_keys/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + render_views + + it 'deploy_keys/keys.json' do + create(:rsa_deploy_key_2048, public: true) + project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') + internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') + create(:deploy_keys_project, project: project, deploy_key: project_key) + create(:deploy_keys_project, project: project2, deploy_key: internal_key) + create(:deploy_keys_project, project: project3, deploy_key: project_key) + create(:deploy_keys_project, project: project4, deploy_key: project_key) + + get :index, params: { + namespace_id: project.namespace.to_param, + project_id: project + }, format: :json + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb new file mode 100644 index 00000000000..c1bb2d43332 --- /dev/null +++ b/spec/frontend/fixtures/groups.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'Groups (JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')} + + render_views + + before(:all) do + clean_frontend_fixtures('groups/') + end + + before do + group.add_maintainer(admin) + sign_in(admin) + end + + describe GroupsController, '(JavaScript fixtures)', type: :controller do + it 'groups/edit.html' do + get :edit, params: { id: group } + + expect(response).to be_successful + end + end + + describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'groups/ci_cd_settings.html' do + get :show, params: { group_id: group } + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb new file mode 100644 index 00000000000..b5eb38e0023 --- /dev/null +++ b/spec/frontend/fixtures/issues.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin, feed_token: 'feedtoken:coldfeed') } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('issues/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'issues/open-issue.html' do + render_issue(create(:issue, project: project)) + end + + it 'issues/closed-issue.html' do + render_issue(create(:closed_issue, project: project)) + end + + it 'issues/issue-with-task-list.html' do + issue = create(:issue, project: project, description: '- [ ] Task List Item') + render_issue(issue) + end + + it 'issues/issue_with_comment.html' do + issue = create(:issue, project: project) + create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save + render_issue(issue) + end + + it 'issues/issue_list.html' do + create(:issue, project: project) + + get :index, params: { + namespace_id: project.namespace.to_param, + project_id: project + } + + expect(response).to be_successful + end + + private + + def render_issue(issue) + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: issue.to_param + } + + expect(response).to be_successful + end +end + +describe API::Issues, '(JavaScript fixtures)', type: :request do + include ApiHelpers + include JavaScriptFixturesHelpers + + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: "master", + target_branch: "test", + assignee: user, + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + it 'issues/related_merge_requests.json' do + user = create(:user) + project = create(:project, :public, creator_id: user.id, namespace: user.namespace) + issue_title = 'foo' + issue_description = 'closed' + milestone = create(:milestone, title: '1.0.0', project: project) + issue = create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + + project.add_reporter(user) + create_referencing_mr(user, project, issue) + + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "Some description") + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline)) + + get_related_merge_requests(project.id, issue.iid, user) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb new file mode 100644 index 00000000000..a3a7759c85b --- /dev/null +++ b/spec/frontend/fixtures/jobs.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } + let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } + let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } + let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') } + let!(:delayed_job) do + create(:ci_build, :scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test') + end + + render_views + + before(:all) do + clean_frontend_fixtures('builds/') + clean_frontend_fixtures('jobs/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'builds/build-with-artifacts.html' do + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: build_with_artifacts.to_param + } + + expect(response).to be_successful + end + + it 'jobs/delayed.json' do + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: delayed_job.to_param + }, format: :json + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb new file mode 100644 index 00000000000..a312287970f --- /dev/null +++ b/spec/frontend/fixtures/labels.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe 'Labels (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:group) { create(:group, name: 'frontend-fixtures-group' )} + let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') } + + let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') } + let!(:project_label_enhancement) { create(:label, project: project, title: 'enhancement', color: '#00FF00') } + let!(:project_label_feature) { create(:label, project: project, title: 'feature', color: '#0000FF') } + + let!(:group_label_roses) { create(:group_label, group: group, title: 'roses', color: '#FF0000') } + let!(:groub_label_space) { create(:group_label, group: group, title: 'some space', color: '#FFFFFF') } + let!(:groub_label_violets) { create(:group_label, group: group, title: 'violets', color: '#0000FF') } + + before(:all) do + clean_frontend_fixtures('labels/') + end + + after do + remove_repository(project) + end + + describe Groups::LabelsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(admin) + end + + it 'labels/group_labels.json' do + get :index, params: { + group_id: group + }, format: 'json' + + expect(response).to be_successful + end + end + + describe Projects::LabelsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(admin) + end + + it 'labels/project_labels.json' do + get :index, params: { + namespace_id: group, + project_id: project + }, format: 'json' + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb new file mode 100644 index 00000000000..88706e96676 --- /dev/null +++ b/spec/frontend/fixtures/merge_requests.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) } + let(:pipeline) do + create( + :ci_pipeline, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha + ) + end + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + render_views + + before(:all) do + clean_frontend_fixtures('merge_requests/') + end + + before do + sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) + end + + after do + remove_repository(project) + end + + it 'merge_requests/merge_request_of_current_user.html' do + merge_request.update(author: admin) + + render_merge_request(merge_request) + end + + it 'merge_requests/merge_request_with_task_list.html' do + create(:ci_build, :pending, pipeline: pipeline) + + render_merge_request(merge_request) + end + + it 'merge_requests/merged_merge_request.html' do + expect_next_instance_of(MergeRequest) do |merge_request| + allow(merge_request).to receive(:source_branch_exists?).and_return(true) + allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) + end + render_merge_request(merged_merge_request) + end + + it 'merge_requests/diff_comment.html' do + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(merge_request) + end + + it 'merge_requests/merge_request_with_comment.html' do + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') + render_merge_request(merge_request) + end + + it 'merge_requests/discussions.json' do + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + render_discussions_json(merge_request) + end + + it 'merge_requests/diff_discussion.json' do + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + render_discussions_json(merge_request) + end + + it 'merge_requests/resolved_diff_discussion.json' do + note = create(:discussion_note_on_merge_request, :resolved, project: project, author: admin, position: position, noteable: merge_request) + create(:system_note, project: project, author: admin, noteable: merge_request, discussion_id: note.discussion.id) + + render_discussions_json(merge_request) + end + + context 'with image diff' do + let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images") } + let(:image_path) { "files/images/ee_repo_logo.png" } + let(:image_position) do + Gitlab::Diff::Position.new( + old_path: image_path, + new_path: image_path, + width: 100, + height: 100, + x: 1, + y: 1, + position_type: "image", + diff_refs: merge_request2.diff_refs + ) + end + + it 'merge_requests/image_diff_discussion.json' do + create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) + render_discussions_json(merge_request2) + end + end + + private + + def render_discussions_json(merge_request) + get :discussions, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param + }, format: :json + end + + def render_merge_request(merge_request) + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param + }, format: :html + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb new file mode 100644 index 00000000000..b633a0495a6 --- /dev/null +++ b/spec/frontend/fixtures/merge_requests_diffs.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:path) { "files/ruby/popen.rb" } + let(:selected_commit) { merge_request.all_commits[0] } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + render_views + + before(:all) do + clean_frontend_fixtures('merge_request_diffs/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'merge_request_diffs/with_commit.json' do + # Create a user that matches the selected commit author + # This is so that the "author" information will be populated + create(:user, email: selected_commit.author_email, name: selected_commit.author_name) + + render_merge_request(merge_request, commit_id: selected_commit.sha) + end + + it 'merge_request_diffs/inline_changes_tab_with_comments.json' do + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(merge_request) + end + + it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(merge_request, view: 'parallel') + end + + private + + def render_merge_request(merge_request, view: 'inline', **extra_params) + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param, + view: view, + **extra_params + }, format: :json + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb new file mode 100644 index 00000000000..a70091a3919 --- /dev/null +++ b/spec/frontend/fixtures/pipeline_schedules.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :public, :repository) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) } + let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) } + + render_views + + before(:all) do + clean_frontend_fixtures('pipeline_schedules/') + end + + before do + sign_in(admin) + end + + it 'pipeline_schedules/edit.html' do + get :edit, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule.id + } + + expect(response).to be_successful + end + + it 'pipeline_schedules/edit_with_variables.html' do + get :edit, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule_populated.id + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb new file mode 100644 index 00000000000..ed57eb0aa80 --- /dev/null +++ b/spec/frontend/fixtures/pipelines.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') } + let(:commit) { create(:commit, project: project) } + let(:commit_without_author) { RepoHelpers.another_sample_commit } + let!(:user) { create(:user, developer_projects: [project], email: commit.author_email) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) } + let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) } + let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') } + + render_views + + before(:all) do + clean_frontend_fixtures('pipelines/') + end + + before do + sign_in(admin) + end + + it 'pipelines/pipelines.json' do + get :index, params: { + namespace_id: namespace, + project_id: project + }, format: :json + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb new file mode 100644 index 00000000000..91e3b65215a --- /dev/null +++ b/spec/frontend/fixtures/projects.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe 'Projects (JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + runners_token = 'runnerstoken:intabulasreferre' + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) } + let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } + let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } + + render_views + + before(:all) do + clean_frontend_fixtures('projects/') + end + + before do + project.add_maintainer(admin) + sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') + end + + after do + remove_repository(project) + end + + describe ProjectsController, '(JavaScript fixtures)', type: :controller do + it 'projects/dashboard.html' do + get :show, params: { + namespace_id: project.namespace.to_param, + id: project + } + + expect(response).to be_successful + end + + it 'projects/overview.html' do + get :show, params: { + namespace_id: project_with_repo.namespace.to_param, + id: project_with_repo + } + + expect(response).to be_successful + end + + it 'projects/edit.html' do + get :edit, params: { + namespace_id: project.namespace.to_param, + id: project + } + + expect(response).to be_successful + end + end + + describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'projects/ci_cd_settings.html' do + get :show, params: { + namespace_id: project.namespace.to_param, + project_id: project + } + + expect(response).to be_successful + end + + it 'projects/ci_cd_settings_with_variables.html' do + create(:ci_variable, project: project_variable_populated) + create(:ci_variable, project: project_variable_populated) + + get :show, params: { + namespace_id: project_variable_populated.namespace.to_param, + project_id: project_variable_populated + } + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb new file mode 100644 index 00000000000..93ee81120d7 --- /dev/null +++ b/spec/frontend/fixtures/prometheus_service.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:prometheus_service, project: project) } + + render_views + + before(:all) do + clean_frontend_fixtures('services/prometheus') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'services/prometheus/prometheus_service.html' do + get :edit, params: { + namespace_id: namespace, + project_id: project, + id: service.to_param + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb new file mode 100644 index 00000000000..801c80a0112 --- /dev/null +++ b/spec/frontend/fixtures/raw.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Raw files', '(JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') } + let(:response) { @blob.data.force_encoding('UTF-8') } + + before(:all) do + clean_frontend_fixtures('blob/balsamiq/') + clean_frontend_fixtures('blob/notebook/') + clean_frontend_fixtures('blob/pdf/') + end + + after do + remove_repository(project) + end + + it 'blob/balsamiq/test.bmpr' do + @blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr') + end + + it 'blob/notebook/basic.json' do + @blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb') + end + + it 'blob/notebook/worksheets.json' do + @blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb') + end + + it 'blob/notebook/math.json' do + @blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb') + end + + it 'blob/pdf/test.pdf' do + @blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') + end +end diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb new file mode 100644 index 00000000000..c26c6998ae9 --- /dev/null +++ b/spec/frontend/fixtures/search.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe SearchController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + render_views + + before(:all) do + clean_frontend_fixtures('search/') + end + + it 'search/show.html' do + get :show + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb new file mode 100644 index 00000000000..ee1e088f158 --- /dev/null +++ b/spec/frontend/fixtures/services.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } + + render_views + + before(:all) do + clean_frontend_fixtures('services/') + end + + before do + sign_in(admin) + end + + after do + remove_repository(project) + end + + it 'services/edit_service.html' do + get :edit, params: { + namespace_id: namespace, + project_id: project, + id: service.to_param + } + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/sessions.rb b/spec/frontend/fixtures/sessions.rb new file mode 100644 index 00000000000..18574ea06b5 --- /dev/null +++ b/spec/frontend/fixtures/sessions.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'Sessions (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + before(:all) do + clean_frontend_fixtures('sessions/') + end + + describe SessionsController, '(JavaScript fixtures)', type: :controller do + include DeviseHelpers + + render_views + + before do + set_devise_mapping(context: @request) + end + + it 'sessions/new.html' do + get :new + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb new file mode 100644 index 00000000000..23bcdb47ac6 --- /dev/null +++ b/spec/frontend/fixtures/snippet.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe SnippetsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } + + render_views + + before(:all) do + clean_frontend_fixtures('snippets/') + end + + before do + sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) + end + + after do + remove_repository(project) + end + + it 'snippets/show.html' do + create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') + + get(:show, params: { id: snippet.to_param }) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/static/README.md b/spec/frontend/fixtures/static/README.md new file mode 100644 index 00000000000..011601d0df8 --- /dev/null +++ b/spec/frontend/fixtures/static/README.md @@ -0,0 +1,3 @@ +# Please do not add new files here! + +Instead use a Ruby file in the fixtures root directory (`spec/frontend/fixtures/`). diff --git a/spec/frontend/fixtures/static/ajax_loading_spinner.html b/spec/frontend/fixtures/static/ajax_loading_spinner.html new file mode 100644 index 00000000000..0e1ebb32b1c --- /dev/null +++ b/spec/frontend/fixtures/static/ajax_loading_spinner.html @@ -0,0 +1,3 @@ +<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami"> +<i class="fa fa-trash-o"></i> +</a> diff --git a/spec/frontend/fixtures/static/balsamiq_viewer.html b/spec/frontend/fixtures/static/balsamiq_viewer.html new file mode 100644 index 00000000000..cdd723d1a84 --- /dev/null +++ b/spec/frontend/fixtures/static/balsamiq_viewer.html @@ -0,0 +1 @@ +<div class="file-content balsamiq-viewer" data-endpoint="/test" id="js-balsamiq-viewer"></div> diff --git a/spec/frontend/fixtures/static/create_item_dropdown.html b/spec/frontend/fixtures/static/create_item_dropdown.html new file mode 100644 index 00000000000..d2d38370092 --- /dev/null +++ b/spec/frontend/fixtures/static/create_item_dropdown.html @@ -0,0 +1,11 @@ +<div class="js-create-item-dropdown-fixture-root"> +<input name="variable[environment]" type="hidden"> +<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list"> +<li> +<button class="dropdown-create-new-item-button js-dropdown-create-new-item"> +Create wildcard +<code></code> +</button> +</li> +</ul> +</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div> diff --git a/spec/frontend/fixtures/static/environments/table.html b/spec/frontend/fixtures/static/environments/table.html new file mode 100644 index 00000000000..417af564ff1 --- /dev/null +++ b/spec/frontend/fixtures/static/environments/table.html @@ -0,0 +1,15 @@ +<table> +<thead> +<tr> +<th>Environment</th> +<th>Last deployment</th> +<th>Job</th> +<th>Commit</th> +<th></th> +<th></th> +</tr> +</thead> +<tbody> +<tr id="environment-row"></tr> +</tbody> +</table> diff --git a/spec/frontend/fixtures/static/environments_logs.html b/spec/frontend/fixtures/static/environments_logs.html new file mode 100644 index 00000000000..6179d3dbc23 --- /dev/null +++ b/spec/frontend/fixtures/static/environments_logs.html @@ -0,0 +1,29 @@ +<div class="js-kubernetes-logs" data-logs-path="/root/kubernetes-app/environments/1/logs"> + <div class="build-page"> + <div class="build-trace-container prepend-top-default"> + <div class="top-bar js-top-bar"> + <div class="truncated-info hidden-xs pull-left"></div> + <div class="dropdown prepend-left-10 js-pod-dropdown"> + <button aria-expanded="false" class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div> + </div> + <div class="controllers pull-right"> + <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to top"> + <button class="js-scroll-up btn-scroll btn-transparent btn-blank" disabled type="button"></button> + </div> + <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to bottom"> + <button class="js-scroll-down btn-scroll btn-transparent btn-blank" disabled type="button"></button> + </div> + <div class="refresh-control pull-right"> + <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Refresh"> + <button class="js-refresh-log btn-default btn-refresh" disabled type="button"></button> + </div> + </div> + </div> + </div> + <pre class="build-trace" id="build-trace"><code class="bash js-build-output"><div class="build-loader-animation js-build-refresh"></div></code></pre> + </div> + </div> +</div> diff --git a/spec/frontend/fixtures/static/event_filter.html b/spec/frontend/fixtures/static/event_filter.html new file mode 100644 index 00000000000..8e9b6fb1b5c --- /dev/null +++ b/spec/frontend/fixtures/static/event_filter.html @@ -0,0 +1,44 @@ +<ul class="nav-links event-filter scrolling-tabs nav nav-tabs"> +<li class="active"> +<a class="event-filter-link" href="/dashboard/activity" id="all_event_filter" title="Filter by all"> +<span> +All +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="push_event_filter" title="Filter by push events"> +<span> +Push events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="merged_event_filter" title="Filter by merge events"> +<span> +Merge events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="issue_event_filter" title="Filter by issue events"> +<span> +Issue events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="comments_event_filter" title="Filter by comments"> +<span> +Comments +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="team_event_filter" title="Filter by team"> +<span> +Team +</span> +</a> +</li> +</ul> diff --git a/spec/frontend/fixtures/static/gl_dropdown.html b/spec/frontend/fixtures/static/gl_dropdown.html new file mode 100644 index 00000000000..08f6738414e --- /dev/null +++ b/spec/frontend/fixtures/static/gl_dropdown.html @@ -0,0 +1,26 @@ +<div> +<div class="dropdown inline"> +<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button"> +<div class="dropdown-toggle-text"> +Projects +</div> +<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> +<div class="dropdown-title"> +<span>Go to project</span> +<button aria="{:label=>"Close"}" class="dropdown-title-button dropdown-menu-close"> +<i class="fa fa-times dropdown-menu-close-icon"></i> +</button> +</div> +<div class="dropdown-input"> +<input class="dropdown-input-field" placeholder="Filter results" type="search"> +<i class="fa fa-search dropdown-input-search"></i> +</div> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> diff --git a/spec/frontend/fixtures/static/gl_field_errors.html b/spec/frontend/fixtures/static/gl_field_errors.html new file mode 100644 index 00000000000..f8470e02b7c --- /dev/null +++ b/spec/frontend/fixtures/static/gl_field_errors.html @@ -0,0 +1,22 @@ +<form action="submit" class="gl-show-field-errors" method="post"> +<div class="form-group"> +<input class="required-text" required type="text">Text</input> +</div> +<div class="form-group"> +<input class="email" required title="Please provide a valid email address." type="email">Email</input> +</div> +<div class="form-group"> +<input class="password" required type="password">Password</input> +</div> +<div class="form-group"> +<input class="alphanumeric" pattern="[a-zA-Z0-9]" required type="text">Alphanumeric</input> +</div> +<div class="form-group"> +<input class="hidden" type="hidden"> +</div> +<div class="form-group"> +<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input> +</div> +<div class="form-group"></div> +<input class="submit" type="submit">Submit</input> +</form> diff --git a/spec/frontend/fixtures/static/images/green_box.png b/spec/frontend/fixtures/static/images/green_box.png Binary files differnew file mode 100644 index 00000000000..cd1ff9f9ade --- /dev/null +++ b/spec/frontend/fixtures/static/images/green_box.png diff --git a/spec/frontend/fixtures/static/images/one_white_pixel.png b/spec/frontend/fixtures/static/images/one_white_pixel.png Binary files differnew file mode 100644 index 00000000000..073fcf40a18 --- /dev/null +++ b/spec/frontend/fixtures/static/images/one_white_pixel.png diff --git a/spec/frontend/fixtures/static/images/red_box.png b/spec/frontend/fixtures/static/images/red_box.png Binary files differnew file mode 100644 index 00000000000..73b2927da0f --- /dev/null +++ b/spec/frontend/fixtures/static/images/red_box.png diff --git a/spec/frontend/fixtures/static/issuable_filter.html b/spec/frontend/fixtures/static/issuable_filter.html new file mode 100644 index 00000000000..06b70fb43f1 --- /dev/null +++ b/spec/frontend/fixtures/static/issuable_filter.html @@ -0,0 +1,9 @@ +<form action="/user/project/issues?scope=all&state=closed" class="js-filter-form"> +<input id="utf8" name="utf8" value="✓"> +<input id="check-all-issues" name="check-all-issues"> +<input id="search" name="search"> +<input id="author_id" name="author_id"> +<input id="assignee_id" name="assignee_id"> +<input id="milestone_title" name="milestone_title"> +<input id="label_name" name="label_name"> +</form> diff --git a/spec/frontend/fixtures/static/issue_sidebar_label.html b/spec/frontend/fixtures/static/issue_sidebar_label.html new file mode 100644 index 00000000000..ec8fb30f219 --- /dev/null +++ b/spec/frontend/fixtures/static/issue_sidebar_label.html @@ -0,0 +1,26 @@ +<div class="block labels"> +<div class="sidebar-collapsed-icon js-sidebar-labels-tooltip"></div> +<div class="title hide-collapsed"> +<a class="edit-link float-right" href="#"> +Edit +</a> +</div> +<div class="selectbox hide-collapsed" style="display: none;"> +<div class="dropdown"> +<button class="dropdown-menu-toggle js-label-select js-multiselect" data-ability-name="issue" data-field-name="issue[label_names][]" data-issue-update="/root/test/issues/2.json" data-labels="/root/test/labels.json" data-project-id="12" data-show-any="true" data-show-no="true" data-toggle="dropdown" type="button"> +<span class="dropdown-toggle-text"> +Label +</span> +<i class="fa fa-chevron-down"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> +<div class="dropdown-page-one"> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> +</div> +</div> diff --git a/spec/frontend/fixtures/static/line_highlighter.html b/spec/frontend/fixtures/static/line_highlighter.html new file mode 100644 index 00000000000..897a25d6760 --- /dev/null +++ b/spec/frontend/fixtures/static/line_highlighter.html @@ -0,0 +1,107 @@ +<div class="file-holder"> +<div class="file-content"> +<div class="line-numbers"> +<a data-line-number="1" href="#L1" id="L1"> +<i class="fa fa-link"></i> +1 +</a> +<a data-line-number="2" href="#L2" id="L2"> +<i class="fa fa-link"></i> +2 +</a> +<a data-line-number="3" href="#L3" id="L3"> +<i class="fa fa-link"></i> +3 +</a> +<a data-line-number="4" href="#L4" id="L4"> +<i class="fa fa-link"></i> +4 +</a> +<a data-line-number="5" href="#L5" id="L5"> +<i class="fa fa-link"></i> +5 +</a> +<a data-line-number="6" href="#L6" id="L6"> +<i class="fa fa-link"></i> +6 +</a> +<a data-line-number="7" href="#L7" id="L7"> +<i class="fa fa-link"></i> +7 +</a> +<a data-line-number="8" href="#L8" id="L8"> +<i class="fa fa-link"></i> +8 +</a> +<a data-line-number="9" href="#L9" id="L9"> +<i class="fa fa-link"></i> +9 +</a> +<a data-line-number="10" href="#L10" id="L10"> +<i class="fa fa-link"></i> +10 +</a> +<a data-line-number="11" href="#L11" id="L11"> +<i class="fa fa-link"></i> +11 +</a> +<a data-line-number="12" href="#L12" id="L12"> +<i class="fa fa-link"></i> +12 +</a> +<a data-line-number="13" href="#L13" id="L13"> +<i class="fa fa-link"></i> +13 +</a> +<a data-line-number="14" href="#L14" id="L14"> +<i class="fa fa-link"></i> +14 +</a> +<a data-line-number="15" href="#L15" id="L15"> +<i class="fa fa-link"></i> +15 +</a> +<a data-line-number="16" href="#L16" id="L16"> +<i class="fa fa-link"></i> +16 +</a> +<a data-line-number="17" href="#L17" id="L17"> +<i class="fa fa-link"></i> +17 +</a> +<a data-line-number="18" href="#L18" id="L18"> +<i class="fa fa-link"></i> +18 +</a> +<a data-line-number="19" href="#L19" id="L19"> +<i class="fa fa-link"></i> +19 +</a> +<a data-line-number="20" href="#L20" id="L20"> +<i class="fa fa-link"></i> +20 +</a> +<a data-line-number="21" href="#L21" id="L21"> +<i class="fa fa-link"></i> +21 +</a> +<a data-line-number="22" href="#L22" id="L22"> +<i class="fa fa-link"></i> +22 +</a> +<a data-line-number="23" href="#L23" id="L23"> +<i class="fa fa-link"></i> +23 +</a> +<a data-line-number="24" href="#L24" id="L24"> +<i class="fa fa-link"></i> +24 +</a> +<a data-line-number="25" href="#L25" id="L25"> +<i class="fa fa-link"></i> +25 +</a> +</div> +<pre class="code highlight"><code><span class="line" id="LC1">Line 1</span><span class="line" id="LC2">Line 2</span><span class="line" id="LC3">Line 3</span><span class="line" id="LC4">Line 4</span><span class="line" id="LC5">Line 5</span><span class="line" id="LC6">Line 6</span><span class="line" id="LC7">Line 7</span><span class="line" id="LC8">Line 8</span><span class="line" id="LC9">Line 9</span><span class="line" id="LC10">Line 10</span><span class="line" id="LC11">Line 11</span><span class="line" id="LC12">Line 12</span><span class="line" id="LC13">Line 13</span><span class="line" id="LC14">Line 14</span><span class="line" id="LC15">Line 15</span><span class="line" id="LC16">Line 16</span><span class="line" id="LC17">Line 17</span><span class="line" id="LC18">Line 18</span><span class="line" id="LC19">Line 19</span><span class="line" id="LC20">Line 20</span><span class="line" id="LC21">Line 21</span><span class="line" id="LC22">Line 22</span><span class="line" id="LC23">Line 23</span><span class="line" id="LC24">Line 24</span><span class="line" id="LC25">Line 25</span></code></pre> +</div> +</div> diff --git a/spec/frontend/fixtures/static/linked_tabs.html b/spec/frontend/fixtures/static/linked_tabs.html new file mode 100644 index 00000000000..c25463bf1db --- /dev/null +++ b/spec/frontend/fixtures/static/linked_tabs.html @@ -0,0 +1,20 @@ +<ul class="nav nav-tabs new-session-tabs linked-tabs"> +<li class="nav-item"> +<a class="nav-link" data-action="tab1" data-target="div#tab1" data-toggle="tab" href="foo/bar/1"> +Tab 1 +</a> +</li> +<li class="nav-item"> +<a class="nav-link" data-action="tab2" data-target="div#tab2" data-toggle="tab" href="foo/bar/1/context"> +Tab 2 +</a> +</li> +</ul> +<div class="tab-content"> +<div class="tab-pane" id="tab1"> +Tab 1 Content +</div> +<div class="tab-pane" id="tab2"> +Tab 2 Content +</div> +</div> diff --git a/spec/frontend/fixtures/static/merge_requests_show.html b/spec/frontend/fixtures/static/merge_requests_show.html new file mode 100644 index 00000000000..87e36c9f315 --- /dev/null +++ b/spec/frontend/fixtures/static/merge_requests_show.html @@ -0,0 +1,15 @@ +<a class="btn-close"></a> +<div class="detail-page-description"> +<div class="description js-task-list-container"> +<div class="md"> +<ul class="task-list"> +<li class="task-list-item"> +<input class="task-list-item-checkbox" type="checkbox"> +Task List Item +</li> +</ul> +<textarea class="js-task-list-field">- [ ] Task List Item</textarea> +</div> +</div> +</div> +<form action="/foo" class="js-issuable-update"></form> diff --git a/spec/frontend/fixtures/static/mini_dropdown_graph.html b/spec/frontend/fixtures/static/mini_dropdown_graph.html new file mode 100644 index 00000000000..cd0b8dec3fc --- /dev/null +++ b/spec/frontend/fixtures/static/mini_dropdown_graph.html @@ -0,0 +1,13 @@ +<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph"> +<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar"> +Dropdown +</button> +<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> +<li class="js-builds-dropdown-list scrollable-menu"> +<ul></ul> +</li> +<li class="js-builds-dropdown-loading hidden"> +<span class="fa fa-spinner"></span> +</li> +</ul> +</div> diff --git a/spec/frontend/fixtures/static/notebook_viewer.html b/spec/frontend/fixtures/static/notebook_viewer.html new file mode 100644 index 00000000000..4bbb7bf1094 --- /dev/null +++ b/spec/frontend/fixtures/static/notebook_viewer.html @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div> diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html new file mode 100644 index 00000000000..9ba1ffc72fe --- /dev/null +++ b/spec/frontend/fixtures/static/oauth_remember_me.html @@ -0,0 +1,6 @@ +<div id="oauth-container"> +<input id="remember_me" type="checkbox"> +<a class="oauth-login twitter" href="http://example.com/"></a> +<a class="oauth-login github" href="http://example.com/"></a> +<a class="oauth-login facebook" href="http://example.com/?redirect_fragment=L1"></a> +</div> diff --git a/spec/frontend/fixtures/static/pdf_viewer.html b/spec/frontend/fixtures/static/pdf_viewer.html new file mode 100644 index 00000000000..350d35a262f --- /dev/null +++ b/spec/frontend/fixtures/static/pdf_viewer.html @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div> diff --git a/spec/frontend/fixtures/static/pipeline_graph.html b/spec/frontend/fixtures/static/pipeline_graph.html new file mode 100644 index 00000000000..422372bb7d5 --- /dev/null +++ b/spec/frontend/fixtures/static/pipeline_graph.html @@ -0,0 +1,24 @@ +<div class="pipeline-visualization js-pipeline-graph"> +<ul class="stage-column-list"> +<li class="stage-column"> +<div class="stage-name"> +<a href="/"> +Test +<div class="builds-container"> +<ul> +<li class="build"> +<div class="curve"></div> +<a> +<svg></svg> +<div class="ci-status-text"> +stop_review +</div> +</a> +</li> +</ul> +</div> +</a> +</div> +</li> +</ul> +</div> diff --git a/spec/frontend/fixtures/static/pipelines.html b/spec/frontend/fixtures/static/pipelines.html new file mode 100644 index 00000000000..42333f94f2f --- /dev/null +++ b/spec/frontend/fixtures/static/pipelines.html @@ -0,0 +1,3 @@ +<div> +<div data-can-create-pipeline="true" data-ci-lint-path="foo" data-empty-state-svg-path="foo" data-endpoint="foo" data-error-state-svg-path="foo" data-has-ci="foo" data-help-auto-devops-path="foo" data-help-page-path="foo" data-new-pipeline-path="foo" data-reset-cache-path="foo" id="pipelines-list-vue"></div> +</div> diff --git a/spec/frontend/fixtures/static/project_select_combo_button.html b/spec/frontend/fixtures/static/project_select_combo_button.html new file mode 100644 index 00000000000..50c826051c0 --- /dev/null +++ b/spec/frontend/fixtures/static/project_select_combo_button.html @@ -0,0 +1,9 @@ +<div class="project-item-select-holder"> +<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new"> +<a class="new-project-item-link" data-label="New issue" data-type="issues" href=""> +<i class="fa fa-spinner spin"></i> +</a> +<a class="new-project-item-select-button"> +<i class="fa fa-caret-down"></i> +</a> +</div> diff --git a/spec/frontend/fixtures/static/projects.json b/spec/frontend/fixtures/static/projects.json new file mode 100644 index 00000000000..68a150f602a --- /dev/null +++ b/spec/frontend/fixtures/static/projects.json @@ -0,0 +1,445 @@ +[{ + "id": 9, + "description": "", + "default_branch": null, + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:root/test.git", + "http_url_to_repo": "http://localhost:3000/root/test.git", + "web_url": "http://localhost:3000/root/test", + "owner": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + }, + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-14T19:08:05.364Z", + "last_activity_at": "2016-01-14T19:08:07.418Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 1, + "name": "root", + "path": "root", + "owner_id": 1, + "created_at": "2016-01-13T20:19:44.439Z", + "updated_at": "2016-01-13T20:19:44.439Z", + "description": "", + "avatar": null + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 0, + "permissions": { + "project_access": null, + "group_access": null + } +}, { + "id": 8, + "description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:h5bp/html5-boilerplate.git", + "http_url_to_repo": "http://localhost:3000/h5bp/html5-boilerplate.git", + "web_url": "http://localhost:3000/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "H5bp / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:57.525Z", + "last_activity_at": "2016-01-13T20:27:57.280Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "H5bp", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-01-13T20:19:57.239Z", + "updated_at": "2016-01-13T20:19:57.239Z", + "description": "Tempore accusantium possimus aut libero.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 7, + "description": "Modi odio mollitia dolorem qui.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:twitter/typeahead-js.git", + "http_url_to_repo": "http://localhost:3000/twitter/typeahead-js.git", + "web_url": "http://localhost:3000/twitter/typeahead-js", + "name": "Typeahead.Js", + "name_with_namespace": "Twitter / Typeahead.Js", + "path": "typeahead-js", + "path_with_namespace": "twitter/typeahead-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:56.212Z", + "last_activity_at": "2016-01-13T20:27:51.496Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 6, + "description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:twitter/flight.git", + "http_url_to_repo": "http://localhost:3000/twitter/flight.git", + "web_url": "http://localhost:3000/twitter/flight", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:54.754Z", + "last_activity_at": "2016-01-13T20:27:50.502Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 5, + "description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-test.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-test.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-test", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:53.202Z", + "last_activity_at": "2016-01-13T20:27:41.626Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 4, + "description": "Aut molestias quas est ut aperiam officia quod libero.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-shell.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-shell.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-shell", + "name": "Gitlab Shell", + "name_with_namespace": "Gitlab Org / Gitlab Shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab-org/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:51.882Z", + "last_activity_at": "2016-01-13T20:27:35.678Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 20, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 3, + "description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ci.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ci.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ci", + "name": "Gitlab Ci", + "name_with_namespace": "Gitlab Org / Gitlab Ci", + "path": "gitlab-ci", + "path_with_namespace": "gitlab-org/gitlab-ci", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:50.346Z", + "last_activity_at": "2016-01-13T20:27:30.115Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 3, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 2, + "description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ce.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ce.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ce", + "name": "Gitlab Ce", + "name_with_namespace": "Gitlab Org / Gitlab Ce", + "path": "gitlab-ce", + "path_with_namespace": "gitlab-org/gitlab-ce", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:49.065Z", + "last_activity_at": "2016-01-13T20:26:58.454Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 30, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 1, + "description": "Vel voluptatem maxime saepe ex quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:documentcloud/underscore.git", + "http_url_to_repo": "http://localhost:3000/documentcloud/underscore.git", + "web_url": "http://localhost:3000/documentcloud/underscore", + "name": "Underscore", + "name_with_namespace": "Documentcloud / Underscore", + "path": "underscore", + "path_with_namespace": "documentcloud/underscore", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:45.862Z", + "last_activity_at": "2016-01-13T20:25:03.106Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 2, + "name": "Documentcloud", + "path": "documentcloud", + "owner_id": null, + "created_at": "2016-01-13T20:19:44.464Z", + "updated_at": "2016-01-13T20:19:44.464Z", + "description": "Aut impedit perferendis fuga et ipsa repellat cupiditate et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}] diff --git a/spec/frontend/fixtures/static/search_autocomplete.html b/spec/frontend/fixtures/static/search_autocomplete.html new file mode 100644 index 00000000000..29db9020424 --- /dev/null +++ b/spec/frontend/fixtures/static/search_autocomplete.html @@ -0,0 +1,15 @@ +<div class="search search-form"> +<form class="form-inline"> +<div class="search-input-container"> +<div class="search-input-wrap"> +<div class="dropdown"> +<input class="search-input dropdown-menu-toggle" id="search"> +<div class="dropdown-menu dropdown-select"> +<div class="dropdown-content"></div> +</div> +</div> +</div> +</div> +<input class="js-search-project-options" type="hidden"> +</form> +</div> diff --git a/spec/frontend/fixtures/static/signin_tabs.html b/spec/frontend/fixtures/static/signin_tabs.html new file mode 100644 index 00000000000..7e66ab9394b --- /dev/null +++ b/spec/frontend/fixtures/static/signin_tabs.html @@ -0,0 +1,8 @@ +<ul class="nav-links new-session-tabs"> +<li class="active"> +<a href="#ldap">LDAP</a> +</li> +<li> +<a href="#login-pane">Standard</a> +</li> +</ul> diff --git a/spec/frontend/fixtures/static/sketch_viewer.html b/spec/frontend/fixtures/static/sketch_viewer.html new file mode 100644 index 00000000000..e25e554e568 --- /dev/null +++ b/spec/frontend/fixtures/static/sketch_viewer.html @@ -0,0 +1,3 @@ +<div class="file-content" data-endpoint="/test_sketch_file.sketch" id="js-sketch-viewer"> +<div class="js-loading-icon"></div> +</div> diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb new file mode 100644 index 00000000000..a7c183d2414 --- /dev/null +++ b/spec/frontend/fixtures/todos.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'Todos (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) } + + before(:all) do + clean_frontend_fixtures('todos/') + end + + after do + remove_repository(project) + end + + describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(admin) + end + + it 'todos/todos.html' do + get :index + + expect(response).to be_successful + end + end + + describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(admin) + end + + it 'todos/todos.json' do + post :create, params: { + namespace_id: namespace, + project_id: project, + issuable_type: 'issue', + issuable_id: issue_2.id + }, format: 'json' + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb new file mode 100644 index 00000000000..8ecbc0390cd --- /dev/null +++ b/spec/frontend/fixtures/u2f.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +context 'U2F' do + include JavaScriptFixturesHelpers + + let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') } + + before(:all) do + clean_frontend_fixtures('u2f/') + end + + describe SessionsController, '(JavaScript fixtures)', type: :controller do + include DeviseHelpers + + render_views + + before do + set_devise_mapping(context: @request) + end + + it 'u2f/authenticate.html' do + allow(controller).to receive(:find_user).and_return(user) + + post :create, params: { user: { login: user.username, password: user.password } } + + expect(response).to be_successful + end + end + + describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(user) + allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') + end + + it 'u2f/register.html' do + get :show + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js index b77bcd6266e..778196843db 100644 --- a/spec/frontend/helpers/fixtures.js +++ b/spec/frontend/helpers/fixtures.js @@ -4,12 +4,15 @@ import path from 'path'; import { ErrorWithStack } from 'jest-util'; export function getFixture(relativePath) { - const absolutePath = path.join(global.fixturesBasePath, relativePath); + const basePath = relativePath.startsWith('static/') + ? global.staticFixturesBasePath + : global.fixturesBasePath; + const absolutePath = path.join(basePath, relativePath); if (!fs.existsSync(absolutePath)) { throw new ErrorWithStack( `Fixture file ${relativePath} does not exist. -Did you run bin/rake karma:fixtures?`, +Did you run bin/rake frontend:fixtures?`, getFixture, ); } diff --git a/spec/frontend/helpers/vue_test_utils_helper.js b/spec/frontend/helpers/vue_test_utils_helper.js index 121e99c9783..68326e37ae7 100644 --- a/spec/frontend/helpers/vue_test_utils_helper.js +++ b/spec/frontend/helpers/vue_test_utils_helper.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - const vNodeContainsText = (vnode, text) => (vnode.text && vnode.text.includes(text)) || (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length); @@ -19,3 +17,19 @@ export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) = Boolean( shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length, ); + +/** + * Returns a promise that waits for a mutation to be fired before resolving + * NOTE: There's no reject action here so it will hang if it waits for a mutation that won't happen. + * @param {Object} store - The Vue store that contains the mutations + * @param {String} expectedMutationType - The Mutation to wait for + */ +export const waitForMutation = (store, expectedMutationType) => + new Promise(resolve => { + const unsubscribe = store.subscribe(mutation => { + if (mutation.type === expectedMutationType) { + unsubscribe(); + resolve(); + } + }); + }); diff --git a/spec/frontend/helpers/vue_test_utils_helper_spec.js b/spec/frontend/helpers/vue_test_utils_helper_spec.js new file mode 100644 index 00000000000..41714066da5 --- /dev/null +++ b/spec/frontend/helpers/vue_test_utils_helper_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { shallowWrapperContainsSlotText } from './vue_test_utils_helper'; + +describe('Vue test utils helpers', () => { + describe('shallowWrapperContainsSlotText', () => { + const mockText = 'text'; + const mockSlot = `<div>${mockText}</div>`; + let mockComponent; + + beforeEach(() => { + mockComponent = shallowMount( + { + render(h) { + h(`<div>mockedComponent</div>`); + }, + }, + { + slots: { + default: mockText, + namedSlot: mockSlot, + }, + }, + ); + }); + + it('finds text within shallowWrapper default slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'default', mockText)).toBe(true); + }); + + it('finds text within shallowWrapper named slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', mockText)).toBe(true); + }); + + it('returns false when text is not present', () => { + const searchText = 'absent'; + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + + it('searches with case-sensitivity', () => { + const searchText = mockText.toUpperCase(); + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + }); +}); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 499fa8fc012..3d5ed4b5c0c 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -16,40 +16,16 @@ describe('IDE services', () => { branch: TEST_BRANCH, commit_message: 'Hello world', actions: [], - start_sha: undefined, + start_sha: TEST_COMMIT_SHA, }; - Api.createBranch.mockReturnValue(Promise.resolve()); Api.commitMultiple.mockReturnValue(Promise.resolve()); }); - describe.each` - startSha | shouldCreateBranch - ${undefined} | ${false} - ${TEST_COMMIT_SHA} | ${true} - `('when start_sha is $startSha', ({ startSha, shouldCreateBranch }) => { - beforeEach(() => { - payload.start_sha = startSha; + it('should commit', () => { + services.commit(TEST_PROJECT_ID, payload); - return services.commit(TEST_PROJECT_ID, payload); - }); - - if (shouldCreateBranch) { - it('should create branch', () => { - expect(Api.createBranch).toHaveBeenCalledWith(TEST_PROJECT_ID, { - ref: TEST_COMMIT_SHA, - branch: TEST_BRANCH, - }); - }); - } else { - it('should not create branch', () => { - expect(Api.createBranch).not.toHaveBeenCalled(); - }); - } - - it('should commit', () => { - expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload); - }); + expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload); }); }); }); diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js index 246500a2f34..45ac1a86ab3 100644 --- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js @@ -62,12 +62,4 @@ describe('IDE commit module mutations', () => { expect(state.shouldCreateMR).toBe(false); }); }); - - describe('INTERACT_WITH_NEW_MR', () => { - it('sets interactedWithNewMR to true', () => { - mutations.INTERACT_WITH_NEW_MR(state); - - expect(state.interactedWithNewMR).toBe(true); - }); - }); }); diff --git a/spec/frontend/issue_show/components/pinned_links_spec.js b/spec/frontend/issue_show/components/pinned_links_spec.js index 50041667a61..77da3390918 100644 --- a/spec/frontend/issue_show/components/pinned_links_spec.js +++ b/spec/frontend/issue_show/components/pinned_links_spec.js @@ -5,10 +5,6 @@ import PinnedLinks from '~/issue_show/components/pinned_links.vue'; const localVue = createLocalVue(); const plainZoomUrl = 'https://zoom.us/j/123456789'; -const vanityZoomUrl = 'https://gitlab.zoom.us/j/123456789'; -const startZoomUrl = 'https://zoom.us/s/123456789'; -const personalZoomUrl = 'https://zoom.us/my/hunter-zoloman'; -const randomUrl = 'https://zoom.us.com'; describe('PinnedLinks', () => { let wrapper; @@ -27,7 +23,7 @@ describe('PinnedLinks', () => { localVue, sync: false, propsData: { - descriptionHtml: '', + zoomMeetingUrl: null, ...props, }, }); @@ -35,55 +31,15 @@ describe('PinnedLinks', () => { it('displays Zoom link', () => { createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a>`, + zoomMeetingUrl: `<a href="${plainZoomUrl}">Zoom</a>`, }); expect(link.text).toBe('Join Zoom meeting'); }); - it('detects plain Zoom link', () => { + it('does not render if there are no links', () => { createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(plainZoomUrl); - }); - - it('detects vanity Zoom link', () => { - createComponent({ - descriptionHtml: `<a href="${vanityZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(vanityZoomUrl); - }); - - it('detects Zoom start meeting link', () => { - createComponent({ - descriptionHtml: `<a href="${startZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(startZoomUrl); - }); - - it('detects personal Zoom room link', () => { - createComponent({ - descriptionHtml: `<a href="${personalZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(personalZoomUrl); - }); - - it('only renders final Zoom link in description', () => { - createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a><a href="${vanityZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(vanityZoomUrl); - }); - - it('does not render for other links', () => { - createComponent({ - descriptionHtml: `<a href="${randomUrl}">Some other link</a>`, + zoomMeetingUrl: null, }); expect(wrapper.find(GlLink).exists()).toBe(false); diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js deleted file mode 100644 index a2df79bdda0..00000000000 --- a/spec/frontend/jobs/components/empty_state_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import Vue from 'vue'; -import component from '~/jobs/components/empty_state.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Empty State', () => { - const Component = Vue.extend(component); - let vm; - - const props = { - illustrationPath: 'illustrations/pending_job_empty.svg', - illustrationSizeClass: 'svg-430', - title: 'This job has not started yet', - }; - - const content = 'This job is in pending state and is waiting to be picked by a runner'; - - afterEach(() => { - vm.$destroy(); - }); - - describe('renders image and title', () => { - beforeEach(() => { - vm = mountComponent(Component, { - ...props, - content, - }); - }); - - it('renders img with provided path and size', () => { - expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath); - expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass); - }); - - it('renders provided title', () => { - expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual( - props.title, - ); - }); - }); - - describe('with content', () => { - it('renders content', () => { - vm = mountComponent(Component, { - ...props, - content, - }); - - expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual( - content, - ); - }); - }); - - describe('without content', () => { - it('does not render content', () => { - vm = mountComponent(Component, { - ...props, - }); - - expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull(); - }); - }); - - describe('with action', () => { - it('renders action', () => { - vm = mountComponent(Component, { - ...props, - content, - action: { - path: 'runner', - button_title: 'Check runner', - method: 'post', - }, - }); - - expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual( - 'runner', - ); - }); - }); - - describe('without action', () => { - it('does not render action', () => { - vm = mountComponent(Component, { - ...props, - content, - action: null, - }); - - expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); - }); - }); -}); diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js new file mode 100644 index 00000000000..433e9d5a85e --- /dev/null +++ b/spec/frontend/lib/utils/color_utils_spec.js @@ -0,0 +1,35 @@ +import { textColorForBackground, hexToRgb } from '~/lib/utils/color_utils'; + +describe('Color utils', () => { + describe('Converting hex code to rgb', () => { + it('convert hex code to rgb', () => { + expect(hexToRgb('#000000')).toEqual([0, 0, 0]); + expect(hexToRgb('#ffffff')).toEqual([255, 255, 255]); + }); + + it('convert short hex code to rgb', () => { + expect(hexToRgb('#000')).toEqual([0, 0, 0]); + expect(hexToRgb('#fff')).toEqual([255, 255, 255]); + }); + + it('handle conversion regardless of the characters case', () => { + expect(hexToRgb('#f0F')).toEqual([255, 0, 255]); + }); + }); + + describe('Getting text color for given background', () => { + // following tests are being ported from `text_color_for_bg` section in labels_helper_spec.rb + it('uses light text on dark backgrounds', () => { + expect(textColorForBackground('#222E2E')).toEqual('#FFFFFF'); + }); + + it('uses dark text on light backgrounds', () => { + expect(textColorForBackground('#EEEEEE')).toEqual('#333333'); + }); + + it('supports RGB triplets', () => { + expect(textColorForBackground('#FFF')).toEqual('#333333'); + expect(textColorForBackground('#000')).toEqual('#FFFFFF'); + }); + }); +}); diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js new file mode 100644 index 00000000000..cac17235f0d --- /dev/null +++ b/spec/frontend/lib/utils/forms_spec.js @@ -0,0 +1,74 @@ +import { serializeForm } from '~/lib/utils/forms'; + +describe('lib/utils/forms', () => { + const createDummyForm = inputs => { + const form = document.createElement('form'); + + form.innerHTML = inputs + .map(({ type, name, value }) => { + let str = ``; + if (type === 'select') { + str = `<select name="${name}">`; + value.forEach(v => { + if (v.length > 0) { + str += `<option value="${v}"></option> `; + } + }); + str += `</select>`; + } else { + str = `<input type="${type}" name="${name}" value="${value}" checked/>`; + } + return str; + }) + .join(''); + + return form; + }; + + describe('serializeForm', () => { + it('returns an object of key values from inputs', () => { + const form = createDummyForm([ + { type: 'text', name: 'foo', value: 'foo-value' }, + { type: 'text', name: 'bar', value: 'bar-value' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: 'foo-value', + bar: 'bar-value', + }); + }); + + it('works with select', () => { + const form = createDummyForm([ + { type: 'select', name: 'foo', value: ['foo-value1', 'foo-value2'] }, + { type: 'text', name: 'bar', value: 'bar-value1' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: 'foo-value1', + bar: 'bar-value1', + }); + }); + + it('works with multiple inputs of the same name', () => { + const form = createDummyForm([ + { type: 'checkbox', name: 'foo', value: 'foo-value3' }, + { type: 'checkbox', name: 'foo', value: 'foo-value2' }, + { type: 'checkbox', name: 'foo', value: 'foo-value1' }, + { type: 'text', name: 'bar', value: 'bar-value2' }, + { type: 'text', name: 'bar', value: 'bar-value1' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: ['foo-value3', 'foo-value2', 'foo-value1'], + bar: ['bar-value2', 'bar-value1'], + }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index dc886d0db3b..b6f1aef9ce4 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -29,20 +29,6 @@ describe('text_utility', () => { }); }); - describe('pluralize', () => { - it('should pluralize given string', () => { - expect(textUtils.pluralize('test', 2)).toBe('tests'); - }); - - it('should pluralize when count is 0', () => { - expect(textUtils.pluralize('test', 0)).toBe('tests'); - }); - - it('should not pluralize when count is 1', () => { - expect(textUtils.pluralize('test', 1)).toBe('test'); - }); - }); - describe('dasherize', () => { it('should replace underscores with dashes', () => { expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo'); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index c771984a137..b0bdd924921 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -34,6 +34,41 @@ describe('URL utility', () => { }); }); + describe('getParameterValues', () => { + beforeEach(() => { + setWindowLocation({ + href: 'https://gitlab.com?test=passing&multiple=1&multiple=2', + // make our fake location act like real window.location.toString + // URL() (used in getParameterValues) does this if passed an object + toString() { + return this.href; + }, + }); + }); + + it('returns empty array for no params', () => { + expect(urlUtils.getParameterValues()).toEqual([]); + }); + + it('returns empty array for non-matching params', () => { + expect(urlUtils.getParameterValues('notFound')).toEqual([]); + }); + + it('returns single match', () => { + expect(urlUtils.getParameterValues('test')).toEqual(['passing']); + }); + + it('returns multiple matches', () => { + expect(urlUtils.getParameterValues('multiple')).toEqual(['1', '2']); + }); + + it('accepts url as second arg', () => { + const url = 'https://gitlab.com?everything=works'; + expect(urlUtils.getParameterValues('everything', url)).toEqual(['works']); + expect(urlUtils.getParameterValues('test', url)).toEqual([]); + }); + }); + describe('mergeUrlParams', () => { it('adds w', () => { expect(urlUtils.mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); @@ -59,6 +94,12 @@ describe('URL utility', () => { it('adds and updates encoded params', () => { expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); }); + + it('treats "+" as "%20"', () => { + expect(urlUtils.mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe( + '?a=lorem%20ipsum&ref=bogus', + ); + }); }); describe('removeParams', () => { diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js new file mode 100644 index 00000000000..35c362d0bf5 --- /dev/null +++ b/spec/frontend/matchers.js @@ -0,0 +1,38 @@ +export default { + toHaveSpriteIcon: (element, iconName) => { + if (!iconName) { + throw new Error('toHaveSpriteIcon is missing iconName argument!'); + } + + if (!(element instanceof HTMLElement)) { + throw new Error(`${element} is not a DOM element!`); + } + + const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); + const matchingIcon = iconReferences.find(reference => + reference.getAttribute('xlink:href').endsWith(`#${iconName}`), + ); + + const pass = Boolean(matchingIcon); + + let message; + if (pass) { + message = `${element.outerHTML} contains the sprite icon "${iconName}"!`; + } else { + message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; + + const existingIcons = iconReferences.map(reference => { + const iconUrl = reference.getAttribute('xlink:href'); + return `"${iconUrl.replace(/^.+#/, '')}"`; + }); + if (existingIcons.length > 0) { + message += ` (only found ${existingIcons.join(',')})`; + } + } + + return { + pass, + message: () => message, + }; + }, +}; diff --git a/spec/frontend/mocks/ce/lib/utils/axios_utils.js b/spec/frontend/mocks/ce/lib/utils/axios_utils.js new file mode 100644 index 00000000000..b4065626b09 --- /dev/null +++ b/spec/frontend/mocks/ce/lib/utils/axios_utils.js @@ -0,0 +1,15 @@ +const axios = jest.requireActual('~/lib/utils/axios_utils').default; + +axios.isMock = true; + +// Fail tests for unmocked requests +axios.defaults.adapter = config => { + const message = + `Unexpected unmocked request: ${JSON.stringify(config, null, 2)}\n` + + 'Consider using the `axios-mock-adapter` in tests.'; + const error = new Error(message); + error.config = config; + throw error; +}; + +export default axios; diff --git a/spec/frontend/mocks/mocks_helper.js b/spec/frontend/mocks/mocks_helper.js new file mode 100644 index 00000000000..21c032cd3c9 --- /dev/null +++ b/spec/frontend/mocks/mocks_helper.js @@ -0,0 +1,60 @@ +/** + * @module + * + * This module implements auto-injected manual mocks that are cleaner than Jest's approach. + * + * See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html + */ + +import fs from 'fs'; +import path from 'path'; + +import readdir from 'readdir-enhanced'; + +const MAX_DEPTH = 20; +const prefixMap = [ + // E.g. the mock ce/foo/bar maps to require path ~/foo/bar + { mocksRoot: 'ce', requirePrefix: '~' }, + // { mocksRoot: 'ee', requirePrefix: 'ee' }, // We'll deal with EE-specific mocks later + { mocksRoot: 'node', requirePrefix: '' }, + // { mocksRoot: 'virtual', requirePrefix: '' }, // We'll deal with virtual mocks later +]; + +const mockFileFilter = stats => stats.isFile() && stats.path.endsWith('.js'); + +const getMockFiles = root => readdir.sync(root, { deep: MAX_DEPTH, filter: mockFileFilter }); + +// Function that performs setting a mock. This has to be overridden by the unit test, because +// jest.setMock can't be overwritten across files. +// Use require() because jest.setMock expects the CommonJS exports object +const defaultSetMock = (srcPath, mockPath) => + jest.mock(srcPath, () => jest.requireActual(mockPath)); + +// eslint-disable-next-line import/prefer-default-export +export const setupManualMocks = function setupManualMocks(setMock = defaultSetMock) { + prefixMap.forEach(({ mocksRoot, requirePrefix }) => { + const mocksRootAbsolute = path.join(__dirname, mocksRoot); + if (!fs.existsSync(mocksRootAbsolute)) { + return; + } + + getMockFiles(path.join(__dirname, mocksRoot)).forEach(mockPath => { + const mockPathNoExt = mockPath.substring(0, mockPath.length - path.extname(mockPath).length); + const sourcePath = path.join(requirePrefix, mockPathNoExt); + const mockPathRelative = `./${path.join(mocksRoot, mockPathNoExt)}`; + + try { + setMock(sourcePath, mockPathRelative); + } catch (e) { + if (e.message.includes('Could not locate module')) { + // The corresponding mocked module doesn't exist. Raise a better error. + // Eventualy, we may support virtual mocks (mocks whose path doesn't directly correspond + // to a module, like with the `ee_else_ce` prefix). + throw new Error( + `A manual mock was defined for module ${sourcePath}, but the module doesn't exist!`, + ); + } + } + }); + }); +}; diff --git a/spec/frontend/mocks/mocks_helper_spec.js b/spec/frontend/mocks/mocks_helper_spec.js new file mode 100644 index 00000000000..b8bb02c2f43 --- /dev/null +++ b/spec/frontend/mocks/mocks_helper_spec.js @@ -0,0 +1,149 @@ +/* eslint-disable global-require, promise/catch-or-return */ + +import path from 'path'; + +import axios from '~/lib/utils/axios_utils'; + +const absPath = path.join.bind(null, __dirname); + +jest.mock('fs'); +jest.mock('readdir-enhanced'); + +describe('mocks_helper.js', () => { + let setupManualMocks; + const setMock = jest.fn().mockName('setMock'); + let fs; + let readdir; + + beforeAll(() => { + jest.resetModules(); + jest.setMock = jest.fn().mockName('jest.setMock'); + fs = require('fs'); + readdir = require('readdir-enhanced'); + + // We need to provide setupManualMocks with a mock function that pretends to do the setup of + // the mock. This is because we can't mock jest.setMock across files. + setupManualMocks = () => require('./mocks_helper').setupManualMocks(setMock); + }); + + afterEach(() => { + fs.existsSync.mockReset(); + readdir.sync.mockReset(); + setMock.mockReset(); + }); + + it('enumerates through mock file roots', () => { + setupManualMocks(); + expect(fs.existsSync).toHaveBeenCalledTimes(2); + expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce')); + expect(fs.existsSync).toHaveBeenNthCalledWith(2, absPath('node')); + + expect(readdir.sync).toHaveBeenCalledTimes(0); + }); + + it("doesn't traverse the directory tree infinitely", () => { + fs.existsSync.mockReturnValue(true); + readdir.sync.mockReturnValue([]); + setupManualMocks(); + + const readdirSpy = readdir.sync; + expect(readdirSpy).toHaveBeenCalled(); + readdirSpy.mock.calls.forEach(call => { + expect(call[1].deep).toBeLessThan(100); + }); + }); + + it('sets up mocks for CE (the ~/ prefix)', () => { + fs.existsSync.mockImplementation(root => root.endsWith('ce')); + readdir.sync.mockReturnValue(['root.js', 'lib/utils/util.js']); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + + expect(setMock).toHaveBeenCalledTimes(2); + expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root'); + expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util'); + }); + + it('sets up mocks for node_modules', () => { + fs.existsSync.mockImplementation(root => root.endsWith('node')); + readdir.sync.mockReturnValue(['jquery', '@babel/core']); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('node')); + + expect(setMock).toHaveBeenCalledTimes(2); + expect(setMock).toHaveBeenNthCalledWith(1, 'jquery', './node/jquery'); + expect(setMock).toHaveBeenNthCalledWith(2, '@babel/core', './node/@babel/core'); + }); + + it('sets up mocks for all roots', () => { + const files = { + [absPath('ce')]: ['root', 'lib/utils/util'], + [absPath('node')]: ['jquery', '@babel/core'], + }; + + fs.existsSync.mockReturnValue(true); + readdir.sync.mockImplementation(root => files[root]); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(2); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + expect(readdir.sync.mock.calls[1][0]).toBe(absPath('node')); + + expect(setMock).toHaveBeenCalledTimes(4); + expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root'); + expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util'); + expect(setMock).toHaveBeenNthCalledWith(3, 'jquery', './node/jquery'); + expect(setMock).toHaveBeenNthCalledWith(4, '@babel/core', './node/@babel/core'); + }); + + it('fails when given a virtual mock', () => { + fs.existsSync.mockImplementation(p => p.endsWith('ce')); + readdir.sync.mockReturnValue(['virtual', 'shouldntBeImported']); + setMock.mockImplementation(() => { + throw new Error('Could not locate module'); + }); + + expect(setupManualMocks).toThrow( + new Error("A manual mock was defined for module ~/virtual, but the module doesn't exist!"), + ); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + }); + + describe('auto-injection', () => { + it('handles ambiguous paths', () => { + jest.isolateModules(() => { + const axios2 = require('../../../app/assets/javascripts/lib/utils/axios_utils').default; + expect(axios2.isMock).toBe(true); + }); + }); + + it('survives jest.isolateModules()', done => { + jest.isolateModules(() => { + const axios2 = require('~/lib/utils/axios_utils').default; + expect(axios2.get('http://gitlab.com')) + .rejects.toThrow('Unexpected unmocked request') + .then(done); + }); + }); + + it('can be unmocked and remocked', () => { + jest.dontMock('~/lib/utils/axios_utils'); + jest.resetModules(); + const axios2 = require('~/lib/utils/axios_utils').default; + expect(axios2).not.toBe(axios); + expect(axios2.isMock).toBeUndefined(); + + jest.doMock('~/lib/utils/axios_utils'); + jest.resetModules(); + const axios3 = require('~/lib/utils/axios_utils').default; + expect(axios3).not.toBe(axios2); + expect(axios3.isMock).toBe(true); + }); + }); +}); diff --git a/spec/frontend/mocks/node/jquery.js b/spec/frontend/mocks/node/jquery.js new file mode 100644 index 00000000000..34a25772f67 --- /dev/null +++ b/spec/frontend/mocks/node/jquery.js @@ -0,0 +1,13 @@ +/* eslint-disable import/no-commonjs */ + +const $ = jest.requireActual('jquery'); + +// Fail tests for unmocked requests +$.ajax = () => { + throw new Error( + 'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.', + ); +}; + +// jquery is not an ES6 module +module.exports = $; diff --git a/spec/frontend/mocks_spec.js b/spec/frontend/mocks_spec.js new file mode 100644 index 00000000000..2d2324120fd --- /dev/null +++ b/spec/frontend/mocks_spec.js @@ -0,0 +1,13 @@ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; + +describe('Mock auto-injection', () => { + describe('mocks', () => { + it('~/lib/utils/axios_utils', () => + expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request')); + + it('jQuery.ajax()', () => { + expect($.ajax).toThrow('Unexpected unmocked'); + }); + }); +}); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js new file mode 100644 index 00000000000..1ce14e2418a --- /dev/null +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -0,0 +1,78 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import Embed from '~/monitoring/components/embed.vue'; +import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import { groups, initialState, metricsData, metricsWithData } from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Embed', () => { + let wrapper; + let store; + let actions; + + function mountComponent() { + wrapper = shallowMount(Embed, { + localVue, + store, + propsData: { + dashboardUrl: TEST_HOST, + }, + }); + } + + beforeEach(() => { + actions = { + setFeatureFlags: () => {}, + setShowErrorBanner: () => {}, + setEndpoints: () => {}, + fetchMetricsData: () => {}, + }; + + store = new Vuex.Store({ + modules: { + monitoringDashboard: { + namespaced: true, + actions, + state: initialState, + }, + }, + }); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('no metrics are available yet', () => { + beforeEach(() => { + mountComponent(); + }); + + it('shows an empty state when no metrics are present', () => { + expect(wrapper.find('.metrics-embed').exists()).toBe(true); + expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(false); + }); + }); + + describe('metrics are available', () => { + beforeEach(() => { + store.state.monitoringDashboard.groups = groups; + store.state.monitoringDashboard.groups[0].metrics = metricsData; + store.state.monitoringDashboard.metricsWithData = metricsWithData; + + mountComponent(); + }); + + it('shows a chart when metrics are present', () => { + wrapper.setProps({}); + expect(wrapper.find('.metrics-embed').exists()).toBe(true); + expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true); + expect(wrapper.findAll(MonitorTimeSeriesChart).length).toBe(2); + }); + }); +}); diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js new file mode 100644 index 00000000000..df4acb82e95 --- /dev/null +++ b/spec/frontend/monitoring/embed/mock_data.js @@ -0,0 +1,87 @@ +export const metricsWithData = [15, 16]; + +export const groups = [ + { + panels: [ + { + title: 'Memory Usage (Total)', + type: 'area-chart', + y_label: 'Total Memory Used', + weight: 4, + metrics: [ + { + id: 'system_metrics_kubernetes_container_memory_total', + metric_id: 15, + }, + ], + }, + { + title: 'Core Usage (Total)', + type: 'area-chart', + y_label: 'Total Cores', + weight: 3, + metrics: [ + { + id: 'system_metrics_kubernetes_container_cores_total', + metric_id: 16, + }, + ], + }, + ], + }, +]; + +export const metrics = [ + { + id: 'system_metrics_kubernetes_container_memory_total', + metric_id: 15, + }, + { + id: 'system_metrics_kubernetes_container_cores_total', + metric_id: 16, + }, +]; + +const queries = [ + { + result: [ + { + values: [ + ['Mon', 1220], + ['Tue', 932], + ['Wed', 901], + ['Thu', 934], + ['Fri', 1290], + ['Sat', 1330], + ['Sun', 1320], + ], + }, + ], + }, +]; + +export const metricsData = [ + { + queries, + metrics: [ + { + metric_id: 15, + }, + ], + }, + { + queries, + metrics: [ + { + metric_id: 16, + }, + ], + }, +]; + +export const initialState = { + monitoringDashboard: {}, + groups: [], + metricsWithData: [], + useDashboardEndpoint: true, +}; diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js index 989b0458481..fd439ba46bd 100644 --- a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js +++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js @@ -23,8 +23,7 @@ describe('JumpToNextDiscussionButton', () => { button.trigger('click'); - expect(wrapper.emitted()).toEqual({ - onClick: [[]], - }); + expect(wrapper.emitted().onClick).toBeTruthy(); + expect(wrapper.emitted().onClick.length).toBe(1); }); }); diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js new file mode 100644 index 00000000000..8881bedf3cc --- /dev/null +++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js @@ -0,0 +1,104 @@ +/* global Mousetrap */ +import 'mousetrap'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import DiscussionKeyboardNavigator from '~/notes/components/discussion_keyboard_navigator.vue'; +import notesModule from '~/notes/stores/modules'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +const NEXT_ID = 'abc123'; +const PREV_ID = 'def456'; +const NEXT_DIFF_ID = 'abc123_diff'; +const PREV_DIFF_ID = 'def456_diff'; + +describe('notes/components/discussion_keyboard_navigator', () => { + let storeOptions; + let wrapper; + let store; + + const createComponent = (options = {}) => { + store = new Vuex.Store(storeOptions); + + wrapper = shallowMount(DiscussionKeyboardNavigator, { + localVue, + store, + ...options, + }); + + wrapper.vm.jumpToDiscussion = jest.fn(); + }; + + beforeEach(() => { + const notes = notesModule(); + + notes.getters.nextUnresolvedDiscussionId = () => (currId, isDiff) => + isDiff ? NEXT_DIFF_ID : NEXT_ID; + notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) => + isDiff ? PREV_DIFF_ID : PREV_ID; + + storeOptions = { + modules: { + notes, + }, + }; + }); + + afterEach(() => { + wrapper.destroy(); + storeOptions = null; + store = null; + }); + + describe.each` + isDiffView | expectedNextId | expectedPrevId + ${true} | ${NEXT_DIFF_ID} | ${PREV_DIFF_ID} + ${false} | ${NEXT_ID} | ${PREV_ID} + `('when isDiffView is $isDiffView', ({ isDiffView, expectedNextId, expectedPrevId }) => { + beforeEach(() => { + createComponent({ propsData: { isDiffView } }); + }); + + it('calls jumpToNextDiscussion when pressing `n`', () => { + Mousetrap.trigger('n'); + + expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedNextId); + expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId); + }); + + it('calls jumpToPreviousDiscussion when pressing `p`', () => { + Mousetrap.trigger('p'); + + expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedPrevId); + expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId); + }); + }); + + describe('on destroy', () => { + beforeEach(() => { + jest.spyOn(Mousetrap, 'unbind'); + + createComponent(); + + wrapper.destroy(); + }); + + it('unbinds keys', () => { + expect(Mousetrap.unbind).toHaveBeenCalledWith('n'); + expect(Mousetrap.unbind).toHaveBeenCalledWith('p'); + }); + + it('does not call jumpToNextDiscussion when pressing `n`', () => { + Mousetrap.trigger('n'); + + expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled(); + }); + + it('does not call jumpToNextDiscussion when pressing `p`', () => { + Mousetrap.trigger('p'); + + expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js index a881de8fbfe..39d7c19e731 100644 --- a/spec/frontend/operation_settings/components/external_dashboard_spec.js +++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js @@ -7,7 +7,6 @@ import { refreshCurrentPage } from '~/lib/utils/url_utility'; import createFlash from '~/flash'; import { TEST_HOST } from 'helpers/test_constants'; -jest.mock('~/lib/utils/axios_utils'); jest.mock('~/lib/utils/url_utility'); jest.mock('~/flash'); @@ -32,6 +31,10 @@ describe('operation settings external dashboard component', () => { wrapper = shallow ? shallowMount(...config) : mount(...config); }; + beforeEach(() => { + jest.spyOn(axios, 'patch').mockImplementation(); + }); + afterEach(() => { if (wrapper.destroy) { wrapper.destroy(); diff --git a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap new file mode 100644 index 00000000000..ce456d6c899 --- /dev/null +++ b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = ` +"<div class=\\"badge\\">22</div> +<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&project_id=3&scope=issues\\">4</div> +<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&project_id=3&scope=merge_requests\\">5</div>" +`; diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js new file mode 100644 index 00000000000..ead268b3971 --- /dev/null +++ b/spec/frontend/pages/search/show/refresh_counts_spec.js @@ -0,0 +1,35 @@ +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import refreshCounts from '~/pages/search/show/refresh_counts'; + +const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`; +const urlWithScope = scope => `${URL}&scope=${scope}`; +const counts = [{ scope: 'issues', count: 4 }, { scope: 'merge_requests', count: 5 }]; +const fixture = `<div class="badge">22</div> +<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div> +<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`; + +describe('pages/search/show/refresh_counts', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + setFixtures(fixture); + }); + + afterEach(() => { + mock.restore(); + }); + + it('fetches and displays search counts', () => { + counts.forEach(({ scope, count }) => { + mock.onGet(urlWithScope(scope)).reply(200, { count }); + }); + + // assert before act behavior + return refreshCounts().then(() => { + expect(document.body.innerHTML).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js b/spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js new file mode 100644 index 00000000000..7b8df03d3c3 --- /dev/null +++ b/spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js @@ -0,0 +1,61 @@ +import initGkeNamespace from '~/projects/gke_cluster_namespace'; + +describe('GKE cluster namespace', () => { + const changeEvent = new Event('change'); + const isHidden = el => el.classList.contains('hidden'); + const hasDisabledInput = el => el.querySelector('input').disabled; + + let glManagedCheckbox; + let selfManaged; + let glManaged; + + beforeEach(() => { + setFixtures(` + <input class="js-gl-managed" type="checkbox" value="1" checked /> + <div class="js-namespace"> + <input type="text" /> + </div> + <div class="js-namespace-prefixed"> + <input type="text" /> + </div> + `); + + glManagedCheckbox = document.querySelector('.js-gl-managed'); + selfManaged = document.querySelector('.js-namespace'); + glManaged = document.querySelector('.js-namespace-prefixed'); + + initGkeNamespace(); + }); + + describe('GKE cluster namespace toggles', () => { + it('initially displays the GitLab-managed label and input', () => { + expect(isHidden(glManaged)).toEqual(false); + expect(hasDisabledInput(glManaged)).toEqual(false); + + expect(isHidden(selfManaged)).toEqual(true); + expect(hasDisabledInput(selfManaged)).toEqual(true); + }); + + it('displays the self-managed label and input when the Gitlab-managed checkbox is unchecked', () => { + glManagedCheckbox.checked = false; + glManagedCheckbox.dispatchEvent(changeEvent); + + expect(isHidden(glManaged)).toEqual(true); + expect(hasDisabledInput(glManaged)).toEqual(true); + + expect(isHidden(selfManaged)).toEqual(false); + expect(hasDisabledInput(selfManaged)).toEqual(false); + }); + + it('displays the GitLab-managed label and input when the Gitlab-managed checkbox is checked', () => { + glManagedCheckbox.checked = true; + glManagedCheckbox.dispatchEvent(changeEvent); + + expect(isHidden(glManaged)).toEqual(false); + expect(hasDisabledInput(glManaged)).toEqual(false); + + expect(isHidden(selfManaged)).toEqual(true); + expect(hasDisabledInput(selfManaged)).toEqual(true); + }); + }); +}); diff --git a/spec/frontend/projects/projects_filterable_list_spec.js b/spec/frontend/projects/projects_filterable_list_spec.js new file mode 100644 index 00000000000..e756fb3ab56 --- /dev/null +++ b/spec/frontend/projects/projects_filterable_list_spec.js @@ -0,0 +1,31 @@ +import ProjectsFilterableList from '~/projects/projects_filterable_list'; +import { getJSONFixture, setHTMLFixture } from '../helpers/fixtures'; + +describe('ProjectsFilterableList', () => { + let List; + let form; + let filter; + let holder; + + beforeEach(() => { + setHTMLFixture(` + <form id="project-filter-form"> + <input name="name" class="js-projects-list-filter" /> + </div> + <div class="js-projects-list-holder"></div> + `); + getJSONFixture('static/projects.json'); + form = document.querySelector('form#project-filter-form'); + filter = document.querySelector('.js-projects-list-filter'); + holder = document.querySelector('.js-projects-list-holder'); + List = new ProjectsFilterableList(form, filter, holder); + }); + + describe('getFilterEndpoint', () => { + it('updates converts getPagePath for projects', () => { + jest.spyOn(List, 'getPagePath').mockReturnValue('blah/projects?'); + + expect(List.getFilterEndpoint()).toEqual('blah/projects.json?'); + }); + }); +}); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 068fa317a87..707eae34793 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -1,12 +1,14 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils'; +import { GlDropdown } from '@gitlab/ui'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; let vm; -function factory(currentPath) { +function factory(currentPath, extraProps = {}) { vm = shallowMount(Breadcrumbs, { propsData: { currentPath, + ...extraProps, }, stubs: { RouterLink: RouterLinkStub, @@ -41,4 +43,20 @@ describe('Repository breadcrumbs component', () => { .attributes('aria-current'), ).toEqual('page'); }); + + it('does not render add to tree dropdown when permissions are false', () => { + factory('/', { canCollaborate: false }); + + vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } }); + + expect(vm.find(GlDropdown).exists()).toBe(false); + }); + + it('renders add to tree dropdown when permissions are true', () => { + factory('/', { canCollaborate: true }); + + vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } }); + + expect(vm.find(GlDropdown).exists()).toBe(true); + }); }); diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index c566057ad3f..e539c560975 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,5 +1,5 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils'; -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlLink } from '@gitlab/ui'; import { visitUrl } from '~/lib/utils/url_utility'; import TableRow from '~/repository/components/table/row.vue'; @@ -142,4 +142,18 @@ describe('Repository table row component', () => { expect(vm.find(GlBadge).exists()).toBe(true); }); + + it('renders commit and web links with href for submodule', () => { + factory({ + id: '1', + path: 'test', + type: 'commit', + url: 'https://test.com', + submoduleTreeUrl: 'https://test.com/commit', + currentPath: '/', + }); + + expect(vm.find('a').attributes('href')).toEqual('https://test.com'); + expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit'); + }); }); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js new file mode 100644 index 00000000000..452d4cd07cc --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -0,0 +1,85 @@ +import { shallowMount } from '@vue/test-utils'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { TEST_HOST } from 'helpers/test_constants'; +import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import userDataMock from '../../user_data_mock'; + +const TOOLTIP_PLACEMENT = 'bottom'; +const { name: USER_NAME, username: USER_USERNAME } = userDataMock(); +const TEST_ISSUABLE_TYPE = 'merge_request'; + +describe('AssigneeAvatarLink component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + showLess: true, + rootPath: TEST_HOST, + tooltipPlacement: TOOLTIP_PLACEMENT, + singleUser: false, + issuableType: TEST_ISSUABLE_TYPE, + ...props, + }; + + wrapper = shallowMount(AssigneeAvatarLink, { + propsData, + sync: false, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findTooltipText = () => wrapper.attributes('data-original-title'); + + it('has the root url present in the assigneeUrl method', () => { + createComponent(); + const assigneeUrl = joinPaths(TEST_HOST, USER_USERNAME); + + expect(wrapper.attributes().href).toEqual(assigneeUrl); + }); + + it('renders assignee avatar', () => { + createComponent(); + + expect(wrapper.find(AssigneeAvatar).props()).toEqual( + expect.objectContaining({ + issuableType: TEST_ISSUABLE_TYPE, + user: userDataMock(), + }), + ); + }); + + describe.each` + issuableType | tooltipHasName | canMerge | expected + ${'merge_request'} | ${true} | ${true} | ${USER_NAME} + ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`} + ${'merge_request'} | ${false} | ${true} | ${''} + ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'} + ${'issue'} | ${true} | ${true} | ${USER_NAME} + ${'issue'} | ${true} | ${false} | ${USER_NAME} + ${'issue'} | ${false} | ${true} | ${''} + ${'issue'} | ${false} | ${false} | ${''} + `( + 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge', + ({ issuableType, tooltipHasName, canMerge, expected }) => { + beforeEach(() => { + createComponent({ + issuableType, + tooltipHasName, + user: { + ...userDataMock(), + can_merge: canMerge, + }, + }); + }); + + it('sets tooltip', () => { + expect(findTooltipText()).toBe(expected); + }); + }, + ); +}); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js new file mode 100644 index 00000000000..d60ae17733b --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js @@ -0,0 +1,78 @@ +import { shallowMount } from '@vue/test-utils'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import userDataMock from '../../user_data_mock'; + +const TEST_AVATAR = `${TEST_HOST}/avatar.png`; +const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`; + +describe('AssigneeAvatar', () => { + let origGon; + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + imgSize: 24, + issuableType: 'merge_request', + ...props, + }; + + wrapper = shallowMount(AssigneeAvatar, { + propsData, + sync: false, + }); + } + + beforeEach(() => { + origGon = window.gon; + window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL }; + }); + + afterEach(() => { + window.gon = origGon; + wrapper.destroy(); + }); + + const findImg = () => wrapper.find('img'); + + it('does not show warning icon if assignee can merge', () => { + createComponent(); + + expect(wrapper.find('.merge-icon').exists()).toBe(false); + }); + + it('shows warning icon if assignee cannot merge', () => { + createComponent({ + user: { + can_merge: false, + }, + }); + + expect(wrapper.find('.merge-icon').exists()).toBe(true); + }); + + it('does not show warning icon for issuableType = "issue"', () => { + createComponent({ + issuableType: 'issue', + }); + + expect(wrapper.find('.merge-icon').exists()).toBe(false); + }); + + it.each` + avatar | avatar_url | expected | desc + ${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'} + ${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'} + ${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'} + `('$desc', ({ avatar, avatar_url, expected }) => { + createComponent({ + user: { + avatar, + avatar_url, + }, + }); + + expect(findImg().attributes('src')).toEqual(expected); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js new file mode 100644 index 00000000000..ff0c8d181b5 --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js @@ -0,0 +1,189 @@ +import { shallowMount } from '@vue/test-utils'; +import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue'; +import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue'; +import UsersMockHelper from 'helpers/user_mock_data_helper'; + +const DEFAULT_MAX_COUNTER = 99; + +describe('CollapsedAssigneeList component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + users: [], + issuableType: 'merge_request', + ...props, + }; + + wrapper = shallowMount(CollapsedAssigneeList, { + propsData, + sync: false, + }); + } + + const findNoUsersIcon = () => wrapper.find('i[aria-label=None]'); + const findAvatarCounter = () => wrapper.find('.avatar-counter'); + const findAssignees = () => wrapper.findAll(CollapsedAssignee); + const getTooltipTitle = () => wrapper.attributes('data-original-title'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No assignees/users', () => { + beforeEach(() => { + createComponent({ + users: [], + }); + }); + + it('has no users', () => { + expect(findNoUsersIcon().exists()).toBe(true); + }); + }); + + describe('One assignee/user', () => { + let users; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(1); + }); + + it('should not show no users icon', () => { + createComponent({ users }); + + expect(findNoUsersIcon().exists()).toBe(false); + }); + + it('has correct "cannot merge" tooltip when user cannot merge', () => { + users[0].can_merge = false; + + createComponent({ users }); + + expect(getTooltipTitle()).toContain('cannot merge'); + }); + + it('does not have "merge" word in tooltip if user can merge', () => { + users[0].can_merge = true; + + createComponent({ users }); + + expect(getTooltipTitle()).not.toContain('merge'); + }); + }); + + describe('More than one assignees/users', () => { + let users; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(2); + + createComponent({ users }); + }); + + it('has multiple-users class', () => { + expect(wrapper.classes('multiple-users')).toBe(true); + }); + + it('does not display an avatar count', () => { + expect(findAvatarCounter().exists()).toBe(false); + }); + + it('returns just two collapsed users', () => { + expect(findAssignees().length).toBe(2); + }); + }); + + describe('More than two assignees/users', () => { + let users; + let userNames; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(3); + userNames = users.map(x => x.name).join(', '); + }); + + describe('default', () => { + beforeEach(() => { + createComponent({ users }); + }); + + it('does display an avatar count', () => { + expect(findAvatarCounter().exists()).toBe(true); + expect(findAvatarCounter().text()).toEqual('+2'); + }); + + it('returns one collapsed users', () => { + expect(findAssignees().length).toBe(1); + }); + }); + + it('has corrent "no one can merge" tooltip when no one can merge', () => { + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = false; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (no one can merge)`); + }); + + it('has correct "cannot merge" tooltip when one user can merge', () => { + users[0].can_merge = true; + users[1].can_merge = false; + users[2].can_merge = false; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (1/3 can merge)`); + }); + + it('has correct "cannot merge" tooltip when more than one user can merge', () => { + users[0].can_merge = false; + users[1].can_merge = true; + users[2].can_merge = true; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (2/3 can merge)`); + }); + + it('does not have "merge" in tooltip if everyone can merge', () => { + users[0].can_merge = true; + users[1].can_merge = true; + users[2].can_merge = true; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(userNames); + }); + + it('displays the correct avatar count', () => { + users = UsersMockHelper.createNumberRandomUsers(5); + + createComponent({ + users, + }); + + expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`); + }); + + it('displays the correct avatar count via a computed property if more than default max counter', () => { + users = UsersMockHelper.createNumberRandomUsers(100); + + createComponent({ + users, + }); + + expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js new file mode 100644 index 00000000000..f9ca7bc1ecb --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js @@ -0,0 +1,49 @@ +import { shallowMount } from '@vue/test-utils'; +import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import userDataMock from '../../user_data_mock'; + +const TEST_USER = userDataMock(); +const TEST_ISSUABLE_TYPE = 'merge_request'; + +describe('CollapsedAssignee assignee component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + issuableType: TEST_ISSUABLE_TYPE, + ...props, + }; + + wrapper = shallowMount(CollapsedAssignee, { + propsData, + sync: false, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('has author name', () => { + createComponent(); + + expect( + wrapper + .find('.author') + .text() + .trim(), + ).toEqual(TEST_USER.name); + }); + + it('has assignee avatar', () => { + createComponent(); + + expect(wrapper.find(AssigneeAvatar).props()).toEqual({ + imgSize: 24, + user: TEST_USER, + issuableType: TEST_ISSUABLE_TYPE, + }); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js new file mode 100644 index 00000000000..6398351834c --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js @@ -0,0 +1,103 @@ +import { mount } from '@vue/test-utils'; +import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue'; +import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import userDataMock from '../../user_data_mock'; +import UsersMockHelper from '../../../helpers/user_mock_data_helper'; + +const DEFAULT_RENDER_COUNT = 5; + +describe('UncollapsedAssigneeList component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + users: [], + rootPath: TEST_HOST, + ...props, + }; + + wrapper = mount(UncollapsedAssigneeList, { + sync: false, + propsData, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findMoreButton = () => wrapper.find('.user-list-more button'); + + describe('One assignee/user', () => { + let user; + + beforeEach(() => { + user = userDataMock(); + + createComponent({ + users: [user], + }); + }); + + it('only has one user', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1); + }); + + it('calls the AssigneeAvatarLink with the proper props', () => { + expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true); + expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left'); + }); + + it('Shows one user with avatar, username and author name', () => { + expect(wrapper.text()).toContain(user.name); + expect(wrapper.text()).toContain(`@${user.username}`); + }); + }); + + describe('n+ more label', () => { + describe('when users count is rendered users', () => { + beforeEach(() => { + createComponent({ + users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT), + }); + }); + + it('does not show more label', () => { + expect(findMoreButton().exists()).toBe(false); + }); + }); + + describe('when more than rendered users', () => { + beforeEach(() => { + createComponent({ + users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1), + }); + }); + + it('shows "+1 more" label', () => { + expect(findMoreButton().text()).toBe('+ 1 more'); + }); + + it('shows truncated users', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT); + }); + + describe('when more button is clicked', () => { + beforeEach(() => { + findMoreButton().trigger('click'); + + return wrapper.vm.$nextTick(); + }); + + it('shows "show less" label', () => { + expect(findMoreButton().text()).toBe('- show less'); + }); + + it('shows all users', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1); + }); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js new file mode 100644 index 00000000000..8ad70bb3499 --- /dev/null +++ b/spec/frontend/sidebar/user_data_mock.js @@ -0,0 +1,9 @@ +export default () => ({ + avatar_url: 'mock_path', + id: 1, + name: 'Root', + state: 'active', + username: 'root', + web_url: '', + can_merge: true, +}); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 15cf18700ed..d52aeb1fe6b 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -2,10 +2,11 @@ import Vue from 'vue'; import * as jqueryMatchers from 'custom-jquery-matchers'; import $ from 'jquery'; import Translate from '~/vue_shared/translate'; -import axios from '~/lib/utils/axios_utils'; import { config as testUtilsConfig } from '@vue/test-utils'; import { initializeTestTimeout } from './helpers/timeout'; import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; +import { setupManualMocks } from './mocks/mocks_helper'; +import customMatchers from './matchers'; // Expose jQuery so specs using jQuery plugins can be imported nicely. // Here is an issue to explore better alternatives: @@ -14,6 +15,8 @@ window.jQuery = $; process.on('unhandledRejection', global.promiseRejectionHandler); +setupManualMocks(); + afterEach(() => // give Promises a bit more time so they fail the right test new Promise(setImmediate).then(() => { @@ -24,18 +27,6 @@ afterEach(() => initializeTestTimeout(process.env.CI ? 5000 : 500); -// fail tests for unmocked requests -beforeEach(done => { - axios.defaults.adapter = config => { - const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`); - error.config = config; - done.fail(error); - return Promise.reject(error); - }; - - done(); -}); - Vue.config.devtools = false; Vue.config.productionTip = false; @@ -77,5 +68,34 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => { }); }); +expect.extend(customMatchers); + // Tech debt issue TBD testUtilsConfig.logModifiedComponents = false; + +// Basic stub for MutationObserver +global.MutationObserver = () => ({ + disconnect: () => {}, + observe: () => {}, +}); + +Object.assign(global, { + requestIdleCallback(cb) { + const start = Date.now(); + return setTimeout(() => { + cb({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), + }); + }); + }, + cancelIdleCallback(id) { + clearTimeout(id); + }, +}); + +// make sure that each test actually tests something +// see https://jestjs.io/docs/en/expect#expecthasassertions +beforeEach(() => { + expect.hasAssertions(); +}); diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js new file mode 100644 index 00000000000..0e862c683d3 --- /dev/null +++ b/spec/frontend/tracking_spec.js @@ -0,0 +1,143 @@ +import $ from 'jquery'; +import { setHTMLFixture } from './helpers/fixtures'; + +import Tracking from '~/tracking'; + +describe('Tracking', () => { + beforeEach(() => { + window.snowplow = window.snowplow || (() => {}); + }); + + describe('.event', () => { + let snowplowSpy = null; + + beforeEach(() => { + snowplowSpy = jest.spyOn(window, 'snowplow'); + }); + + afterEach(() => { + window.doNotTrack = undefined; + navigator.doNotTrack = undefined; + navigator.msDoNotTrack = undefined; + }); + + it('tracks to snowplow (our current tracking system)', () => { + Tracking.event('_category_', '_eventName_', { label: '_label_' }); + + expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', { + label: '_label_', + property: '', + value: '', + }); + }); + + it('skips tracking if snowplow is unavailable', () => { + window.snowplow = false; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + + it('skips tracking if the user does not want to be tracked (general spec)', () => { + window.doNotTrack = '1'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + + it('skips tracking if the user does not want to be tracked (firefox legacy)', () => { + navigator.doNotTrack = 'yes'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + + it('skips tracking if the user does not want to be tracked (IE legacy)', () => { + navigator.msDoNotTrack = '1'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + }); + + describe('tracking interface events', () => { + let eventSpy = null; + let subject = null; + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + subject = new Tracking('_category_'); + setHTMLFixture(` + <input data-track-event="click_input1" data-track-label="_label_" value="_value_"/> + <input data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/> + <input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/> + <input class="dropdown" data-track-event="toggle_dropdown"/> + <div class="js-projects-list-holder"></div> + `); + }); + + it('binds to clicks on elements matching [data-track-event]', () => { + subject.bind(document); + $('[data-track-event="click_input1"]').click(); + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', { + label: '_label_', + value: '_value_', + property: '', + }); + }); + + it('allows value override with the data-track-value attribute', () => { + subject.bind(document); + $('[data-track-event="click_input2"]').click(); + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', { + label: '', + value: '_value_override_', + property: '', + }); + }); + + it('handles checkbox values correctly', () => { + subject.bind(document); + const $checkbox = $('[data-track-event="toggle_checkbox"]'); + + $checkbox.click(); // unchecking + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { + label: '', + property: '', + value: false, + }); + + $checkbox.click(); // checking + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { + label: '', + property: '', + value: '_value_', + }); + }); + + it('handles bootstrap dropdowns', () => { + new Tracking('_category_').bind(document); + const $dropdown = $('[data-track-event="toggle_dropdown"]'); + + $dropdown.trigger('show.bs.dropdown'); // showing + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', { + label: '', + property: '', + value: '', + }); + + $dropdown.trigger('hide.bs.dropdown'); // hiding + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', { + label: '', + property: '', + value: '', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js new file mode 100644 index 00000000000..806602877ef --- /dev/null +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -0,0 +1,123 @@ +import { shallowMount } from '@vue/test-utils'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const changedFile = () => ({ changed: true }); +const stagedFile = () => ({ changed: false, staged: true }); +const changedAndStagedFile = () => ({ changed: true, staged: true }); +const newFile = () => ({ changed: true, tempFile: true }); +const unchangedFile = () => ({ changed: false, tempFile: false, staged: false, deleted: false }); + +describe('Changed file icon', () => { + let wrapper; + + const factory = (props = {}) => { + wrapper = shallowMount(ChangedFileIcon, { + propsData: { + file: changedFile(), + showTooltip: true, + ...props, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findIcon = () => wrapper.find(Icon); + const findIconName = () => findIcon().props('name'); + const findIconClasses = () => + findIcon() + .props('cssClasses') + .split(' '); + const findTooltipText = () => wrapper.attributes('data-original-title'); + + it('with isCentered true, adds center class', () => { + factory({ + isCentered: true, + }); + + expect(wrapper.classes('ml-auto')).toBe(true); + }); + + it('with isCentered false, does not center', () => { + factory({ + isCentered: false, + }); + + expect(wrapper.classes('ml-auto')).toBe(false); + }); + + it('with showTooltip false, does not show tooltip', () => { + factory({ + showTooltip: false, + }); + + expect(findTooltipText()).toBeFalsy(); + }); + + describe.each` + file | iconName | tooltipText | desc + ${changedFile()} | ${'file-modified'} | ${'Unstaged modification'} | ${'with file changed'} + ${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'} + ${changedAndStagedFile()} | ${'file-modified'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'} + ${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'} + `('$desc', ({ file, iconName, tooltipText }) => { + beforeEach(() => { + factory({ file }); + }); + + it('renders icon', () => { + expect(findIconName()).toBe(iconName); + expect(findIconClasses()).toContain(iconName); + }); + + it('renders tooltip text', () => { + expect(findTooltipText()).toBe(tooltipText); + }); + }); + + describe('with file unchanged', () => { + beforeEach(() => { + factory({ + file: unchangedFile(), + }); + }); + + it('does not show icon', () => { + expect(findIcon().exists()).toBe(false); + }); + + it('does not have tooltip text', () => { + expect(findTooltipText()).toBe(''); + }); + }); + + it('with size set, sets icon size', () => { + const size = 8; + + factory({ + file: changedFile(), + size, + }); + + expect(findIcon().props('size')).toBe(size); + }); + + // NOTE: It looks like 'showStagedIcon' behavior is backwards to what the name suggests + // https://gitlab.com/gitlab-org/gitlab-ce/issues/66071 + it.each` + showStagedIcon | iconName | desc + ${false} | ${'file-modified-solid'} | ${'with showStagedIcon false, renders staged icon'} + ${true} | ${'file-modified'} | ${'with showStagedIcon true, renders regular icon'} + `('$desc', ({ showStagedIcon, iconName }) => { + factory({ + file: stagedFile(), + showStagedIcon, + }); + + expect(findIconName()).toEqual(iconName); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index aa0b544f948..48f2ee86619 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -101,7 +101,7 @@ describe('Markdown field header component', () => { vm.canSuggest = false; Vue.nextTick(() => { - expect(vm.$el.querySelector('.qa-suggestion-btn')).toBe(null); + expect(vm.$el.querySelector('.js-suggestion-btn')).toBe(null); }); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index 3b6f67457ad..6716e5cd794 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -28,8 +28,8 @@ describe('Suggestion Diff component', () => { wrapper.destroy(); }); - const findApplyButton = () => wrapper.find('.qa-apply-btn'); - const findHeader = () => wrapper.find('.qa-suggestion-diff-header'); + const findApplyButton = () => wrapper.find('.js-apply-btn'); + const findHeader = () => wrapper.find('.js-suggestion-diff-header'); const findHelpButton = () => wrapper.find('.js-help-btn'); const findLoading = () => wrapper.find(GlLoadingIcon); @@ -73,7 +73,10 @@ describe('Suggestion Diff component', () => { }); it('emits apply', () => { - expect(wrapper.emittedByOrder()).toEqual([{ name: 'apply', args: [expect.any(Function)] }]); + expect(wrapper.emittedByOrder()).toContainEqual({ + name: 'apply', + args: [expect.any(Function)], + }); }); it('hides apply button', () => { diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js new file mode 100644 index 00000000000..b2475488d97 --- /dev/null +++ b/spec/frontend/wikis_spec.js @@ -0,0 +1,74 @@ +import Wikis from '~/pages/projects/wikis/wikis'; +import { setHTMLFixture } from './helpers/fixtures'; + +describe('Wikis', () => { + describe('setting the commit message when the title changes', () => { + const editFormHtmlFixture = args => `<form class="wiki-form ${ + args.newPage ? 'js-new-wiki-page' : '' + }"> + <input type="text" id="wiki_title" value="My title" /> + <input type="text" id="wiki_message" /> + </form>`; + + let wikis; + let titleInput; + let messageInput; + + describe('when the wiki page is being created', () => { + const formHtmlFixture = editFormHtmlFixture({ newPage: true }); + + beforeEach(() => { + setHTMLFixture(formHtmlFixture); + + titleInput = document.getElementById('wiki_title'); + messageInput = document.getElementById('wiki_message'); + wikis = new Wikis(); + }); + + it('binds an event listener to the title input', () => { + wikis.handleWikiTitleChange = jest.fn(); + + titleInput.dispatchEvent(new Event('keyup')); + + expect(wikis.handleWikiTitleChange).toHaveBeenCalled(); + }); + + it('sets the commit message when title changes', () => { + titleInput.value = 'My title'; + messageInput.value = ''; + + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Create My title'); + }); + + it('replaces hyphens with spaces', () => { + titleInput.value = 'my-hyphenated-title'; + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Create my hyphenated title'); + }); + }); + + describe('when the wiki page is being updated', () => { + const formHtmlFixture = editFormHtmlFixture({ newPage: false }); + + beforeEach(() => { + setHTMLFixture(formHtmlFixture); + + titleInput = document.getElementById('wiki_title'); + messageInput = document.getElementById('wiki_message'); + wikis = new Wikis(); + }); + + it('sets the commit message when title changes, prefixing with "Update"', () => { + titleInput.value = 'My title'; + messageInput.value = ''; + + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Update My title'); + }); + }); + }); +}); |