diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/frontend/vue_shared | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/frontend/vue_shared')
20 files changed, 382 insertions, 356 deletions
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index ce410a8b3e7..68bcf1dc491 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -89,7 +89,7 @@ describe('AlertDetails', () => { const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError'); const findEnvironmentName = () => wrapper.findByTestId('environmentName'); const findEnvironmentPath = () => wrapper.findByTestId('environmentPath'); - const findDetailsTable = () => wrapper.find(AlertDetailsTable); + const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable); const findMetricsTab = () => wrapper.findByTestId('metrics'); describe('Alert details', () => { @@ -188,27 +188,39 @@ describe('AlertDetails', () => { }); expect(findMetricsTab().exists()).toBe(false); }); + + it('should display "View incident" button that links the issues page when incident exists', () => { + const iid = '3'; + mountComponent({ + data: { alert: { ...mockAlert, issue: { iid } }, sidebarStatus: false }, + provide: { isThreatMonitoringPage: true }, + }); + + expect(findViewIncidentBtn().exists()).toBe(true); + expect(findViewIncidentBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, iid)); + expect(findCreateIncidentBtn().exists()).toBe(false); + }); }); describe('Create incident from alert', () => { it('should display "View incident" button that links the incident page when incident exists', () => { - const issueIid = '3'; + const iid = '3'; mountComponent({ - data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false }, + data: { alert: { ...mockAlert, issue: { iid } }, sidebarStatus: false }, }); expect(findViewIncidentBtn().exists()).toBe(true); expect(findViewIncidentBtn().attributes('href')).toBe( - joinPaths(projectIssuesPath, issueIid), + joinPaths(projectIssuesPath, 'incident', iid), ); expect(findCreateIncidentBtn().exists()).toBe(false); }); it('should display "Create incident" button when incident doesn\'t exist yet', () => { - const issueIid = null; + const issue = null; mountComponent({ mountMethod: mount, - data: { alert: { ...mockAlert, issueIid } }, + data: { alert: { ...mockAlert, issue } }, }); return wrapper.vm.$nextTick().then(() => { diff --git a/spec/frontend/vue_shared/alert_details/mocks/alerts.json b/spec/frontend/vue_shared/alert_details/mocks/alerts.json index 5267a4fe50d..007557e234a 100644 --- a/spec/frontend/vue_shared/alert_details/mocks/alerts.json +++ b/spec/frontend/vue_shared/alert_details/mocks/alerts.json @@ -21,7 +21,7 @@ "endedAt": "2020-04-17T23:18:14.996Z", "status": "ACKNOWLEDGED", "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] }, - "issueIid": "1", + "issue": { "state" : "closed", "iid": "1", "title": "My test issue" }, "notes": { "nodes": [ { diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index f34a2db0851..99bf0d84d0c 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -88,7 +88,7 @@ describe('RelatedIssuableItem', () => { const stateTitle = tokenState().attributes('title'); const formattedCreateDate = formatDate(props.createdAt); - expect(stateTitle).toContain('<span class="bold">Opened</span>'); + expect(stateTitle).toContain('<span class="bold">Created</span>'); expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`); }); 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 bf65adc866d..5364e2d5f52 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 @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue'; import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; const DEFAULT_PROPS = { @@ -38,7 +39,7 @@ describe('Suggestion Diff component', () => { wrapper.destroy(); }); - const findApplyButton = () => wrapper.find('.js-apply-btn'); + const findApplyButton = () => wrapper.find(ApplySuggestion); const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn'); const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn'); const findRemoveFromBatchButton = () => wrapper.find('.js-remove-from-batch-btn'); @@ -88,7 +89,7 @@ describe('Suggestion Diff component', () => { beforeEach(() => { createComponent(); - findApplyButton().vm.$emit('click'); + findApplyButton().vm.$emit('apply'); }); it('emits apply', () => { diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js index 99671f1ffb7..566ca1817f2 100644 --- a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js @@ -1,3 +1,4 @@ +import { GlDropdown } from '@gitlab/ui'; import { getByText } from '@testing-library/dom'; import { shallowMount } from '@vue/test-utils'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; @@ -25,6 +26,9 @@ describe('MultiSelectDropdown Component', () => { slots: { search: '<p>Search</p>', }, + stubs: { + GlDropdown, + }, }); expect(getByText(wrapper.element, 'Search')).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap index da49778f216..30b7f0c2d28 100644 --- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap @@ -2,20 +2,26 @@ exports[`Package code instruction multiline to match the snapshot 1`] = ` <div> - <pre - class="gl-font-monospace" - data-testid="multiline-instruction" + <label + for="instruction-input_3" > - this is some + foo_label + </label> + + <div> + <pre + class="gl-font-monospace" + data-testid="multiline-instruction" + > + this is some multiline text - </pre> + </pre> + </div> </div> `; exports[`Package code instruction single line to match the default snapshot 1`] = ` -<div - class="gl-mb-3" -> +<div> <label for="instruction-input_2" > @@ -23,42 +29,46 @@ exports[`Package code instruction single line to match the default snapshot 1`] </label> <div - class="input-group gl-mb-3" + class="gl-mb-3" > - <input - class="form-control gl-font-monospace" - data-testid="instruction-input" - id="instruction-input_2" - readonly="readonly" - type="text" - /> - - <span - class="input-group-append" - data-testid="instruction-button" + <div + class="input-group gl-mb-3" > - <button - aria-label="Copy this value" - class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" - data-clipboard-text="npm i @my-package" - title="Copy npm install command" - type="button" + <input + class="form-control gl-font-monospace" + data-testid="instruction-input" + id="instruction-input_2" + readonly="readonly" + type="text" + /> + + <span + class="input-group-append" + data-testid="instruction-button" > - <!----> - - <svg - aria-hidden="true" - class="gl-button-icon gl-icon s16" - data-testid="copy-to-clipboard-icon" + <button + aria-label="Copy this value" + class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" + data-clipboard-text="npm i @my-package" + title="Copy npm install command" + type="button" > - <use - href="#copy-to-clipboard" - /> - </svg> - - <!----> - </button> - </span> + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="copy-to-clipboard-icon" + > + <use + href="#copy-to-clipboard" + /> + </svg> + + <!----> + </button> + </span> + </div> </div> </div> `; diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js new file mode 100644 index 00000000000..c65ded000d3 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js @@ -0,0 +1,122 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import component from '~/vue_shared/components/registry/persisted_dropdown_selection.vue'; + +describe('Persisted dropdown selection', () => { + let wrapper; + + const defaultProps = { + storageKey: 'foo_bar', + options: [ + { value: 'maven', label: 'Maven' }, + { value: 'gradle', label: 'Gradle' }, + ], + }; + + function createComponent({ props = {}, data = {} } = {}) { + wrapper = shallowMount(component, { + propsData: { + ...defaultProps, + ...props, + }, + data() { + return data; + }, + }); + } + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('local storage sync', () => { + it('uses the local storage sync component', () => { + createComponent(); + + expect(findLocalStorageSync().exists()).toBe(true); + }); + + it('passes the right props', () => { + createComponent({ data: { selected: 'foo' } }); + + expect(findLocalStorageSync().props()).toMatchObject({ + storageKey: defaultProps.storageKey, + value: 'foo', + }); + }); + + it('on input event updates the model and emits event', async () => { + const inputPayload = 'bar'; + createComponent(); + findLocalStorageSync().vm.$emit('input', inputPayload); + + await nextTick(); + + expect(wrapper.emitted('change')).toStrictEqual([[inputPayload]]); + expect(findLocalStorageSync().props('value')).toBe(inputPayload); + }); + }); + + describe('dropdown', () => { + it('has a dropdown component', () => { + createComponent(); + + expect(findDropdown().exists()).toBe(true); + }); + + describe('dropdown text', () => { + it('when no selection shows the first', () => { + createComponent(); + + expect(findDropdown().props('text')).toBe('Maven'); + }); + + it('when an option is selected, shows that option label', () => { + createComponent({ data: { selected: defaultProps.options[1].value } }); + + expect(findDropdown().props('text')).toBe('Gradle'); + }); + }); + + describe('dropdown items', () => { + it('has one item for each option', () => { + createComponent(); + + expect(findDropdownItems()).toHaveLength(defaultProps.options.length); + }); + + it('binds the correct props', () => { + createComponent({ data: { selected: defaultProps.options[0].value } }); + + expect(findDropdownItems().at(0).props()).toMatchObject({ + isChecked: true, + isCheckItem: true, + }); + + expect(findDropdownItems().at(1).props()).toMatchObject({ + isChecked: false, + isCheckItem: true, + }); + }); + + it('on click updates the data and emits event', async () => { + createComponent({ data: { selected: defaultProps.options[0].value } }); + expect(findDropdownItems().at(0).props('isChecked')).toBe(true); + + findDropdownItems().at(1).vm.$emit('click'); + + await nextTick(); + + expect(wrapper.emitted('change')).toStrictEqual([['gradle']]); + expect(findDropdownItems().at(0).props('isChecked')).toBe(false); + expect(findDropdownItems().at(1).props('isChecked')).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap index 51b8aa162bc..ed085fb66dc 100644 --- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap +++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap @@ -2,7 +2,7 @@ exports[`Settings Block renders the correct markup 1`] = ` <section - class="settings no-animate" + class="settings" > <div class="settings-header" diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js index 2db0b001b5b..be5a15631eb 100644 --- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js +++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js @@ -50,6 +50,27 @@ describe('Settings Block', () => { expect(findDescriptionSlot().exists()).toBe(true); }); + describe('slide animation behaviour', () => { + it('is animated by default', () => { + mountComponent(); + + expect(wrapper.classes('no-animate')).toBe(false); + }); + + it.each` + slideAnimated | noAnimatedClass + ${true} | ${false} + ${false} | ${true} + `( + 'sets the correct state when slideAnimated is $slideAnimated', + ({ slideAnimated, noAnimatedClass }) => { + mountComponent({ slideAnimated }); + + expect(wrapper.classes('no-animate')).toBe(noAnimatedClass); + }, + ); + }); + describe('expanded behaviour', () => { it('is collapsed by default', () => { mountComponent(); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js index 0d1d6ebcfe5..c90e63313b2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js @@ -11,32 +11,31 @@ import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); -const createComponent = (initialState = mockConfig, slots = {}) => { - const store = new Vuex.Store(labelsSelectModule()); - - store.dispatch('setInitialState', initialState); - - return shallowMount(DropdownValue, { - localVue, - store, - slots, - }); -}; - describe('DropdownValue', () => { let wrapper; - beforeEach(() => { - wrapper = createComponent(); - }); + const createComponent = (initialState = {}, slots = {}) => { + const store = new Vuex.Store(labelsSelectModule()); + + store.dispatch('setInitialState', { ...mockConfig, ...initialState }); + + wrapper = shallowMount(DropdownValue, { + localVue, + store, + slots, + }); + }; afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('methods', () => { describe('labelFilterUrl', () => { it('returns a label filter URL based on provided label param', () => { + createComponent(); + expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe( '/gitlab-org/my-project/issues?label_name[]=Foo%20Label', ); @@ -44,6 +43,10 @@ describe('DropdownValue', () => { }); describe('scopedLabel', () => { + beforeEach(() => { + createComponent(); + }); + it('returns `true` when provided label param is a scoped label', () => { expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true); }); @@ -56,28 +59,29 @@ describe('DropdownValue', () => { describe('template', () => { it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => { + createComponent(); + expect(wrapper.attributes('class')).toContain('has-labels'); }); it('renders element containing `None` when `selectedLabels` is empty', () => { - const wrapperNoLabels = createComponent( + createComponent( { - ...mockConfig, selectedLabels: [], }, { default: 'None', }, ); - const noneEl = wrapperNoLabels.find('span.text-secondary'); + const noneEl = wrapper.find('span.text-secondary'); expect(noneEl.exists()).toBe(true); expect(noneEl.text()).toBe('None'); - - wrapperNoLabels.destroy(); }); it('renders labels when `selectedLabels` is not empty', () => { + createComponent(); + expect(wrapper.findAll(GlLabel).length).toBe(2); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js index 85a14226585..f293b8422e7 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js @@ -47,6 +47,7 @@ export const mockConfig = { labelsFetchPath: '/gitlab-org/my-project/-/labels.json', labelsManagePath: '/gitlab-org/my-project/-/labels', labelsFilterBasePath: '/gitlab-org/my-project/issues', + labelsFilterParam: 'label_name', }; export const mockSuggestedColors = { diff --git a/spec/frontend/vue_shared/components/tabs/tab_spec.js b/spec/frontend/vue_shared/components/tabs/tab_spec.js deleted file mode 100644 index ee0c983c764..00000000000 --- a/spec/frontend/vue_shared/components/tabs/tab_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import Tab from '~/vue_shared/components/tabs/tab.vue'; - -describe('Tab component', () => { - const Component = Vue.extend(Tab); - let vm; - - beforeEach(() => { - vm = mountComponent(Component); - }); - - it('sets localActive to equal active', (done) => { - vm.active = true; - - vm.$nextTick(() => { - expect(vm.localActive).toBe(true); - - done(); - }); - }); - - it('sets active class', (done) => { - vm.active = true; - - vm.$nextTick(() => { - expect(vm.$el.classList).toContain('active'); - - done(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js deleted file mode 100644 index fe7be5be899..00000000000 --- a/spec/frontend/vue_shared/components/tabs/tabs_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; -import Tab from '~/vue_shared/components/tabs/tab.vue'; -import Tabs from '~/vue_shared/components/tabs/tabs'; - -describe('Tabs component', () => { - let vm; - - beforeEach(() => { - vm = new Vue({ - components: { - Tabs, - Tab, - }, - render(h) { - return h('div', [ - h('tabs', [ - h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'), - h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']), - ]), - ]); - }, - }).$mount(); - - return vm.$nextTick(); - }); - - describe('tab links', () => { - it('renders links for tabs', () => { - expect(vm.$el.querySelectorAll('a').length).toBe(2); - }); - - it('renders link titles from props', () => { - expect(vm.$el.querySelector('a').textContent).toContain('Testing'); - }); - - it('renders link titles from slot', () => { - expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot'); - }); - - it('renders active class', () => { - expect(vm.$el.querySelector('a').classList).toContain('active'); - }); - - it('updates active class on click', () => { - vm.$el.querySelectorAll('a')[1].click(); - - return vm.$nextTick(() => { - expect(vm.$el.querySelector('a').classList).not.toContain('active'); - expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active'); - }); - }); - }); - - describe('content', () => { - it('renders content panes', () => { - expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2); - expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab'); - expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab'); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js index 27c9b099306..380b7231acd 100644 --- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js @@ -11,6 +11,15 @@ jest.mock('~/lib/utils/dom_utils', () => ({ throw new Error('this needs to be mocked'); }), })); +jest.mock('@gitlab/ui', () => ({ + GlTooltipDirective: { + bind(el, binding) { + el.classList.add('gl-tooltip'); + el.setAttribute('data-original-title', el.title); + el.dataset.placement = binding.value.placement; + }, + }, +})); describe('TooltipOnTruncate component', () => { let wrapper; @@ -52,7 +61,7 @@ describe('TooltipOnTruncate component', () => { wrapper = parent.find(TooltipOnTruncate); }; - const hasTooltip = () => wrapper.classes('js-show-tooltip'); + const hasTooltip = () => wrapper.classes('gl-tooltip'); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap index d2fe3cd76cb..af4fa462cbf 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap +++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap @@ -19,6 +19,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess <p class="gl-mb-0" + data-testid="upload-text" > <span> Test %{linkStart}description%{linkEnd} message. @@ -98,10 +99,15 @@ exports[`Upload dropzone component when dragging renders correct template when d <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> @@ -178,10 +184,15 @@ exports[`Upload dropzone component when dragging renders correct template when d <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> @@ -258,10 +269,15 @@ exports[`Upload dropzone component when dragging renders correct template when d <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> @@ -337,10 +353,15 @@ exports[`Upload dropzone component when dragging renders correct template when d <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> @@ -416,10 +437,15 @@ exports[`Upload dropzone component when dragging renders correct template when d <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> @@ -495,10 +521,15 @@ exports[`Upload dropzone component when no slot provided renders default dropzon <p class="gl-mb-0" + data-testid="upload-text" > - <gl-sprintf-stub - message="Drop or %{linkStart}upload%{linkEnd} files to attach" - /> + Drop or + <gl-link-stub> + + upload + + </gl-link-stub> + files to attach </p> </div> </button> diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js index ace486b1f32..b3cdbccb271 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js +++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js @@ -1,4 +1,4 @@ -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; @@ -14,6 +14,7 @@ describe('Upload dropzone component', () => { const findDropzoneCard = () => wrapper.find('.upload-dropzone-card'); const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]'); const findIcon = () => wrapper.find(GlIcon); + const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text(); function createComponent({ slots = {}, data = {}, props = {} } = {}) { wrapper = shallowMount(UploadDropzone, { @@ -22,6 +23,9 @@ describe('Upload dropzone component', () => { displayAsCard: true, ...props, }, + stubs: { + GlSprintf, + }, data() { return data; }, @@ -30,6 +34,7 @@ describe('Upload dropzone component', () => { afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('when slot provided', () => { @@ -60,6 +65,18 @@ describe('Upload dropzone component', () => { }); }); + describe('upload text', () => { + it.each` + collection | description | props | expected + ${'multiple'} | ${'by default'} | ${null} | ${'files to attach'} + ${'singular'} | ${'when singleFileSelection'} | ${{ singleFileSelection: true }} | ${'file to attach'} + `('displays $collection version $description', ({ props, expected }) => { + createComponent({ props }); + + expect(findUploadText()).toContain(expected); + }); + }); + describe('when dragging', () => { it.each` description | eventPayload @@ -141,6 +158,21 @@ describe('Upload dropzone component', () => { wrapper.vm.ondrop(mockEvent); expect(wrapper.emitted()).not.toHaveProperty('error'); }); + + describe('singleFileSelection = true', () => { + it('emits a single file on drop', () => { + createComponent({ + data: mockData, + props: { singleFileSelection: true }, + }); + + const mockFile = { type: 'image/jpg' }; + const mockEvent = mockDragEvent({ files: [mockFile] }); + + wrapper.vm.ondrop(mockEvent); + expect(wrapper.emitted().change[0]).toEqual([mockFile]); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/user_access_role_badge_spec.js b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js new file mode 100644 index 00000000000..7f25f7c08e7 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js @@ -0,0 +1,26 @@ +import { GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; + +describe('UserAccessRoleBadge', () => { + let wrapper; + + const createComponent = ({ slots } = {}) => { + wrapper = shallowMount(UserAccessRoleBadge, { + slots, + }); + }; + + it('renders slot content inside GlBadge', () => { + createComponent({ + slots: { + default: 'test slot content', + }, + }); + + const badge = wrapper.find(GlBadge); + + expect(badge.exists()).toBe(true); + expect(badge.html()).toContain('test slot content'); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index a6c5e23ae14..184a1e458b5 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -9,6 +9,7 @@ const DEFAULT_PROPS = { username: 'root', name: 'Administrator', location: 'Vienna', + bot: false, bio: null, workInformation: null, status: null, @@ -18,14 +19,10 @@ const DEFAULT_PROPS = { describe('User Popover Component', () => { const fixtureTemplate = 'merge_requests/diff_comment.html'; - preloadFixtures(fixtureTemplate); let wrapper; beforeEach(() => { - window.gon.features = { - securityAutoFix: true, - }; loadFixtures(fixtureTemplate); }); @@ -37,6 +34,7 @@ describe('User Popover Component', () => { const findUserStatus = () => wrapper.find('.js-user-status'); const findTarget = () => document.querySelector('.js-user-link'); const findUserName = () => wrapper.find(UserNameWithStatus); + const findSecurityBotDocsLink = () => findByTestId('user-popover-bot-docs-link'); const createWrapper = (props = {}, options = {}) => { wrapper = shallowMount(UserPopover, { @@ -86,6 +84,12 @@ describe('User Popover Component', () => { expect(iconEl.props('name')).toEqual('location'); }); + + it("should not show a link to bot's documentation", () => { + createWrapper(); + const securityBotDocsLink = findSecurityBotDocsLink(); + expect(securityBotDocsLink.exists()).toBe(false); + }); }); describe('job data', () => { @@ -230,14 +234,14 @@ describe('User Popover Component', () => { }); }); - describe('security bot', () => { + describe('bot user', () => { const SECURITY_BOT_USER = { ...DEFAULT_PROPS.user, name: 'GitLab Security Bot', username: 'GitLab-Security-Bot', websiteUrl: '/security/bot/docs', + bot: true, }; - const findSecurityBotDocsLink = () => findByTestId('user-popover-bot-docs-link'); it("shows a link to the bot's documentation", () => { createWrapper({ user: SECURITY_BOT_USER }); @@ -245,14 +249,5 @@ describe('User Popover Component', () => { expect(securityBotDocsLink.exists()).toBe(true); expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl); }); - - it('does not show the link if the feature flag is disabled', () => { - window.gon.features = { - securityAutoFix: false, - }; - createWrapper({ user: SECURITY_BOT_USER }); - - expect(findSecurityBotDocsLink().exists()).toBe(false); - }); }); }); diff --git a/spec/frontend/vue_shared/directives/tooltip_spec.js b/spec/frontend/vue_shared/directives/tooltip_spec.js deleted file mode 100644 index 99e8b5b552b..00000000000 --- a/spec/frontend/vue_shared/directives/tooltip_spec.js +++ /dev/null @@ -1,157 +0,0 @@ -import { mount } from '@vue/test-utils'; -import $ from 'jquery'; -import { escape } from 'lodash'; -import tooltip from '~/vue_shared/directives/tooltip'; - -const DEFAULT_TOOLTIP_TEMPLATE = '<div v-tooltip :title="tooltip"></div>'; -const HTML_TOOLTIP_TEMPLATE = '<div v-tooltip data-html="true" :title="tooltip"></div>'; - -describe('Tooltip directive', () => { - let wrapper; - - function createTooltipContainer({ - template = DEFAULT_TOOLTIP_TEMPLATE, - text = 'some text', - } = {}) { - wrapper = mount( - { - directives: { tooltip }, - data: () => ({ tooltip: text }), - template, - }, - { attachTo: document.body }, - ); - } - - async function showTooltip() { - $(wrapper.vm.$el).tooltip('show'); - jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - } - - function findTooltipInnerHtml() { - return document.querySelector('.tooltip-inner').innerHTML; - } - - function findTooltipHtml() { - return document.querySelector('.tooltip').innerHTML; - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('with a single tooltip', () => { - it('should have tooltip plugin applied', () => { - createTooltipContainer(); - - expect($(wrapper.vm.$el).data('bs.tooltip')).toBeDefined(); - }); - - it('displays the title as tooltip', () => { - createTooltipContainer(); - - $(wrapper.vm.$el).tooltip('show'); - - jest.runOnlyPendingTimers(); - - const tooltipElement = document.querySelector('.tooltip-inner'); - - expect(tooltipElement.textContent).toContain('some text'); - }); - - it.each` - condition | template | sanitize - ${'does not contain any html'} | ${DEFAULT_TOOLTIP_TEMPLATE} | ${false} - ${'contains html'} | ${HTML_TOOLTIP_TEMPLATE} | ${true} - `('passes sanitize=$sanitize if the tooltip $condition', ({ template, sanitize }) => { - createTooltipContainer({ template }); - - expect($(wrapper.vm.$el).data('bs.tooltip').config.sanitize).toEqual(sanitize); - }); - - it('updates a visible tooltip', async () => { - createTooltipContainer(); - - $(wrapper.vm.$el).tooltip('show'); - jest.runOnlyPendingTimers(); - - const tooltipElement = document.querySelector('.tooltip-inner'); - - wrapper.vm.tooltip = 'other text'; - - jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(tooltipElement.textContent).toContain('other text'); - }); - - describe('tooltip sanitization', () => { - it('reads tooltip content as text if data-html is not passed', async () => { - createTooltipContainer({ text: 'sample text<script>alert("XSS!!")</script>' }); - - await showTooltip(); - - const result = findTooltipInnerHtml(); - expect(result).toEqual('sample text<script>alert("XSS!!")</script>'); - }); - - it('sanitizes tooltip if data-html is passed', async () => { - createTooltipContainer({ - template: HTML_TOOLTIP_TEMPLATE, - text: 'sample text<script>alert("XSS!!")</script>', - }); - - await showTooltip(); - - const result = findTooltipInnerHtml(); - expect(result).toEqual('sample text'); - expect(result).not.toContain('XSS!!'); - }); - - it('sanitizes tooltip if data-template is passed', async () => { - const tooltipTemplate = escape( - '<div class="tooltip" role="tooltip"><div onclick="alert(\'XSS!\')" class="arrow"></div><div class="tooltip-inner"></div></div>', - ); - - createTooltipContainer({ - template: `<div v-tooltip :title="tooltip" data-html="false" data-template="${tooltipTemplate}"></div>`, - }); - - await showTooltip(); - - const result = findTooltipHtml(); - expect(result).toEqual( - // objectionable element is removed - '<div class="arrow"></div><div class="tooltip-inner">some text</div>', - ); - expect(result).not.toContain('XSS!!'); - }); - }); - }); - - describe('with multiple tooltips', () => { - beforeEach(() => { - createTooltipContainer({ - template: ` - <div> - <div - v-tooltip - class="js-look-for-tooltip" - title="foo"> - </div> - <div - v-tooltip - title="bar"> - </div> - </div> - `, - }); - }); - - it('should have tooltip plugin applied to all instances', () => { - expect($(wrapper.vm.$el).find('.js-look-for-tooltip').data('bs.tooltip')).toBeDefined(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js b/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js index 6ecc330b5af..3fb60c254c9 100644 --- a/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js +++ b/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js @@ -11,6 +11,10 @@ describe('GitLab Feature Flags Plugin', () => { aFeature: true, bFeature: false, }, + licensed_features: { + cFeature: true, + dFeature: false, + }, }; localVue.use(GlFeatureFlags); @@ -25,6 +29,8 @@ describe('GitLab Feature Flags Plugin', () => { expect(wrapper.vm.glFeatures).toEqual({ aFeature: true, bFeature: false, + cFeature: true, + dFeature: false, }); }); @@ -37,6 +43,8 @@ describe('GitLab Feature Flags Plugin', () => { expect(wrapper.vm.glFeatures).toEqual({ aFeature: true, bFeature: false, + cFeature: true, + dFeature: false, }); }); }); |