diff options
Diffstat (limited to 'spec/frontend/vue_shared')
37 files changed, 715 insertions, 199 deletions
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js index 28b3bf5287a..8cbe0630426 100644 --- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js +++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js @@ -3,6 +3,8 @@ import { mount, shallowMount } from '@vue/test-utils'; import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; +jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1)); + describe('ColorPicker', () => { let wrapper; @@ -14,10 +16,11 @@ describe('ColorPicker', () => { const setColor = '#000000'; const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value'; - const label = () => wrapper.find(GlFormGroup).attributes('label'); + const findGlFormGroup = () => wrapper.find(GlFormGroup); const colorPreview = () => wrapper.find('[data-testid="color-preview"]'); const colorPicker = () => wrapper.find(GlFormInput); - const colorInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]'); + const colorInput = () => wrapper.find('input[type="color"]'); + const colorTextInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]'); const invalidFeedback = () => wrapper.find('.invalid-feedback'); const description = () => wrapper.find(GlFormGroup).attributes('description'); const presetColors = () => wrapper.findAll(GlLink); @@ -39,13 +42,29 @@ describe('ColorPicker', () => { it('hides the label if the label is not passed', () => { createComponent(shallowMount); - expect(label()).toBe(''); + expect(findGlFormGroup().attributes('label')).toBe(''); }); it('shows the label if the label is passed', () => { createComponent(shallowMount, { label: 'test' }); - expect(label()).toBe('test'); + expect(findGlFormGroup().attributes('label')).toBe('test'); + }); + + describe.each` + desc | id + ${'with prop id'} | ${'test-id'} + ${'without prop id'} | ${undefined} + `('$desc', ({ id }) => { + beforeEach(() => { + createComponent(mount, { id, label: 'test' }); + }); + + it('renders the same `ID` for input and `for` for label', () => { + expect(findGlFormGroup().find('label').attributes('for')).toBe( + colorInput().attributes('id'), + ); + }); }); }); @@ -55,30 +74,30 @@ describe('ColorPicker', () => { expect(colorPreview().attributes('style')).toBe(undefined); expect(colorPicker().attributes('value')).toBe(undefined); - expect(colorInput().props('value')).toBe(''); + expect(colorTextInput().props('value')).toBe(''); expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400'); }); it('has a color set on initialization', () => { createComponent(mount, { value: setColor }); - expect(colorInput().props('value')).toBe(setColor); + expect(colorTextInput().props('value')).toBe(setColor); }); it('emits input event from component when a color is selected', async () => { createComponent(); - await colorInput().setValue(setColor); + await colorTextInput().setValue(setColor); expect(wrapper.emitted().input[0]).toStrictEqual([setColor]); }); it('trims spaces from submitted colors', async () => { createComponent(); - await colorInput().setValue(` ${setColor} `); + await colorTextInput().setValue(` ${setColor} `); expect(wrapper.emitted().input[0]).toStrictEqual([setColor]); expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400'); - expect(colorInput().attributes('class')).not.toContain('is-invalid'); + expect(colorTextInput().attributes('class')).not.toContain('is-invalid'); }); it('shows invalid feedback when the state is marked as invalid', async () => { @@ -86,14 +105,14 @@ describe('ColorPicker', () => { expect(invalidFeedback().text()).toBe(invalidText); expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500'); - expect(colorInput().attributes('class')).toContain('is-invalid'); + expect(colorTextInput().attributes('class')).toContain('is-invalid'); }); }); describe('inputs', () => { it('has color input value entered', async () => { createComponent(); - await colorInput().setValue(setColor); + await colorTextInput().setValue(setColor); expect(wrapper.emitted().input[0]).toStrictEqual([setColor]); }); diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js new file mode 100644 index 00000000000..9d11fbbaf55 --- /dev/null +++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js @@ -0,0 +1,52 @@ +import { GlBadge } from '@gitlab/ui'; + +import { shallowMount } from '@vue/test-utils'; +import { WorkspaceType, IssuableType } from '~/issues/constants'; + +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; + +const createComponent = ({ + workspaceType = WorkspaceType.project, + issuableType = IssuableType.Issue, +} = {}) => + shallowMount(ConfidentialityBadge, { + propsData: { + workspaceType, + issuableType, + }, + }); + +describe('ConfidentialityBadge', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + workspaceType | issuableType | expectedTooltip + ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least Reporter role can view or be notified about this issue.'} + ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least Reporter role can view or be notified about this epic.'} + `( + 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType', + ({ workspaceType, issuableType, expectedTooltip }) => { + wrapper = createComponent({ + workspaceType, + issuableType, + }); + + const badgeEl = wrapper.findComponent(GlBadge); + + expect(badgeEl.props()).toMatchObject({ + icon: 'eye-slash', + variant: 'warning', + }); + expect(badgeEl.attributes('title')).toBe(expectedTooltip); + expect(badgeEl.text()).toBe('Confidential'); + }, + ); +}); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js index f75694bd504..a660643d74f 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js @@ -3,6 +3,7 @@ import { CONFIRM_DANGER_WARNING, CONFIRM_DANGER_MODAL_BUTTON, CONFIRM_DANGER_MODAL_ID, + CONFIRM_DANGER_MODAL_CANCEL, } from '~/vue_shared/components/confirm_danger/constants'; import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -10,6 +11,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('Confirm Danger Modal', () => { const confirmDangerMessage = 'This is a dangerous activity'; const confirmButtonText = 'Confirm button text'; + const cancelButtonText = 'Cancel button text'; const phrase = 'You must construct additional pylons'; const modalId = CONFIRM_DANGER_MODAL_ID; @@ -21,6 +23,7 @@ describe('Confirm Danger Modal', () => { const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning'); const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message'); const findPrimaryAction = () => findModal().props('actionPrimary'); + const findCancelAction = () => findModal().props('actionCancel'); const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr]; const createComponent = ({ provide = {} } = {}) => @@ -34,7 +37,9 @@ describe('Confirm Danger Modal', () => { }); beforeEach(() => { - wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } }); + wrapper = createComponent({ + provide: { confirmDangerMessage, confirmButtonText, cancelButtonText }, + }); }); afterEach(() => { @@ -54,6 +59,10 @@ describe('Confirm Danger Modal', () => { expect(findPrimaryActionAttributes('variant')).toBe('danger'); }); + it('renders the cancel button', () => { + expect(findCancelAction().text).toBe(cancelButtonText); + }); + it('renders the correct confirmation phrase', () => { expect(findConfirmationPhrase().text()).toBe( `Please type ${phrase} to proceed or close this modal to cancel.`, @@ -72,6 +81,10 @@ describe('Confirm Danger Modal', () => { it('renders the default confirm button', () => { expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON); }); + + it('renders the default cancel button', () => { + expect(findCancelAction().text).toBe(CONFIRM_DANGER_MODAL_CANCEL); + }); }); describe('with a valid confirmation phrase', () => { diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js index d4b6b987c69..aa41df438d2 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js @@ -15,7 +15,7 @@ describe('DateTimePicker', () => { const dropdownToggle = () => wrapper.find('.dropdown-toggle'); const dropdownMenu = () => wrapper.find('.dropdown-menu'); const cancelButton = () => wrapper.find('[data-testid="cancelButton"]'); - const applyButtonElement = () => wrapper.find('button.btn-success').element; + const applyButtonElement = () => wrapper.find('button.btn-confirm').element; const findQuickRangeItems = () => wrapper.findAll('.dropdown-item'); const createComponent = (props) => { diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js index 59653a0ec13..e3d8bfd22ca 100644 --- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js +++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js @@ -6,12 +6,16 @@ import { folder } from './mock_data'; describe('Deploy Board Instance', () => { let wrapper; - const createComponent = (props = {}) => + const createComponent = (props = {}, provide) => shallowMount(DeployBoardInstance, { propsData: { status: 'succeeded', ...props, }, + provide: { + glFeatures: { monitorLogging: true }, + ...provide, + }, }); describe('as a non-canary deployment', () => { @@ -95,4 +99,23 @@ describe('Deploy Board Instance', () => { expect(wrapper.attributes('title')).toEqual(''); }); }); + + describe(':monitor_logging feature flag', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + flagState | logsState | expected + ${true} | ${'shows'} | ${'/root/review-app/-/logs?environment_name=foo&pod_name=tanuki-1'} + ${false} | ${'hides'} | ${undefined} + `('$logsState log link when flag state is $flagState', async ({ flagState, expected }) => { + wrapper = createComponent( + { logsPath: folder.logs_path, podName: 'tanuki-1' }, + { glFeatures: { monitorLogging: flagState } }, + ); + + expect(wrapper.attributes('href')).toEqual(expected); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js deleted file mode 100644 index 30b8e869aab..00000000000 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import Vue from 'vue'; - -import mountComponent from 'helpers/vue_mount_component_helper'; -import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; - -import { mockLabels } from './mock_data'; - -const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => { - const Component = Vue.extend(dropdownHiddenInputComponent); - - return mountComponent(Component, { - name, - value, - }); -}; - -describe('DropdownHiddenInputComponent', () => { - let vm; - - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('template', () => { - it('renders input element of type `hidden`', () => { - expect(vm.$el.nodeName).toBe('INPUT'); - expect(vm.$el.getAttribute('type')).toBe('hidden'); - expect(vm.$el.getAttribute('name')).toBe(vm.name); - expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js deleted file mode 100644 index b32dbeb8852..00000000000 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; - -describe('DropdownSearchInputComponent', () => { - let wrapper; - - const defaultProps = { - placeholderText: 'Search something', - }; - const buildVM = (propsData = defaultProps) => { - wrapper = mount(DropdownSearchInputComponent, { - propsData, - }); - }; - const findInputEl = () => wrapper.find('.dropdown-input-field'); - - beforeEach(() => { - buildVM(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('template', () => { - it('renders input element with type `search`', () => { - expect(findInputEl().exists()).toBe(true); - expect(findInputEl().attributes('type')).toBe('search'); - }); - - it('renders search icon element', () => { - expect(wrapper.find('.dropdown-input-search[data-testid="search-icon"]').exists()).toBe(true); - }); - - it('displays custom placeholder text', () => { - expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText); - }); - - it('focuses input element when focused property equals true', async () => { - const inputEl = findInputEl().element; - - jest.spyOn(inputEl, 'focus'); - - wrapper.setProps({ focused: true }); - - await nextTick(); - expect(inputEl.focus).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js index 921091c5b84..5cf891a2e52 100644 --- a/spec/frontend/vue_shared/components/file_finder/index_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js @@ -1,5 +1,6 @@ import Mousetrap from 'mousetrap'; import Vue, { nextTick } from 'vue'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { file } from 'jest/ide/helpers'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; @@ -22,7 +23,11 @@ describe('File finder item spec', () => { } beforeEach(() => { - setFixtures('<div id="app"></div>'); + setHTMLFixture('<div id="app"></div>'); + }); + + afterEach(() => { + resetHTMLFixture(); }); afterEach(() => { @@ -105,18 +110,6 @@ describe('File finder item spec', () => { }); }); - describe('listHeight', () => { - it('returns 55 when entries exist', () => { - expect(vm.listHeight).toBe(55); - }); - - it('returns 33 when entries dont exist', () => { - vm.searchText = 'testing 123'; - - expect(vm.listHeight).toBe(33); - }); - }); - describe('filteredBlobsLength', () => { it('returns length of filtered blobs', () => { vm.searchText = 'index'; @@ -253,11 +246,9 @@ describe('File finder item spec', () => { describe('without entries', () => { it('renders loading text when loading', () => { - createComponent({ - loading: true, - }); + createComponent({ loading: true }); - expect(vm.$el.textContent).toContain('Loading...'); + expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null); }); it('renders no files text', () => { @@ -307,7 +298,7 @@ describe('File finder item spec', () => { }); it('stops callback in monaco editor', () => { - setFixtures('<div class="inputarea"></div>'); + setHTMLFixture('<div class="inputarea"></div>'); expect( Mousetrap.prototype.stopCallback(null, document.querySelector('.inputarea'), 't'), diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index b6a181e6a0b..e44bc8771f5 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -11,7 +11,10 @@ import { shallowMount, mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; -import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + FILTERED_SEARCH_TERM, + SortDirection, +} from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; @@ -68,6 +71,10 @@ const createComponent = ({ describe('FilteredSearchBarRoot', () => { let wrapper; + const findGlButton = () => wrapper.findComponent(GlButton); + const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); + beforeEach(() => { wrapper = createComponent({ sortOptions: mockSortOptions }); }); @@ -79,7 +86,7 @@ describe('FilteredSearchBarRoot', () => { describe('data', () => { it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => { expect(wrapper.vm.filterValue).toEqual([]); - expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending); + expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]); expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending); expect(wrapper.find(GlButtonGroup).exists()).toBe(true); expect(wrapper.find(GlButton).exists()).toBe(true); @@ -225,9 +232,7 @@ describe('FilteredSearchBarRoot', () => { }); it('initializes `recentSearchesPromise` prop with a promise by using `recentSearchesService.fetch()`', () => { - jest - .spyOn(wrapper.vm.recentSearchesService, 'fetch') - .mockReturnValue(new Promise(() => [])); + jest.spyOn(wrapper.vm.recentSearchesService, 'fetch').mockResolvedValue([]); wrapper.vm.setupRecentSearch(); @@ -489,4 +494,40 @@ describe('FilteredSearchBarRoot', () => { expect(sortButtonEl.props('icon')).toBe('sort-highest'); }); }); + + describe('watchers', () => { + const tokenValue = { + id: 'id-1', + type: FILTERED_SEARCH_TERM, + value: { data: '' }, + }; + + it('syncs filter value', async () => { + await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: true }); + + expect(findGlFilteredSearch().props('value')).toEqual([tokenValue]); + }); + + it('does not sync filter value when syncFilterAndSort=false', async () => { + await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: false }); + + expect(findGlFilteredSearch().props('value')).toEqual([]); + }); + + it('syncs sort values', async () => { + await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true }); + + expect(findGlDropdown().props('text')).toBe('Last updated'); + expect(findGlButton().props('icon')).toBe('sort-lowest'); + expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Ascending'); + }); + + it('does not sync sort values when syncFilterAndSort=false', async () => { + await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: false }); + + expect(findGlDropdown().props('text')).toBe('Created date'); + expect(findGlButton().props('icon')).toBe('sort-highest'); + expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Descending'); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 87066b70023..3f24d5df858 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -51,6 +51,7 @@ function createComponent(options = {}) { config, value, active, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index af8a2a496ea..ca8cd419d87 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -78,6 +78,7 @@ const mockProps = { suggestionsLoading: false, defaultSuggestions: DEFAULT_NONE_ANY, getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data), + cursorPosition: 'start', }; function createComponent({ diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index 7a7db434052..7b495ec9bee 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -39,6 +39,7 @@ function createComponent(options = {}) { config, value, active, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index b163563cea4..dcb0d095b1b 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -45,6 +45,7 @@ function createComponent(options = {}) { config, value, active, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index 52df27c2d00..f03a2e7934f 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -45,6 +45,7 @@ function createComponent(options = {}) { config, value, active, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index de9ec863dd5..7c545f76c0b 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -42,6 +42,7 @@ function createComponent(options = {}) { config, value, active, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js index 8be21b35414..4bbbaab9b7a 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js @@ -18,6 +18,7 @@ describe('ReleaseToken', () => { active: false, config, value, + cursorPosition: 'start', }, provide: { portalName: 'fake target', diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js index b673e5407d4..b180e8c12dd 100644 --- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js +++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js @@ -1,7 +1,7 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import flushPromises from 'helpers/flush_promises'; +import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue'; @@ -43,7 +43,7 @@ describe('GitlabVersionCheck', () => { describe(`is ${description}`, () => { beforeEach(async () => { createComponent(mockResponse); - await flushPromises(); // Ensure we wrap up the axios call + await waitForPromises(); // Ensure we wrap up the axios call }); it(`does${renders ? '' : ' not'} render GlBadge`, () => { @@ -61,7 +61,7 @@ describe('GitlabVersionCheck', () => { describe(`when response is ${mockResponse.res.severity}`, () => { beforeEach(async () => { createComponent(mockResponse); - await flushPromises(); // Ensure we wrap up the axios call + await waitForPromises(); // Ensure we wrap up the axios call }); it(`title is ${expectedUI.title}`, () => { diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index d1c4d777d44..b3376f26a25 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -5,12 +5,14 @@ import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue'; +import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; const markdownPreviewPath = `${TEST_HOST}/preview`; const markdownDocsPath = `${TEST_HOST}/docs`; const textareaValue = 'testing\n123'; const uploadsPath = 'test/uploads'; +const restrictedToolBarItems = ['quote']; function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) { expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite); @@ -63,6 +65,7 @@ describe('Markdown field component', () => { textareaValue, lines, enablePreview, + restrictedToolBarItems, }, provide: { glFeatures: { @@ -81,6 +84,8 @@ describe('Markdown field component', () => { const getAttachButton = () => subject.find('.button-attach-file'); const clickAttachButton = () => getAttachButton().trigger('click'); const findDropzone = () => subject.find('.div-dropzone'); + const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader); + const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar); describe('mounted', () => { const previewHTML = ` @@ -184,9 +189,23 @@ describe('Markdown field component', () => { assertMarkdownTabs(false, writeLink, previewLink, subject); }); + + it('passes correct props to MarkdownToolbar', () => { + expect(findMarkdownToolbar().props()).toEqual({ + canAttachFile: true, + markdownDocsPath, + quickActionsDocsPath: '', + showCommentToolBar: true, + }); + }); }); describe('markdown buttons', () => { + beforeEach(() => { + // needed for the underlying insertText to work + document.execCommand = jest.fn(() => false); + }); + it('converts single words', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 7); @@ -309,9 +328,7 @@ describe('Markdown field component', () => { it('escapes new line characters', () => { createSubject({ lines: [{ rich_text: 'hello world\\n' }] }); - expect(subject.find('[data-testid="markdownHeader"]').props('lineContent')).toBe( - 'hello world%br', - ); + expect(findMarkdownHeader().props('lineContent')).toBe('hello world%br'); }); }); @@ -325,4 +342,12 @@ describe('Markdown field component', () => { expect(subject.findComponent(MarkdownFieldHeader).props('enablePreview')).toBe(true); }); + + it('passess restricted tool bar items', () => { + createSubject(); + + expect(subject.findComponent(MarkdownFieldHeader).props('restrictedToolBarItems')).toBe( + restrictedToolBarItems, + ); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index fa4ca63f910..67222cab247 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -166,4 +166,26 @@ describe('Markdown field header component', () => { expect(wrapper.findByTestId('preview-tab').exists()).toBe(false); }); + + describe('restricted tool bar items', () => { + let defaultCount; + + beforeEach(() => { + defaultCount = findToolbarButtons().length; + }); + + it('restricts items as per input', () => { + createWrapper({ + restrictedToolBarItems: ['quote'], + }); + + expect(findToolbarButtons().length).toBe(defaultCount - 1); + }); + + it('shows all items by default', () => { + createWrapper(); + + expect(findToolbarButtons().length).toBe(defaultCount); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index 8bff85b0bda..f698794b951 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js @@ -33,4 +33,18 @@ describe('toolbar', () => { expect(wrapper.vm.$el.querySelector('.uploading-container')).toBeNull(); }); }); + + describe('comment tool bar settings', () => { + it('does not show comment tool bar div', () => { + createMountedWrapper({ showCommentToolBar: false }); + + expect(wrapper.find('.comment-toolbar').exists()).toBe(false); + }); + + it('shows comment tool bar by default', () => { + createMountedWrapper(); + + expect(wrapper.find('.comment-toolbar').exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap index 5dd12d9edf5..015049795a1 100644 --- a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap +++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap @@ -10,6 +10,7 @@ exports[`Metrics upload item render the metrics image component 1`] = ` <gl-modal-stub actioncancel="[object Object]" actionprimary="[object Object]" + arialabel="" body-class="gl-pb-0! gl-min-h-6!" dismisslabel="Close" modalclass="" @@ -26,6 +27,7 @@ exports[`Metrics upload item render the metrics image component 1`] = ` <gl-modal-stub actioncancel="[object Object]" actionprimary="[object Object]" + arialabel="" data-testid="metric-image-edit-modal" dismisslabel="Close" modalclass="" diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js index 1b93292e37b..6e9abb2bfb3 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -101,20 +101,6 @@ describe('list item', () => { }); }); - describe('disabled prop', () => { - it('when true applies gl-opacity-5 class', () => { - mountComponent({ disabled: true }); - - expect(wrapper.classes('gl-opacity-5')).toBe(true); - }); - - it('when false does not apply gl-opacity-5 class', () => { - mountComponent({ disabled: false }); - - expect(wrapper.classes('gl-opacity-5')).toBe(false); - }); - }); - describe('borders and selection', () => { it.each` first | selected | shouldHave | shouldNotHave diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap index ac313e556fc..8ff49271eb5 100644 --- a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap +++ b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap @@ -4,6 +4,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-modal-stub actionprimary="[object Object]" actionsecondary="[object Object]" + arialabel="" dismisslabel="Close" modalclass="" modalid="runner-aws-deployments-modal" diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index 0da9939e97f..001b6ee4a6f 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -45,8 +45,10 @@ describe('RunnerInstructionsModal component', () => { const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAlert = () => wrapper.findComponent(GlAlert); + const findModal = () => wrapper.findComponent(GlModal); const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons'); const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton); + const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item'); const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions'); const findRegisterCommand = () => wrapper.findByTestId('register-command'); @@ -140,6 +142,38 @@ describe('RunnerInstructionsModal component', () => { expect(instructions).toBe(registerInstructions); }); }); + + describe('when the modal is shown', () => { + it('sets the focus on the selected platform', () => { + findPlatformButtons().at(0).element.focus = jest.fn(); + + findModal().vm.$emit('shown'); + + expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled(); + }); + }); + + describe('when providing a defaultPlatformName', () => { + beforeEach(async () => { + createComponent({ props: { defaultPlatformName: 'osx' } }); + await waitForPromises(); + }); + + it('runner instructions for the default selected platform are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ + platform: 'osx', + architecture: 'amd64', + }); + }); + + it('sets the focus on the default selected platform', () => { + findOsxPlatformButton().element.focus = jest.fn(); + + findModal().vm.$emit('shown'); + + expect(findOsxPlatformButton().element.focus).toHaveBeenCalled(); + }); + }); }); describe('after a platform and architecture are selected', () => { diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js new file mode 100644 index 00000000000..88445b6684c --- /dev/null +++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js @@ -0,0 +1,104 @@ +import { GlButtonGroup, GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue'; + +const DEFAULT_OPTIONS = [ + { text: 'Lorem', value: 'abc' }, + { text: 'Ipsum', value: 'def' }, + { text: 'Foo', value: 'x', disabled: true }, + { text: 'Dolar', value: 'ghi' }, +]; + +describe('~/vue_shared/components/segmented_control_button_group.vue', () => { + let wrapper; + + const createComponent = (props = {}, scopedSlots = {}) => { + wrapper = shallowMount(SegmentedControlButtonGroup, { + propsData: { + value: DEFAULT_OPTIONS[0].value, + options: DEFAULT_OPTIONS, + ...props, + }, + scopedSlots, + }); + }; + + const findButtonGroup = () => wrapper.findComponent(GlButtonGroup); + const findButtons = () => findButtonGroup().findAllComponents(GlButton); + const findButtonsData = () => + findButtons().wrappers.map((x) => ({ + selected: x.props('selected'), + text: x.text(), + disabled: x.props('disabled'), + })); + const findButtonWithText = (text) => findButtons().wrappers.find((x) => x.text() === text); + + const optionsAsButtonData = (options) => + options.map(({ text, disabled = false }) => ({ + selected: false, + text, + disabled, + })); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders button group', () => { + expect(findButtonGroup().exists()).toBe(true); + }); + + it('renders buttons', () => { + const expectation = optionsAsButtonData(DEFAULT_OPTIONS); + expectation[0].selected = true; + + expect(findButtonsData()).toEqual(expectation); + }); + + describe.each(DEFAULT_OPTIONS.filter((x) => !x.disabled))( + 'when button clicked %p', + ({ text, value }) => { + it('emits input with value', () => { + expect(wrapper.emitted('input')).toBeUndefined(); + + findButtonWithText(text).vm.$emit('click'); + + expect(wrapper.emitted('input')).toEqual([[value]]); + }); + }, + ); + }); + + const VALUE_TEST_CASES = [0, 1, 3].map((index) => [DEFAULT_OPTIONS[index].value, index]); + + describe.each(VALUE_TEST_CASES)('with value=%s', (value, index) => { + it(`renders selected button at ${index}`, () => { + createComponent({ value }); + + const expectation = optionsAsButtonData(DEFAULT_OPTIONS); + expectation[index].selected = true; + + expect(findButtonsData()).toEqual(expectation); + }); + }); + + describe('with button-content slot', () => { + it('renders button content based on slot', () => { + createComponent( + {}, + { + 'button-content': `<template #button-content="{ text }">In a slot - {{ text }}</template>`, + }, + ); + + expect(findButtonsData().map((x) => x.text)).toEqual( + DEFAULT_OPTIONS.map((x) => `In a slot - ${x.text}`), + ); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js index 3ceed670d77..9c29f304c71 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js @@ -153,7 +153,11 @@ describe('DropdownContentsCreateView', () => { }); it('enables a Create button', () => { - expect(findCreateButton().props('disabled')).toBe(false); + expect(findCreateButton().props()).toMatchObject({ + disabled: false, + category: 'primary', + variant: 'confirm', + }); }); it('renders a loader spinner after Create button click', async () => { diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js new file mode 100644 index 00000000000..662c09d02bf --- /dev/null +++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js @@ -0,0 +1,62 @@ +import { GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import component from '~/vue_shared/components/usage_quotas/usage_banner.vue'; + +describe('usage banner', () => { + let wrapper; + + const findLeftPrimaryTextSlot = () => wrapper.findByTestId('left-primary-text'); + const findLeftSecondaryTextSlot = () => wrapper.findByTestId('left-secondary-text'); + const findRightPrimaryTextSlot = () => wrapper.findByTestId('right-primary-text'); + const findRightSecondaryTextSlot = () => wrapper.findByTestId('right-secondary-text'); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + + const mountComponent = (propsData, slots) => { + wrapper = shallowMountExtended(component, { + propsData, + slots: { + 'left-primary-text': '<div data-testid="left-primary-text" />', + 'left-secondary-text': '<div data-testid="left-secondary-text" />', + 'right-primary-text': '<div data-testid="right-primary-text" />', + 'right-secondary-text': '<div data-testid="right-secondary-text" />', + ...slots, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + slotName | finderFunction + ${'left-primary-text'} | ${findLeftPrimaryTextSlot} + ${'left-secondary-text'} | ${findLeftSecondaryTextSlot} + ${'right-primary-text'} | ${findRightPrimaryTextSlot} + ${'right-secondary-text'} | ${findRightSecondaryTextSlot} + `('$slotName slot', ({ finderFunction, slotName }) => { + it('exist when the slot is filled', () => { + mountComponent(); + + expect(finderFunction().exists()).toBe(true); + }); + + it('does not exist when the slot is empty', () => { + mountComponent({}, { [slotName]: '' }); + + expect(finderFunction().exists()).toBe(false); + }); + }); + + it('should show a skeleton loader component', () => { + mountComponent({ loading: true }); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('should not show a skeleton loader component', () => { + mountComponent(); + + expect(findSkeletonLoader().exists()).toBe(false); + }); +}); 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 3329199a46b..a54f3450633 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 @@ -1,11 +1,22 @@ import { GlSkeletonLoader, GlIcon } from '@gitlab/ui'; +import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { followUser, unfollowUser } from '~/api/user_api'; + +jest.mock('~/flash'); +jest.mock('~/api/user_api', () => ({ + followUser: jest.fn(), + unfollowUser: jest.fn(), +})); const DEFAULT_PROPS = { user: { + id: 1, username: 'root', name: 'Administrator', location: 'Vienna', @@ -15,6 +26,7 @@ const DEFAULT_PROPS = { workInformation: null, status: null, pronouns: 'they/them', + isFollowed: false, loaded: true, }, }; @@ -25,11 +37,13 @@ describe('User Popover Component', () => { let wrapper; beforeEach(() => { - loadFixtures(fixtureTemplate); + loadHTMLFixture(fixtureTemplate); + gon.features = {}; }); afterEach(() => { wrapper.destroy(); + resetHTMLFixture(); }); const findUserStatus = () => wrapper.findByTestId('user-popover-status'); @@ -37,15 +51,15 @@ describe('User Popover Component', () => { const findUserName = () => wrapper.find(UserNameWithStatus); const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link'); const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time'); + const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button'); - const createWrapper = (props = {}, options = {}) => { + const createWrapper = (props = {}) => { wrapper = mountExtended(UserPopover, { propsData: { ...DEFAULT_PROPS, target: findTarget(), ...props, }, - ...options, }); }; @@ -289,4 +303,124 @@ describe('User Popover Component', () => { expect(findUserLocalTime().exists()).toBe(false); }); }); + + describe("when current user doesn't follow the user", () => { + beforeEach(() => createWrapper()); + + it('renders the Follow button with the correct variant', () => { + expect(findToggleFollowButton().text()).toBe('Follow'); + expect(findToggleFollowButton().props('variant')).toBe('confirm'); + }); + + describe('when clicking', () => { + it('follows the user', async () => { + followUser.mockResolvedValue({}); + + await findToggleFollowButton().trigger('click'); + + expect(findToggleFollowButton().props('loading')).toBe(true); + + await axios.waitForAll(); + + expect(wrapper.emitted().follow.length).toBe(1); + expect(wrapper.emitted().unfollow).toBeFalsy(); + }); + + describe('when an error occurs', () => { + beforeEach(() => { + followUser.mockRejectedValue({}); + + findToggleFollowButton().trigger('click'); + }); + + it('shows an error message', async () => { + await axios.waitForAll(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred while trying to follow this user, please try again.', + error: {}, + captureError: true, + }); + }); + + it('emits no events', async () => { + await axios.waitForAll(); + + expect(wrapper.emitted().follow).toBe(undefined); + expect(wrapper.emitted().unfollow).toBe(undefined); + }); + }); + }); + }); + + describe('when current user follows the user', () => { + beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } })); + + it('renders the Unfollow button with the correct variant', () => { + expect(findToggleFollowButton().text()).toBe('Unfollow'); + expect(findToggleFollowButton().props('variant')).toBe('default'); + }); + + describe('when clicking', () => { + it('unfollows the user', async () => { + unfollowUser.mockResolvedValue({}); + + findToggleFollowButton().trigger('click'); + + await axios.waitForAll(); + + expect(wrapper.emitted().follow).toBe(undefined); + expect(wrapper.emitted().unfollow.length).toBe(1); + }); + + describe('when an error occurs', () => { + beforeEach(async () => { + unfollowUser.mockRejectedValue({}); + + findToggleFollowButton().trigger('click'); + + await axios.waitForAll(); + }); + + it('shows an error message', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred while trying to unfollow this user, please try again.', + error: {}, + captureError: true, + }); + }); + + it('emits no events', () => { + expect(wrapper.emitted().follow).toBe(undefined); + expect(wrapper.emitted().unfollow).toBe(undefined); + }); + }); + }); + }); + + describe('when the current user is the user', () => { + beforeEach(() => { + gon.current_username = DEFAULT_PROPS.user.username; + createWrapper(); + }); + + it("doesn't render the toggle follow button", () => { + expect(findToggleFollowButton().exists()).toBe(false); + }); + }); + + describe('when API does not support `isFollowed`', () => { + beforeEach(() => { + const user = { + ...DEFAULT_PROPS.user, + isFollowed: undefined, + }; + + createWrapper({ user }); + }); + + it('does not render the toggle follow button', () => { + expect(findToggleFollowButton().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js index 59ce9f086c3..d052c99ec0e 100644 --- a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js +++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js @@ -1,3 +1,4 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; /** @@ -10,10 +11,14 @@ describe('AutofocusOnShow directive', () => { let el; beforeEach(() => { - setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>'); + setHTMLFixture('<div id="container" style="display: none;"><input id="inputel"/></div>'); el = document.querySelector('#inputel'); }); + afterEach(() => { + resetHTMLFixture(); + }); + it('should bind IntersectionObserver on input element', () => { jest.spyOn(el, 'focus').mockImplementation(() => {}); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js index 7dfeced571a..a25f92c9cf2 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import IssuableBulkEditSidebar from '~/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue'; const createComponent = ({ expanded = true } = {}) => @@ -22,12 +23,13 @@ describe('IssuableBulkEditSidebar', () => { let wrapper; beforeEach(() => { - setFixtures('<div class="layout-page right-sidebar-collapsed"></div>'); + setHTMLFixture('<div class="layout-page right-sidebar-collapsed"></div>'); wrapper = createComponent(); }); afterEach(() => { wrapper.destroy(); + resetHTMLFixture(); }); describe('watch', () => { diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index b79dc0bf976..d3e484cf913 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -36,7 +36,6 @@ describe('IssuableEditForm', () => { beforeEach(() => { wrapper = createComponent(); - gon.features = { markdownContinueLists: true }; }); afterEach(() => { diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index 1cdd709159f..544db891a13 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -1,8 +1,6 @@ import { GlIcon, GlAvatarLabeled } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; - +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; import { mockIssuableShowProps, mockIssuable } from '../mock_data'; @@ -12,10 +10,17 @@ const issuableHeaderProps = { ...mockIssuableShowProps, }; -const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) => - extendedWrapper( - shallowMount(IssuableHeader, { - propsData, +describe('IssuableHeader', () => { + let wrapper; + + const findTaskStatusEl = () => wrapper.findByTestId('task-status'); + + const createComponent = (props = {}, { stubs } = {}) => { + wrapper = shallowMountExtended(IssuableHeader, { + propsData: { + ...issuableHeaderProps, + ...props, + }, slots: { 'status-badge': 'Open', 'header-actions': ` @@ -24,23 +29,18 @@ const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) => `, }, stubs, - }), - ); - -describe('IssuableHeader', () => { - let wrapper; - - beforeEach(() => { - wrapper = createComponent(); - }); + }); + }; afterEach(() => { wrapper.destroy(); + resetHTMLFixture(); }); describe('computed', () => { describe('authorId', () => { it('returns numeric ID from GraphQL ID of `author` prop', () => { + createComponent(); expect(wrapper.vm.authorId).toBe(1); }); }); @@ -48,10 +48,11 @@ describe('IssuableHeader', () => { describe('handleRightSidebarToggleClick', () => { beforeEach(() => { - setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>'); + setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>'); }); it('dispatches `click` event on sidebar toggle button', () => { + createComponent(); wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn); @@ -67,20 +68,21 @@ describe('IssuableHeader', () => { describe('template', () => { it('renders issuable status icon and text', () => { + createComponent(); const statusBoxEl = wrapper.findByTestId('status'); + const statusIconEl = statusBoxEl.findComponent(GlIcon); expect(statusBoxEl.exists()).toBe(true); - expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass); expect(statusBoxEl.text()).toContain('Open'); }); it('renders blocked icon when issuable is blocked', async () => { - wrapper.setProps({ + createComponent({ blocked: true, }); - await nextTick(); - const blockedEl = wrapper.findByTestId('blocked'); expect(blockedEl.exists()).toBe(true); @@ -88,12 +90,10 @@ describe('IssuableHeader', () => { }); it('renders confidential icon when issuable is confidential', async () => { - wrapper.setProps({ + createComponent({ confidential: true, }); - await nextTick(); - const confidentialEl = wrapper.findByTestId('confidential'); expect(confidentialEl.exists()).toBe(true); @@ -101,6 +101,7 @@ describe('IssuableHeader', () => { }); it('renders issuable author avatar', () => { + createComponent(); const { username, name, webUrl, avatarUrl } = mockIssuable.author; const avatarElAttrs = { 'data-user-id': '1', @@ -120,28 +121,26 @@ describe('IssuableHeader', () => { expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false); }); - it('renders tast status text when `taskCompletionStatus` prop is defined', () => { - let taskStatusEl = wrapper.findByTestId('task-status'); + it('renders task status text when `taskCompletionStatus` prop is defined', () => { + createComponent(); - expect(taskStatusEl.exists()).toBe(true); - expect(taskStatusEl.text()).toContain('0 of 5 tasks completed'); + expect(findTaskStatusEl().exists()).toBe(true); + expect(findTaskStatusEl().text()).toContain('0 of 5 tasks completed'); + }); - const wrapperSingleTask = createComponent({ - ...issuableHeaderProps, + it('does not render task status text when tasks count is 0', () => { + createComponent({ taskCompletionStatus: { + count: 0, completedCount: 0, - count: 1, }, }); - taskStatusEl = wrapperSingleTask.findByTestId('task-status'); - - expect(taskStatusEl.text()).toContain('0 of 1 task completed'); - - wrapperSingleTask.destroy(); + expect(findTaskStatusEl().exists()).toBe(false); }); it('renders sidebar toggle button', () => { + createComponent(); const toggleButtonEl = wrapper.findByTestId('sidebar-toggle'); expect(toggleButtonEl.exists()).toBe(true); @@ -149,6 +148,7 @@ describe('IssuableHeader', () => { }); it('renders header actions', () => { + createComponent(); const actionsEl = wrapper.findByTestId('header-actions'); expect(actionsEl.find('button.js-close').exists()).toBe(true); @@ -157,9 +157,8 @@ describe('IssuableHeader', () => { describe('when author exists outside of GitLab', () => { it("renders 'external-link' icon in avatar label", () => { - wrapper = createComponent( + createComponent( { - ...issuableHeaderProps, author: { ...issuableHeaderProps.author, webUrl: 'https://jira.com/test-user/author.jpg', diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js index d1eb1366225..8b027f990a2 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js @@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => { const { statusBadgeClass, statusIcon, + statusIconClass, enableEdit, enableAutocomplete, editFormVisible, @@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => { descriptionHelpPath, taskCompletionStatus, } = mockIssuableShowProps; - const { blocked, confidential, createdAt, author } = mockIssuable; + const { state, blocked, confidential, createdAt, author } = mockIssuable; it('renders component container element with class `issuable-show-container`', () => { expect(wrapper.classes()).toContain('issuable-show-container'); @@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => { expect(issuableHeader.exists()).toBe(true); expect(issuableHeader.props()).toMatchObject({ + issuableState: state, statusBadgeClass, statusIcon, + statusIconClass, blocked, confidential, createdAt, author, taskCompletionStatus, }); - expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open'); + expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open'); expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe( true, ); diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js index f5f3ed58655..32bb9edfe08 100644 --- a/spec/frontend/vue_shared/issuable/show/mock_data.js +++ b/spec/frontend/vue_shared/issuable/show/mock_data.js @@ -36,8 +36,9 @@ export const mockIssuableShowProps = { enableTaskList: true, enableEdit: true, showFieldTitle: false, - statusBadgeClass: 'status-box-open', - statusIcon: 'issue-open-m', + statusBadgeClass: 'issuable-status-badge-open', + statusIcon: 'issues', + statusIconClass: 'gl-sm-display-none', taskCompletionStatus: { completedCount: 0, count: 5, diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js index 47bf3c8ed83..6c9e5f85fa0 100644 --- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js +++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js @@ -1,6 +1,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import Cookies from 'js-cookie'; import { nextTick } from 'vue'; +import Cookies from '~/lib/utils/cookies'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import IssuableSidebarRoot from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue'; @@ -9,7 +10,7 @@ import { USER_COLLAPSED_GUTTER_COOKIE } from '~/vue_shared/issuable/sidebar/cons const MOCK_LAYOUT_PAGE_CLASS = 'layout-page'; const createComponent = () => { - setFixtures(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`); + setHTMLFixture(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`); return shallowMountExtended(IssuableSidebarRoot, { slots: { @@ -38,6 +39,7 @@ describe('IssuableSidebarRoot', () => { afterEach(() => { wrapper.destroy(); + resetHTMLFixture(); }); describe('when sidebar is expanded', () => { diff --git a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js new file mode 100644 index 00000000000..136fe74b0d6 --- /dev/null +++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js @@ -0,0 +1,58 @@ +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue'; +import SectionLoader from '~/vue_shared/security_configuration/components/section_loader.vue'; + +describe('Section Layout component', () => { + let wrapper; + + const createComponent = (propsData) => { + wrapper = extendedWrapper( + mount(SectionLayout, { + propsData, + scopedSlots: { + description: '<span>foo</span>', + features: '<span>bar</span>', + }, + }), + ); + }; + + const findHeading = () => wrapper.find('h2'); + const findLoader = () => wrapper.findComponent(SectionLoader); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('basic structure', () => { + beforeEach(() => { + createComponent({ heading: 'testheading' }); + }); + + const slots = { + description: 'foo', + features: 'bar', + }; + + it('should render heading when passed in as props', () => { + expect(findHeading().exists()).toBe(true); + expect(findHeading().text()).toBe('testheading'); + }); + + Object.keys(slots).forEach((slot) => { + it('renders the slots', () => { + const slotContent = slots[slot]; + createComponent({ heading: '' }); + expect(wrapper.text()).toContain(slotContent); + }); + }); + }); + + describe('loading state', () => { + it('should show loaders when loading', () => { + createComponent({ heading: 'testheading', isLoading: true }); + expect(findLoader().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js index dac9accbbf5..a9ad675e538 100644 --- a/spec/frontend/vue_shared/security_reports/mock_data.js +++ b/spec/frontend/vue_shared/security_reports/mock_data.js @@ -62,7 +62,7 @@ export const mockFindings = [ report_type: 'dependency_scanning', name: '3rd party CORS request may execute in jquery', severity: 'high', - scanner: { external_id: 'retire.js', name: 'Retire.js' }, + scanner: { external_id: 'gemnasium', name: 'gemnasium' }, identifiers: [ { external_type: 'cve', @@ -145,7 +145,7 @@ export const mockFindings = [ name: 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery', severity: 'low', - scanner: { external_id: 'retire.js', name: 'Retire.js' }, + scanner: { external_id: 'gemnasium', name: 'gemnasium' }, identifiers: [ { external_type: 'cve', @@ -227,7 +227,7 @@ export const mockFindings = [ name: 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery', severity: 'low', - scanner: { external_id: 'retire.js', name: 'Retire.js' }, + scanner: { external_id: 'gemnasium', name: 'gemnasium' }, identifiers: [ { external_type: 'cve', |