diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 12:16:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 12:16:11 +0300 |
commit | edaa33dee2ff2f7ea3fac488d41558eb5f86d68c (patch) | |
tree | 11f143effbfeba52329fb7afbd05e6e2a3790241 /spec/frontend/vue_shared | |
parent | d8a5691316400a0f7ec4f83832698f1988eb27c1 (diff) |
Add latest changes from gitlab-org/gitlab@14-7-stable-eev14.7.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared')
29 files changed, 551 insertions, 61 deletions
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index 1fc655f1ebc..221beed744b 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -349,6 +349,8 @@ describe('AlertDetails', () => { ${1} | ${'metrics'} ${2} | ${'activity'} `('will navigate to the correct tab via $tabId', ({ index, tabId }) => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentTabIndex: index }); expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } }); }); diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js index 9ae45071f45..29e0eee2c9a 100644 --- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js @@ -109,6 +109,8 @@ describe('Alert Details Sidebar Assignees', () => { }); it('renders a unassigned option', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ isDropdownSearching: false }); await wrapper.vm.$nextTick(); expect(findDropdown().text()).toBe('Unassigned'); @@ -120,6 +122,8 @@ describe('Alert Details Sidebar Assignees', () => { it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ isDropdownSearching: false }); await wrapper.vm.$nextTick(); @@ -136,6 +140,8 @@ describe('Alert Details Sidebar Assignees', () => { }); it('emits an error when request contains error messages', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ isDropdownSearching: false }); const errorMutationResult = { data: { diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js index 530d01402c6..083a5f60d1d 100644 --- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js +++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js @@ -315,6 +315,8 @@ describe('vue_shared/components/chronic_duration_input', () => { }); it('passes updated prop via v-model', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ value: MOCK_VALUE }); await wrapper.vm.$nextTick(); diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js index 33445923a49..fca5e664a96 100644 --- a/spec/frontend/vue_shared/components/clipboard_button_spec.js +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -1,8 +1,16 @@ import { GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import initCopyToClipboard from '~/behaviors/copy_to_clipboard'; +import { nextTick } from 'vue'; + +import initCopyToClipboard, { + CLIPBOARD_SUCCESS_EVENT, + CLIPBOARD_ERROR_EVENT, + I18N_ERROR_MESSAGE, +} from '~/behaviors/copy_to_clipboard'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1)); + describe('clipboard button', () => { let wrapper; @@ -15,6 +23,42 @@ describe('clipboard button', () => { const findButton = () => wrapper.find(GlButton); + const expectConfirmationTooltip = async ({ event, message }) => { + const title = 'Copy this value'; + + createWrapper({ + text: 'copy me', + title, + }); + + wrapper.vm.$root.$emit = jest.fn(); + + const button = findButton(); + + expect(button.attributes()).toMatchObject({ + title, + 'aria-label': title, + }); + + await button.trigger(event); + + expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1'); + + expect(button.attributes()).toMatchObject({ + title: message, + 'aria-label': message, + }); + + jest.runAllTimers(); + await nextTick(); + + expect(button.attributes()).toMatchObject({ + title, + 'aria-label': title, + }); + expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1'); + }; + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -99,6 +143,32 @@ describe('clipboard button', () => { expect(findButton().props('variant')).toBe(variant); }); + describe('confirmation tooltip', () => { + it('adds `id` and `data-clipboard-handle-tooltip` attributes to button', () => { + createWrapper({ + text: 'copy me', + title: 'Copy this value', + }); + + expect(findButton().attributes()).toMatchObject({ + id: 'clipboard-button-1', + 'data-clipboard-handle-tooltip': 'false', + 'aria-live': 'polite', + }); + }); + + it('shows success tooltip after successful copy', () => { + expectConfirmationTooltip({ + event: CLIPBOARD_SUCCESS_EVENT, + message: ClipboardButton.i18n.copied, + }); + }); + + it('shows error tooltip after failed copy', () => { + expectConfirmationTooltip({ event: CLIPBOARD_ERROR_EVENT, message: I18N_ERROR_MESSAGE }); + }); + }); + describe('integration', () => { it('actually copies to clipboard', () => { initCopyToClipboard(); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js index af7f85769aa..a179afccae0 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js @@ -10,6 +10,7 @@ describe('Confirm Danger Modal', () => { const phrase = 'En Taro Adun'; const buttonText = 'Click me!'; const buttonClass = 'gl-w-full'; + const buttonVariant = 'info'; const modalId = CONFIRM_DANGER_MODAL_ID; const findBtn = () => wrapper.findComponent(GlButton); @@ -21,6 +22,7 @@ describe('Confirm Danger Modal', () => { propsData: { buttonText, buttonClass, + buttonVariant, phrase, ...props, }, @@ -57,6 +59,10 @@ describe('Confirm Danger Modal', () => { expect(findBtn().classes()).toContain(buttonClass); }); + it('passes `buttonVariant` prop to button', () => { + expect(findBtn().attributes('variant')).toBe(buttonVariant); + }); + it('will emit `confirm` when the modal confirms', () => { expect(wrapper.emitted('confirm')).toBeUndefined(); 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 64d15884333..4e9eac2dde2 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 @@ -122,6 +122,8 @@ describe('FilteredSearchBarRoot', () => { describe('sortDirectionIcon', () => { it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortDirection: SortDirection.ascending, }); @@ -130,6 +132,8 @@ describe('FilteredSearchBarRoot', () => { }); it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortDirection: SortDirection.descending, }); @@ -140,6 +144,8 @@ describe('FilteredSearchBarRoot', () => { describe('sortDirectionTooltip', () => { it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortDirection: SortDirection.ascending, }); @@ -148,6 +154,8 @@ describe('FilteredSearchBarRoot', () => { }); it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortDirection: SortDirection.descending, }); @@ -158,6 +166,8 @@ describe('FilteredSearchBarRoot', () => { describe('filteredRecentSearches', () => { it('returns array of recent searches filtering out any string type (unsupported) items', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ recentSearches: [{ foo: 'bar' }, 'foo'], }); @@ -169,6 +179,8 @@ describe('FilteredSearchBarRoot', () => { }); it('returns array of recent searches sanitizing any duplicate token values', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ recentSearches: [ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel], @@ -198,6 +210,8 @@ describe('FilteredSearchBarRoot', () => { describe('filterValue', () => { it('emits component event `onFilter` with empty array and false when filter was never selected', () => { wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ initialRender: false, filterValue: [tokenValueEmpty], @@ -210,6 +224,8 @@ describe('FilteredSearchBarRoot', () => { it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => { wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ initialRender: false, filterValue: [tokenValueEmpty], @@ -264,6 +280,8 @@ describe('FilteredSearchBarRoot', () => { describe('handleSortDirectionClick', () => { beforeEach(() => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortOption: mockSortOptions[0], }); @@ -312,6 +330,8 @@ describe('FilteredSearchBarRoot', () => { const mockFilters = [tokenValueAuthor, 'foo']; beforeEach(async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ filterValue: mockFilters, }); @@ -376,6 +396,8 @@ describe('FilteredSearchBarRoot', () => { describe('template', () => { beforeEach(() => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedSortOption: mockSortOptions[0], selectedSortDirection: SortDirection.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 b29c394e7ae..5865c6a41b8 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 @@ -10,10 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { - DEFAULT_LABEL_ANY, - DEFAULT_NONE_ANY, -} from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -227,6 +224,8 @@ describe('AuthorToken', () => { expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ authors: [ { @@ -274,7 +273,7 @@ describe('AuthorToken', () => { expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); }); - it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => { + it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => { wrapper = createComponent({ active: true, config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors }, @@ -285,8 +284,9 @@ describe('AuthorToken', () => { const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); - expect(suggestions).toHaveLength(1 + currentUserLength); - expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.text); + expect(suggestions).toHaveLength(2 + currentUserLength); + expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text); + expect(suggestions.at(1).text()).toBe(DEFAULT_NONE_ANY[1].text); }); it('emits listeners in the base-token', () => { 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 f3e8b2d0c1b..cd8be765fb5 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 @@ -121,6 +121,8 @@ describe('BranchToken', () => { beforeEach(async () => { wrapper = createComponent({ value: { data: mockBranches[0].name } }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ branches: mockBranches, }); 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 36071c900df..ed9ac7c271e 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 @@ -123,6 +123,8 @@ describe('EmojiToken', () => { value: { data: `"${mockEmojis[0].name}"` }, }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ emojis: mockEmojis, }); 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 f55fb2836e3..b9af71ad8a7 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 @@ -144,6 +144,8 @@ describe('LabelToken', () => { beforeEach(async () => { wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ labels: mockLabels, }); 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 4a098db33c5..c0d8b5fd139 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 @@ -121,6 +121,8 @@ describe('MilestoneToken', () => { beforeEach(async () => { wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ milestones: mockMilestones, }); diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js new file mode 100644 index 00000000000..b673e5407d4 --- /dev/null +++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js @@ -0,0 +1,77 @@ +import { GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import flushPromises from 'helpers/flush_promises'; +import axios from '~/lib/utils/axios_utils'; +import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue'; + +describe('GitlabVersionCheck', () => { + let wrapper; + let mock; + + const defaultResponse = { + code: 200, + res: { severity: 'success' }, + }; + + const createComponent = (mockResponse) => { + const response = { + ...defaultResponse, + ...mockResponse, + }; + + mock = new MockAdapter(axios); + mock.onGet().replyOnce(response.code, response.res); + + wrapper = shallowMount(GitlabVersionCheck); + }; + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + const findGlBadge = () => wrapper.findComponent(GlBadge); + + describe('template', () => { + describe.each` + description | mockResponse | renders + ${'successful but null'} | ${{ code: 200, res: null }} | ${false} + ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true} + ${'an error'} | ${{ code: 500, res: null }} | ${false} + `('version_check.json response', ({ description, mockResponse, renders }) => { + describe(`is ${description}`, () => { + beforeEach(async () => { + createComponent(mockResponse); + await flushPromises(); // Ensure we wrap up the axios call + }); + + it(`does${renders ? '' : ' not'} render GlBadge`, () => { + expect(findGlBadge().exists()).toBe(renders); + }); + }); + }); + + describe.each` + mockResponse | expectedUI + ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }} + ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }} + ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }} + `('badge ui', ({ mockResponse, expectedUI }) => { + describe(`when response is ${mockResponse.res.severity}`, () => { + beforeEach(async () => { + createComponent(mockResponse); + await flushPromises(); // Ensure we wrap up the axios call + }); + + it(`title is ${expectedUI.title}`, () => { + expect(findGlBadge().text()).toBe(expectedUI.title); + }); + + it(`variant is ${expectedUI.variant}`, () => { + expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant); + }); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js index 5bedd0ccd02..38c26226863 100644 --- a/spec/frontend/vue_shared/components/line_numbers_spec.js +++ b/spec/frontend/vue_shared/components/line_numbers_spec.js @@ -13,7 +13,6 @@ describe('Line Numbers component', () => { const findGlIcon = () => wrapper.findComponent(GlIcon); const findLineNumbers = () => wrapper.findAllComponents(GlLink); const findFirstLineNumber = () => findLineNumbers().at(0); - const findSecondLineNumber = () => findLineNumbers().at(1); beforeEach(() => createComponent()); @@ -24,7 +23,7 @@ describe('Line Numbers component', () => { expect(findLineNumbers().length).toBe(lines); expect(findFirstLineNumber().attributes()).toMatchObject({ id: 'L1', - href: '#L1', + to: '#LC1', }); }); @@ -35,37 +34,4 @@ describe('Line Numbers component', () => { }); }); }); - - describe('clicking a line number', () => { - let firstLineNumber; - let firstLineNumberElement; - - beforeEach(() => { - firstLineNumber = findFirstLineNumber(); - firstLineNumberElement = firstLineNumber.element; - - jest.spyOn(firstLineNumberElement, 'scrollIntoView'); - jest.spyOn(firstLineNumberElement.classList, 'add'); - jest.spyOn(firstLineNumberElement.classList, 'remove'); - - firstLineNumber.vm.$emit('click'); - }); - - it('adds the highlight (hll) class', () => { - expect(firstLineNumberElement.classList.add).toHaveBeenCalledWith('hll'); - }); - - it('removes the highlight (hll) class from a previously highlighted line', () => { - findSecondLineNumber().vm.$emit('click'); - - expect(firstLineNumberElement.classList.remove).toHaveBeenCalledWith('hll'); - }); - - it('scrolls the line into view', () => { - expect(firstLineNumberElement.scrollIntoView).toHaveBeenCalledWith({ - behavior: 'smooth', - block: 'center', - }); - }); - }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 76e1a1162ad..0d90ca7f1f6 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; @@ -242,6 +243,41 @@ describe('Markdown field component', () => { expect(dropzoneSpy).toHaveBeenCalled(); }); + + describe('mentioning all users', () => { + const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`); + + it('shows warning on mention of all users', async () => { + axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } }); + + subject.setProps({ textareaValue: 'hello @all' }); + + await axios.waitFor(markdownPreviewPath).then(() => { + expect(subject.text()).toContain( + 'You are about to add 11 people to the discussion. They will all receive a notification.', + ); + }); + }); + + it('removes warning when all mention is removed', async () => { + axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } }); + + subject.setProps({ textareaValue: 'hello @all' }); + + await axios.waitFor(markdownPreviewPath); + + jest.spyOn(axios, 'post'); + + subject.setProps({ textareaValue: 'hello @allan' }); + + await nextTick(); + + expect(axios.post).not.toHaveBeenCalled(); + expect(subject.text()).not.toContain( + 'You are about to add 11 people to the discussion. They will all receive a notification.', + ); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js index acf97713885..b330b4f5657 100644 --- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js +++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js @@ -313,6 +313,8 @@ describe('AlertManagementEmptyState', () => { it('returns correctly applied filter search values', async () => { const searchTerm = 'foo'; + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchTerm, }); @@ -330,6 +332,8 @@ describe('AlertManagementEmptyState', () => { }); it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ authorUsername: 'foo', searchTerm: 'bar', diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap index 23cf6ef9785..e8d76991b90 100644 --- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap @@ -3,7 +3,7 @@ exports[`Package code instruction multiline to match the snapshot 1`] = ` <div> <label - for="instruction-input_3" + for="instruction-input_1" > foo_label </label> @@ -23,7 +23,7 @@ multiline text exports[`Package code instruction single line to match the default snapshot 1`] = ` <div> <label - for="instruction-input_2" + for="instruction-input_1" > foo_label </label> @@ -37,7 +37,7 @@ exports[`Package code instruction single line to match the default snapshot 1`] <input class="form-control gl-font-monospace" data-testid="instruction-input" - id="instruction-input_2" + id="instruction-input_1" readonly="readonly" type="text" /> @@ -47,9 +47,12 @@ exports[`Package code instruction single line to match the default snapshot 1`] data-testid="instruction-button" > <button - aria-label="Copy this value" + aria-label="Copy npm install command" + aria-live="polite" class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" + data-clipboard-handle-tooltip="false" data-clipboard-text="npm i @my-package" + id="clipboard-button-1" title="Copy npm install command" type="button" > diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js index 4ec608aaf07..3a2ea263a05 100644 --- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js +++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js @@ -3,6 +3,8 @@ import Tracking from '~/tracking'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1)); + describe('Package code instruction', () => { let wrapper; diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js index a5a099d803a..5336ecc614c 100644 --- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js @@ -68,6 +68,8 @@ describe('IssuableMoveDropdown', () => { describe('searchKey', () => { it('calls `fetchProjects` with value of the prop', async () => { jest.spyOn(wrapper.vm, 'fetchProjects'); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'foo', }); @@ -143,6 +145,8 @@ describe('IssuableMoveDropdown', () => { `( 'returns $returnValue when selectedProject and provided project param $title', async ({ project, selectedProject, returnValue }) => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedProject, }); @@ -154,6 +158,8 @@ describe('IssuableMoveDropdown', () => { ); it('returns false when selectedProject is null', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedProject: null, }); @@ -206,6 +212,8 @@ describe('IssuableMoveDropdown', () => { }); it('renders gl-loading-icon component when projectsListLoading prop is true', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projectsListLoading: true, }); @@ -216,6 +224,8 @@ describe('IssuableMoveDropdown', () => { }); it('renders gl-dropdown-item components for available projects', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projects: mockProjects, selectedProject: mockProjects[0], @@ -234,6 +244,8 @@ describe('IssuableMoveDropdown', () => { }); it('renders string "No matching results" when search does not yield any matches', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'foo', }); @@ -241,6 +253,8 @@ describe('IssuableMoveDropdown', () => { // Wait for `searchKey` watcher to run. await wrapper.vm.$nextTick(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projects: [], projectsListLoading: false, @@ -254,6 +268,8 @@ describe('IssuableMoveDropdown', () => { }); it('renders string "Failed to load projects" when loading projects list fails', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projects: [], projectsListLoading: false, @@ -273,6 +289,8 @@ describe('IssuableMoveDropdown', () => { expect(moveButtonEl.text()).toBe('Move'); expect(moveButtonEl.attributes('disabled')).toBe('true'); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedProject: mockProjects[0], }); @@ -303,6 +321,8 @@ describe('IssuableMoveDropdown', () => { }); it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projectItemClick: true, }); @@ -326,6 +346,8 @@ describe('IssuableMoveDropdown', () => { }); it('sets project for clicked gl-dropdown-item to selectedProject', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ projects: mockProjects, }); @@ -338,6 +360,8 @@ describe('IssuableMoveDropdown', () => { }); it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedProject: mockProjects[0], }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js index 1fe85637a62..0eff6a1dace 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js @@ -43,6 +43,8 @@ describe('DropdownContentsCreateView', () => { }); it('returns `true` when `labelCreateInProgress` is true', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ labelTitle: 'Foo', selectedColor: '#ff0000', @@ -55,6 +57,8 @@ describe('DropdownContentsCreateView', () => { }); it('returns `false` when label title and color is defined and create request is not already in progress', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ labelTitle: 'Foo', selectedColor: '#ff0000', @@ -99,6 +103,8 @@ describe('DropdownContentsCreateView', () => { describe('handleCreateClick', () => { it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => { jest.spyOn(wrapper.vm, 'createLabel').mockImplementation(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ labelTitle: 'Foo', selectedColor: '#ff0000', @@ -164,6 +170,8 @@ describe('DropdownContentsCreateView', () => { }); it('renders color input element', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ selectedColor: '#ff0000', }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 80b8edd28ba..93a0e2f75bb 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -63,6 +63,8 @@ describe('DropdownContentsLabelsView', () => { describe('computed', () => { describe('visibleLabels', () => { it('returns matching labels filtered with `searchKey`', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'bug', }); @@ -72,6 +74,8 @@ describe('DropdownContentsLabelsView', () => { }); it('returns matching labels with fuzzy filtering', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'bg', }); @@ -82,6 +86,8 @@ describe('DropdownContentsLabelsView', () => { }); it('returns all labels when `searchKey` is empty', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: '', }); @@ -100,6 +106,8 @@ describe('DropdownContentsLabelsView', () => { `( 'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription', async ({ searchKey, labels, returnValue }) => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey, }); @@ -161,6 +169,8 @@ describe('DropdownContentsLabelsView', () => { describe('handleKeyDown', () => { it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); @@ -173,6 +183,8 @@ describe('DropdownContentsLabelsView', () => { }); it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); @@ -185,6 +197,8 @@ describe('DropdownContentsLabelsView', () => { }); it('resets the search text when the Enter key is pressed', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, searchKey: 'bug', @@ -201,6 +215,8 @@ describe('DropdownContentsLabelsView', () => { it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => { jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 2, }); @@ -220,6 +236,8 @@ describe('DropdownContentsLabelsView', () => { it('calls action `toggleDropdownContents` when Esc key is pressed', () => { jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); @@ -233,6 +251,8 @@ describe('DropdownContentsLabelsView', () => { it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => { jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); @@ -320,6 +340,8 @@ describe('DropdownContentsLabelsView', () => { }); it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 0, }); @@ -332,6 +354,8 @@ describe('DropdownContentsLabelsView', () => { }); it('renders element containing "No matching results" when `searchKey` does not match with any label', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'abc', }); 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 d8491334b5d..3ceed670d77 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 @@ -1,4 +1,4 @@ -import { GlLoadingIcon, GlLink } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -9,6 +9,7 @@ import { workspaceLabelsQueries } from '~/sidebar/constants'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql'; import { + mockRegularLabel, mockSuggestedColors, createLabelSuccessfulResponse, workspaceLabelsQueryResponse, @@ -25,8 +26,18 @@ const userRecoverableError = { errors: ['Houston, we have a problem'], }; +const titleTakenError = { + data: { + labelCreate: { + label: mockRegularLabel, + errors: ['Title has already been taken'], + }, + }, +}; + const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse); const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError); +const createLabelDuplicateErrorHandler = jest.fn().mockResolvedValue(titleTakenError); const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); describe('DropdownContentsCreateView', () => { @@ -208,4 +219,17 @@ describe('DropdownContentsCreateView', () => { expect(createFlash).toHaveBeenCalled(); }); + + it('displays error in alert if label title is already taken', async () => { + createComponent({ mutationHandler: createLabelDuplicateErrorHandler }); + fillLabelAttributes(); + await nextTick(); + + findCreateButton().vm.$emit('click'); + await waitForPromises(); + + expect(wrapper.findComponent(GlAlert).text()).toEqual( + titleTakenError.data.labelCreate.errors[0], + ); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js index 6f5a4b7e613..7f6770e0bea 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -110,6 +110,19 @@ describe('DropdownContentsLabelsView', () => { }); }); + it('first item is highlighted when search is not empty', async () => { + createComponent({ + queryHandler: jest.fn().mockResolvedValue(workspaceLabelsQueryResponse), + searchKey: 'Label', + }); + await makeObserverAppear(); + await waitForPromises(); + await nextTick(); + + expect(findLabelsList().exists()).toBe(true); + expect(findFirstLabel().attributes('active')).toBe('true'); + }); + it('when search returns 0 results', async () => { createComponent({ queryHandler: jest.fn().mockResolvedValue({ diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer_spec.js index 758068379de..094d8d42a47 100644 --- a/spec/frontend/vue_shared/components/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer_spec.js @@ -1,27 +1,35 @@ import hljs from 'highlight.js/lib/core'; +import Vue, { nextTick } from 'vue'; +import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer.vue'; import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; jest.mock('highlight.js/lib/core'); +Vue.use(VueRouter); +const router = new VueRouter(); describe('Source Viewer component', () => { let wrapper; const content = `// Some source code`; - const highlightedContent = `<span data-testid='test-highlighted'>${content}</span>`; + const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; const language = 'javascript'; hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); const createComponent = async (props = {}) => { - wrapper = shallowMountExtended(SourceViewer, { propsData: { content, language, ...props } }); + wrapper = shallowMountExtended(SourceViewer, { + router, + propsData: { content, language, ...props }, + }); await waitForPromises(); }; const findLineNumbers = () => wrapper.findComponent(LineNumbers); const findHighlightedContent = () => wrapper.findByTestId('test-highlighted'); + const findFirstLine = () => wrapper.find('#LC1'); beforeEach(() => createComponent()); @@ -56,4 +64,39 @@ describe('Source Viewer component', () => { expect(findHighlightedContent().exists()).toBe(true); }); }); + + describe('selecting a line', () => { + let firstLine; + let firstLineElement; + + beforeEach(() => { + firstLine = findFirstLine(); + firstLineElement = firstLine.element; + + jest.spyOn(firstLineElement, 'scrollIntoView'); + jest.spyOn(firstLineElement.classList, 'add'); + jest.spyOn(firstLineElement.classList, 'remove'); + }); + + it('adds the highlight (hll) class', async () => { + wrapper.vm.$router.push('#LC1'); + await nextTick(); + + expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll'); + }); + + it('removes the highlight (hll) class from a previously highlighted line', async () => { + wrapper.vm.$router.push('#LC2'); + await nextTick(); + + expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll'); + }); + + it('scrolls the line into view', () => { + expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'center', + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 92938b2717f..659d93d6597 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -1,11 +1,18 @@ -import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import { nextTick } from 'vue'; + import ActionsButton from '~/vue_shared/components/actions_button.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; + const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/'; const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/'; const TEST_GITPOD_URL = 'https://gitpod.test/'; +const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled'; +const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true'; const ACTION_EDIT = { href: TEST_EDIT_URL, @@ -54,21 +61,31 @@ const ACTION_GITPOD = { }; const ACTION_GITPOD_ENABLE = { ...ACTION_GITPOD, - href: '#modal-enable-gitpod', + href: undefined, handle: expect.any(Function), }; describe('Web IDE link component', () => { let wrapper; - function createComponent(props) { - wrapper = shallowMount(WebIdeLink, { + function createComponent(props, mountFn = shallowMountExtended) { + wrapper = mountFn(WebIdeLink, { propsData: { editUrl: TEST_EDIT_URL, webIdeUrl: TEST_WEB_IDE_URL, gitpodUrl: TEST_GITPOD_URL, ...props, }, + stubs: { + GlModal: stubComponent(GlModal, { + template: ` + <div> + <slot name="modal-title"></slot> + <slot></slot> + <slot name="modal-footer"></slot> + </div>`, + }), + }, }); } @@ -78,6 +95,7 @@ describe('Web IDE link component', () => { const findActionsButton = () => wrapper.find(ActionsButton); const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + const findModal = () => wrapper.findComponent(GlModal); it.each([ { @@ -97,19 +115,68 @@ describe('Web IDE link component', () => { expectedActions: [ACTION_WEB_IDE_CONFIRM_FORK, ACTION_EDIT_CONFIRM_FORK], }, { - props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }, + props: { + showWebIdeButton: false, + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: true, + }, expectedActions: [ACTION_EDIT, ACTION_GITPOD], }, { - props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }, + props: { + showWebIdeButton: false, + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + gitpodEnabled: true, + }, + expectedActions: [ACTION_EDIT], + }, + { + props: { + showWebIdeButton: false, + showGitpodButton: true, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: true, + }, + expectedActions: [ACTION_EDIT], + }, + { + props: { + showWebIdeButton: false, + showGitpodButton: true, + gitpodEnabled: true, + }, + expectedActions: [ACTION_EDIT], + }, + { + props: { + showWebIdeButton: false, + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: false, + }, expectedActions: [ACTION_EDIT, ACTION_GITPOD_ENABLE], }, { - props: { showGitpodButton: true, gitpodEnabled: false }, + props: { + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: false, + }, expectedActions: [ACTION_WEB_IDE, ACTION_EDIT, ACTION_GITPOD_ENABLE], }, { - props: { showEditButton: false, showGitpodButton: true, gitpodText: 'Test Gitpod' }, + props: { + showEditButton: false, + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodText: 'Test Gitpod', + }, expectedActions: [ACTION_WEB_IDE, { ...ACTION_GITPOD_ENABLE, text: 'Test Gitpod' }], }, { @@ -128,6 +195,8 @@ describe('Web IDE link component', () => { showEditButton: false, showWebIdeButton: true, showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, gitpodEnabled: true, }); }); @@ -174,7 +243,7 @@ describe('Web IDE link component', () => { ])( 'emits the correct event when an action handler is called', async ({ props, expectedEventPayload }) => { - createComponent({ ...props, needsToFork: true }); + createComponent({ ...props, needsToFork: true, disableForkModal: true }); findActionsButton().props('actions')[0].handle(); @@ -182,4 +251,72 @@ describe('Web IDE link component', () => { }, ); }); + + describe('when Gitpod is not enabled', () => { + it('renders closed modal to enable Gitpod', () => { + createComponent({ + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: false, + }); + + const modal = findModal(); + + expect(modal.exists()).toBe(true); + expect(modal.props()).toMatchObject({ + visible: false, + modalId: 'enable-gitpod-modal', + size: 'sm', + title: WebIdeLink.i18n.modal.title, + actionCancel: { + text: WebIdeLink.i18n.modal.actionCancelText, + }, + actionPrimary: { + text: WebIdeLink.i18n.modal.actionPrimaryText, + attributes: { + variant: 'confirm', + category: 'primary', + href: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + 'data-method': 'put', + }, + }, + }); + }); + + it('opens modal when `Gitpod` action is clicked', async () => { + const gitpodText = 'Open in Gitpod'; + + createComponent( + { + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: false, + gitpodText, + }, + mountExtended, + ); + + findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); + + await nextTick(); + await wrapper.findByRole('button', { name: gitpodText }).trigger('click'); + + expect(findModal().props('visible')).toBe(true); + }); + }); + + describe('when Gitpod is enabled', () => { + it('does not render modal', () => { + createComponent({ + showGitpodButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: true, + }); + + expect(findModal().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js index d7d7f4edc3f..b3f94d0242a 100644 --- a/spec/frontend/vue_shared/directives/track_event_spec.js +++ b/spec/frontend/vue_shared/directives/track_event_spec.js @@ -38,6 +38,8 @@ describe('Error Tracking directive', () => { label: 'Trackable Info', }; + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ trackingOptions }); const { category, action, label, property, value } = trackingOptions; diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index 5979a65e3cd..14e93108447 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -98,6 +98,8 @@ describe('IssuableListRoot', () => { await wrapper.vm.$nextTick(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ checkedIssuables, }); @@ -111,6 +113,8 @@ describe('IssuableListRoot', () => { describe('bulkEditIssuables', () => { it('returns array of issuables which have `checked` set to true within checkedIssuables map', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ checkedIssuables: mockCheckedIssuables, }); @@ -180,6 +184,8 @@ describe('IssuableListRoot', () => { describe('issuableChecked', () => { it('returns boolean value representing checked status of issuable item', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ checkedIssuables: { [mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] }, diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js index 8c22b67bdbe..5723e2da586 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js @@ -1,5 +1,6 @@ import { GlTab, GlBadge } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { setLanguage } from 'helpers/locale_helper'; import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue'; @@ -27,10 +28,12 @@ describe('IssuableTabs', () => { let wrapper; beforeEach(() => { + setLanguage('en'); wrapper = createComponent(); }); afterEach(() => { + setLanguage(null); wrapper.destroy(); }); @@ -71,7 +74,7 @@ describe('IssuableTabs', () => { // Does not render `All` badge since it has an undefined count expect(badges).toHaveLength(2); - expect(badges.at(0).text()).toBe(`${mockIssuableListProps.tabCounts.opened}`); + expect(badges.at(0).text()).toBe('5,000'); expect(badges.at(1).text()).toBe(`${mockIssuableListProps.tabCounts.closed}`); }); diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js index e2fa99f7cc9..cfc7937b412 100644 --- a/spec/frontend/vue_shared/issuable/list/mock_data.js +++ b/spec/frontend/vue_shared/issuable/list/mock_data.js @@ -133,7 +133,7 @@ export const mockTabs = [ ]; export const mockTabCounts = { - opened: 5, + opened: 5000, closed: 0, all: undefined, }; diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js index 1fcf37a0477..cb418371760 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js @@ -84,6 +84,8 @@ describe('IssuableTitle', () => { }); it('renders sticky header when `stickyTitleVisible` prop is true', async () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax wrapper.setData({ stickyTitleVisible: true, }); |