diff options
Diffstat (limited to 'spec/frontend')
15 files changed, 509 insertions, 44 deletions
diff --git a/spec/frontend/fixtures/static/projects.json b/spec/frontend/fixtures/static/projects.json index d92d3acdea0..f28d9899099 100644 --- a/spec/frontend/fixtures/static/projects.json +++ b/spec/frontend/fixtures/static/projects.json @@ -99,6 +99,15 @@ "access_level": 50, "notification_level": 3 } + }, + "_links": { + "self": "https://gitlab.com/api/v4/projects/278964", + "issues": "https://gitlab.com/api/v4/projects/278964/issues", + "merge_requests": "https://gitlab.com/api/v4/projects/278964/merge_requests", + "repo_branches": "https://gitlab.com/api/v4/projects/278964/repository/branches", + "labels": "https://gitlab.com/api/v4/projects/278964/labels", + "events": "https://gitlab.com/api/v4/projects/278964/events", + "members": "https://gitlab.com/api/v4/projects/278964/members" } }, { "id": 7, diff --git a/spec/frontend/helpers/dom_shims/index.js b/spec/frontend/helpers/dom_shims/index.js index 40256398e6d..1fc5130cefc 100644 --- a/spec/frontend/helpers/dom_shims/index.js +++ b/spec/frontend/helpers/dom_shims/index.js @@ -1 +1,2 @@ import './get_client_rects'; +import './inner_text'; diff --git a/spec/frontend/helpers/dom_shims/inner_text.js b/spec/frontend/helpers/dom_shims/inner_text.js new file mode 100644 index 00000000000..2b8201eed31 --- /dev/null +++ b/spec/frontend/helpers/dom_shims/inner_text.js @@ -0,0 +1,11 @@ +// workaround for JSDOM not supporting innerText +// see https://github.com/jsdom/jsdom/issues/1245 +Object.defineProperty(global.Element.prototype, 'innerText', { + get() { + return this.textContent; + }, + set(value) { + this.textContext = value; + }, + configurable: true, // make it so that it doesn't blow chunks on re-running tests with things like --watch +}); diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js index d196f6f79d5..21c5e886738 100644 --- a/spec/frontend/ide/stores/getters_spec.js +++ b/spec/frontend/ide/stores/getters_spec.js @@ -1,12 +1,14 @@ import * as getters from '~/ide/stores/getters'; -import state from '~/ide/stores/state'; +import { createStore } from '~/ide/stores'; import { file } from '../helpers'; describe('IDE store getters', () => { let localState; + let localStore; beforeEach(() => { - localState = state(); + localStore = createStore(); + localState = localStore.state; }); describe('activeFile', () => { @@ -310,4 +312,90 @@ describe('IDE store getters', () => { expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); }); }); + + describe('isFileDeletedAndReadded', () => { + const f = { ...file('sample'), content: 'sample', raw: 'sample' }; + + it.each([ + { + entry: { ...f, tempFile: true }, + staged: { ...f, deleted: true }, + output: true, + }, + { + entry: { ...f, content: 'changed' }, + staged: { ...f, content: 'changed' }, + output: false, + }, + { + entry: { ...f, content: 'changed' }, + output: false, + }, + ])( + 'checks staged and unstaged files to see if a file was deleted and readded (case %#)', + ({ entry, staged, output }) => { + Object.assign(localState, { + entries: { + [entry.path]: entry, + }, + stagedFiles: [], + }); + + if (staged) localState.stagedFiles.push(staged); + + expect(localStore.getters.isFileDeletedAndReadded(entry.path)).toBe(output); + }, + ); + }); + + describe('getDiffInfo', () => { + const f = { ...file('sample'), content: 'sample', raw: 'sample' }; + it.each([ + { + entry: { ...f, tempFile: true }, + staged: { ...f, deleted: true }, + output: { deleted: false, changed: false, tempFile: false }, + }, + { + entry: { ...f, tempFile: true, content: 'changed', raw: '' }, + staged: { ...f, deleted: true }, + output: { deleted: false, changed: true, tempFile: false }, + }, + { + entry: { ...f, content: 'changed' }, + output: { changed: true }, + }, + { + entry: { ...f, content: 'sample' }, + staged: { ...f, content: 'changed' }, + output: { changed: false }, + }, + { + entry: { ...f, deleted: true }, + output: { deleted: true, changed: false }, + }, + { + entry: { ...f, prevPath: 'old_path' }, + output: { renamed: true, changed: false }, + }, + { + entry: { ...f, prevPath: 'old_path', content: 'changed' }, + output: { renamed: true, changed: true }, + }, + ])( + 'compares changes in a file entry and returns a resulting diff info (case %#)', + ({ entry, staged, output }) => { + Object.assign(localState, { + entries: { + [entry.path]: entry, + }, + stagedFiles: [], + }); + + if (staged) localState.stagedFiles.push(staged); + + expect(localStore.getters.getDiffInfo(entry.path)).toEqual(expect.objectContaining(output)); + }, + ); + }); }); diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js new file mode 100644 index 00000000000..b4539801e0f --- /dev/null +++ b/spec/frontend/monitoring/components/charts/column_spec.js @@ -0,0 +1,66 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import ColumnChart from '~/monitoring/components/charts/column.vue'; + +const localVue = createLocalVue(); + +jest.mock('~/lib/utils/icon_utils', () => ({ + getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'), +})); + +describe('Column component', () => { + let columnChart; + + beforeEach(() => { + columnChart = shallowMount(localVue.extend(ColumnChart), { + propsData: { + graphData: { + metrics: [ + { + x_label: 'Time', + y_label: 'Usage', + result: [ + { + metric: {}, + values: [ + [1495700554.925, '8.0390625'], + [1495700614.925, '8.0390625'], + [1495700674.925, '8.0390625'], + ], + }, + ], + }, + ], + }, + containerWidth: 100, + }, + sync: false, + localVue, + }); + }); + + afterEach(() => { + columnChart.destroy(); + }); + + describe('wrapped components', () => { + describe('GitLab UI column chart', () => { + let glColumnChart; + + beforeEach(() => { + glColumnChart = columnChart.find(GlColumnChart); + }); + + it('is a Vue instance', () => { + expect(glColumnChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glColumnChart.props(); + + expect(props.data).toBe(columnChart.vm.chartData); + expect(props.option).toBe(columnChart.vm.chartOptions); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/empty_chart_spec.js b/spec/frontend/monitoring/components/charts/empty_chart_spec.js new file mode 100644 index 00000000000..06822126b59 --- /dev/null +++ b/spec/frontend/monitoring/components/charts/empty_chart_spec.js @@ -0,0 +1,33 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; + +const localVue = createLocalVue(); + +describe('Empty Chart component', () => { + let emptyChart; + const graphTitle = 'Memory Usage'; + + beforeEach(() => { + emptyChart = shallowMount(localVue.extend(EmptyChart), { + propsData: { + graphTitle, + }, + sync: false, + localVue, + }); + }); + + afterEach(() => { + emptyChart.destroy(); + }); + + it('render the chart title', () => { + expect(emptyChart.find({ ref: 'graphTitle' }).text()).toBe(graphTitle); + }); + + describe('Computed props', () => { + it('sets the height for the svg container', () => { + expect(emptyChart.vm.svgContainerStyle.height).toBe('300px'); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js new file mode 100644 index 00000000000..5e2c1932e9e --- /dev/null +++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js @@ -0,0 +1,69 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlHeatmap } from '@gitlab/ui/dist/charts'; +import Heatmap from '~/monitoring/components/charts/heatmap.vue'; +import { graphDataPrometheusQueryRangeMultiTrack } from '../../mock_data'; + +describe('Heatmap component', () => { + let heatmapChart; + let store; + + beforeEach(() => { + heatmapChart = shallowMount(Heatmap, { + propsData: { + graphData: graphDataPrometheusQueryRangeMultiTrack, + containerWidth: 100, + }, + store, + }); + }); + + afterEach(() => { + heatmapChart.destroy(); + }); + + describe('wrapped components', () => { + describe('GitLab UI heatmap chart', () => { + let glHeatmapChart; + + beforeEach(() => { + glHeatmapChart = heatmapChart.find(GlHeatmap); + }); + + it('is a Vue instance', () => { + expect(glHeatmapChart.isVueInstance()).toBe(true); + }); + + it('should display a label on the x axis', () => { + expect(heatmapChart.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label); + }); + + it('should display a label on the y axis', () => { + expect(heatmapChart.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label); + }); + + // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data + // each row of the heatmap chart is represented by an array inside another parent array + // e.g. [[0, 0, 10]], the format represents the column, the row and finally the value + // corresponding to the cell + + it('should return chartData with a length of x by y, with a length of 3 per array', () => { + const row = heatmapChart.vm.chartData[0]; + + expect(row.length).toBe(3); + expect(heatmapChart.vm.chartData.length).toBe(30); + }); + + it('returns a series of labels for the x axis', () => { + const { xAxisLabels } = heatmapChart.vm; + + expect(xAxisLabels.length).toBe(5); + }); + + it('returns a series of labels for the y axis', () => { + const { yAxisLabels } = heatmapChart.vm; + + expect(yAxisLabels.length).toBe(6); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js new file mode 100644 index 00000000000..78bcc400787 --- /dev/null +++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js @@ -0,0 +1,31 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import SingleStatChart from '~/monitoring/components/charts/single_stat.vue'; +import { graphDataPrometheusQuery } from '../../mock_data'; + +const localVue = createLocalVue(); + +describe('Single Stat Chart component', () => { + let singleStatChart; + + beforeEach(() => { + singleStatChart = shallowMount(localVue.extend(SingleStatChart), { + propsData: { + graphData: graphDataPrometheusQuery, + }, + sync: false, + localVue, + }); + }); + + afterEach(() => { + singleStatChart.destroy(); + }); + + describe('computed', () => { + describe('engineeringNotation', () => { + it('should interpolate the value and unit props', () => { + expect(singleStatChart.vm.engineeringNotation).toBe('91MB'); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 128c4bc49f1..098b3408e67 100644 --- a/spec/frontend/monitoring/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -12,20 +12,14 @@ import { mockedQueryResultPayload, mockProjectDir, mockHost, -} from '../mock_data'; - +} from '../../mock_data'; import * as iconUtils from '~/lib/utils/icon_utils'; -const mockSvgPathContent = 'mockSvgPathContent'; const mockWidgets = 'mockWidgets'; +const mockSvgPathContent = 'mockSvgPathContent'; jest.mock('~/lib/utils/icon_utils', () => ({ - getSvgIconPathContent: jest.fn().mockImplementation( - () => - new Promise(resolve => { - resolve(mockSvgPathContent); - }), - ), + getSvgIconPathContent: jest.fn().mockImplementation(() => Promise.resolve(mockSvgPathContent)), })); describe('Time series component', () => { diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js index 7020055271f..63f84b2597b 100644 --- a/spec/frontend/repository/components/table/parent_row_spec.js +++ b/spec/frontend/repository/components/table/parent_row_spec.js @@ -30,8 +30,8 @@ describe('Repository parent row component', () => { it.each` path | to - ${'app'} | ${'/tree/master/'} - ${'app/assets'} | ${'/tree/master/app'} + ${'app'} | ${'/-/tree/master/'} + ${'app/assets'} | ${'/-/tree/master/app'} `('renders link in $path to $to', ({ path, to }) => { factory(path); @@ -46,7 +46,7 @@ describe('Repository parent row component', () => { vm.find('td').trigger('click'); expect($router.push).toHaveBeenCalledWith({ - path: '/tree/master/app', + path: '/-/tree/master/app', }); }); @@ -58,7 +58,7 @@ describe('Repository parent row component', () => { vm.find('a').trigger('click'); expect($router.push).not.toHaveBeenCalledWith({ - path: '/tree/master/app', + path: '/-/tree/master/app', }); }); }); diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 94fa8b1e363..3114b6a2eaa 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -83,7 +83,7 @@ describe('Repository table row component', () => { vm.trigger('click'); if (pushes) { - expect($router.push).toHaveBeenCalledWith({ path: '/tree/master/test' }); + expect($router.push).toHaveBeenCalledWith({ path: '/-/tree/master/test' }); } else { expect($router.push).not.toHaveBeenCalled(); } diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index f61a0ccd1e6..1efd74a30c2 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -4,11 +4,11 @@ import createRouter from '~/repository/router'; describe('Repository router spec', () => { it.each` - path | component | componentName - ${'/'} | ${IndexPage} | ${'IndexPage'} - ${'/tree/master'} | ${TreePage} | ${'TreePage'} - ${'/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} - ${'/tree/123/app/assets'} | ${null} | ${'null'} + path | component | componentName + ${'/'} | ${IndexPage} | ${'IndexPage'} + ${'/-/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/123/app/assets'} | ${null} | ${'null'} `('sets component as $componentName for path "$path"', ({ path, component }) => { const router = createRouter('', 'master'); diff --git a/spec/frontend/snippets/components/app_spec.js b/spec/frontend/snippets/components/app_spec.js index 535e71b6da7..f2800f9e6af 100644 --- a/spec/frontend/snippets/components/app_spec.js +++ b/spec/frontend/snippets/components/app_spec.js @@ -1,19 +1,22 @@ import SnippetApp from '~/snippets/components/app.vue'; +import SnippetHeader from '~/snippets/components/snippet_header.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; + import { createLocalVue, shallowMount } from '@vue/test-utils'; describe('Snippet view app', () => { let wrapper; - let snippetDataMock; const localVue = createLocalVue(); const defaultProps = { - snippetGid: 'gid://gitlab/PersonalSnippet/35', + snippetGid: 'gid://gitlab/PersonalSnippet/42', }; - function createComponent({ props = defaultProps, snippetData = {} } = {}) { - snippetDataMock = jest.fn(); + function createComponent({ props = defaultProps, loading = false } = {}) { const $apollo = { queries: { - snippetData: snippetDataMock, + snippet: { + loading, + }, }, }; @@ -25,17 +28,18 @@ describe('Snippet view app', () => { ...props, }, }); - - wrapper.setData({ - snippetData, - }); } afterEach(() => { wrapper.destroy(); }); - it('renders itself', () => { + it('renders loader while the query is in flight', () => { + createComponent({ loading: true }); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders SnippetHeader component after the query is finished', () => { createComponent(); - expect(wrapper.find('.js-snippet-view').exists()).toBe(true); + expect(wrapper.find(SnippetHeader).exists()).toBe(true); }); }); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js new file mode 100644 index 00000000000..8847a3a6938 --- /dev/null +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -0,0 +1,171 @@ +import SnippetHeader from '~/snippets/components/snippet_header.vue'; +import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; +import { ApolloMutation } from 'vue-apollo'; +import { GlButton, GlModal } from '@gitlab/ui'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; + +describe('Snippet header component', () => { + let wrapper; + const localVue = createLocalVue(); + const snippet = { + snippet: { + id: 'gid://gitlab/PersonalSnippet/50', + title: 'The property of Thor', + visibilityLevel: 'private', + webUrl: 'http://personal.dev.null/42', + userPermissions: { + adminSnippet: true, + updateSnippet: true, + reportSnippet: false, + }, + project: null, + author: { + name: 'Thor Odinson', + }, + }, + }; + const mutationVariables = { + mutation: DeleteSnippetMutation, + variables: { + id: snippet.snippet.id, + }, + }; + const errorMsg = 'Foo bar'; + const err = { message: errorMsg }; + + const resolveMutate = jest.fn(() => Promise.resolve()); + const rejectMutation = jest.fn(() => Promise.reject(err)); + + const mutationTypes = { + RESOLVE: resolveMutate, + REJECT: rejectMutation, + }; + + function createComponent({ + loading = false, + permissions = {}, + mutationRes = mutationTypes.RESOLVE, + } = {}) { + const defaultProps = Object.assign({}, snippet); + if (permissions) { + Object.assign(defaultProps.snippet.userPermissions, { + ...permissions, + }); + } + const $apollo = { + queries: { + canCreateSnippet: { + loading, + }, + }, + mutate: mutationRes, + }; + + wrapper = shallowMount(SnippetHeader, { + sync: false, + mocks: { $apollo }, + localVue, + propsData: { + ...defaultProps, + }, + stubs: { + ApolloMutation, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders itself', () => { + createComponent(); + expect(wrapper.find('.detail-page-header').exists()).toBe(true); + }); + + it('renders action buttons based on permissions', () => { + createComponent({ + permissions: { + adminSnippet: false, + updateSnippet: false, + }, + }); + expect(wrapper.findAll(GlButton).length).toEqual(0); + + createComponent({ + permissions: { + adminSnippet: true, + updateSnippet: false, + }, + }); + expect(wrapper.findAll(GlButton).length).toEqual(1); + + createComponent({ + permissions: { + adminSnippet: true, + updateSnippet: true, + }, + }); + expect(wrapper.findAll(GlButton).length).toEqual(2); + + createComponent({ + permissions: { + adminSnippet: true, + updateSnippet: true, + }, + }); + wrapper.setData({ + canCreateSnippet: true, + }); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.findAll(GlButton).length).toEqual(3); + }); + }); + + it('renders modal for deletion of a snippet', () => { + createComponent(); + expect(wrapper.find(GlModal).exists()).toBe(true); + }); + + describe('Delete mutation', () => { + const { location } = window; + + beforeEach(() => { + delete window.location; + window.location = { + pathname: '', + }; + }); + + afterEach(() => { + window.location = location; + }); + + it('dispatches a mutation to delete the snippet with correct variables', () => { + createComponent(); + wrapper.vm.deleteSnippet(); + expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables); + }); + + it('sets error message if mutation fails', () => { + createComponent({ mutationRes: mutationTypes.REJECT }); + expect(Boolean(wrapper.vm.errorMessage)).toBe(false); + + wrapper.vm.deleteSnippet(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.errorMessage).toEqual(errorMsg); + }); + }); + + it('closes modal and redirects to snippets listing in case of successful mutation', () => { + createComponent(); + wrapper.vm.closeDeleteModal = jest.fn(); + + wrapper.vm.deleteSnippet(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled(); + expect(window.location.pathname).toEqual('dashboard/snippets'); + }); + }); + }); +}); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 4636de6b8b6..ab42dbe7cd1 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -34,18 +34,6 @@ Vue.config.productionTip = false; Vue.use(Translate); -// workaround for JSDOM not supporting innerText -// see https://github.com/jsdom/jsdom/issues/1245 -Object.defineProperty(global.Element.prototype, 'innerText', { - get() { - return this.textContent; - }, - set(value) { - this.textContext = value; - }, - configurable: true, // make it so that it doesn't blow chunks on re-running tests with things like --watch -}); - // convenience wrapper for migration from Karma Object.assign(global, { getJSONFixture, |