diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /spec/frontend/vue_shared/components | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared/components')
28 files changed, 595 insertions, 148 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); + }); + }); }); |