diff options
Diffstat (limited to 'spec/frontend/vue_shared/components')
13 files changed, 212 insertions, 85 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap deleted file mode 100644 index 7f655d67ae8..00000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Code Block with default props renders correctly 1`] = ` -<pre - class="code-block rounded code" -> - <code - class="d-block" - > - test-code - </code> -</pre> -`; - -exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = ` -<pre - class="code-block rounded code" - style="max-height: 200px; overflow-y: auto;" -> - <code - class="d-block" - > - test-code - </code> -</pre> -`; diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js index a943d931f67..27b6718fb8e 100644 --- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js +++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js @@ -1,6 +1,11 @@ import { shallowMount } from '@vue/test-utils'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); describe('CI Badge Link Component', () => { let wrapper; @@ -79,17 +84,20 @@ describe('CI Badge Link Component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); - it.each(Object.keys(statuses))('should render badge for status: %s', (status) => { + it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => { createComponent({ status: statuses[status] }); - expect(wrapper.attributes('href')).toBe(statuses[status].details_path); + expect(wrapper.attributes('href')).toBe(); expect(wrapper.text()).toBe(statuses[status].text); expect(wrapper.classes()).toContain('ci-status'); expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`); expect(findIcon().exists()).toBe(true); + + await wrapper.trigger('click'); + + expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path); }); it('should not render label', () => { @@ -97,4 +105,12 @@ describe('CI Badge Link Component', () => { expect(wrapper.text()).toBe(''); }); + + it('should emit ciStatusBadgeClick event', async () => { + createComponent({ status: statuses.success }); + + await wrapper.trigger('click'); + + expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]); + }); }); diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js new file mode 100644 index 00000000000..181692e61b5 --- /dev/null +++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js @@ -0,0 +1,65 @@ +import { shallowMount } from '@vue/test-utils'; +import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Code Block Highlighted', () => { + let wrapper; + + const code = 'const foo = 1;'; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(CodeBlock, { propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders highlighted code if language is supported', async () => { + createComponent({ code, language: 'javascript' }); + + await waitForPromises(); + + expect(wrapper.element).toMatchInlineSnapshot(` + <code-block-stub + class="highlight" + code="" + maxheight="initial" + > + <span> + <span + class="hljs-keyword" + > + const + </span> + foo = + <span + class="hljs-number" + > + 1 + </span> + ; + </span> + </code-block-stub> + `); + }); + + it("renders plain text if language isn't supported", async () => { + createComponent({ code, language: 'foobar' }); + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]); + + expect(wrapper.element).toMatchInlineSnapshot(` + <code-block-stub + class="highlight" + code="" + maxheight="initial" + > + <span> + const foo = 1; + </span> + </code-block-stub> + `); + }); +}); diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js index 60b0b0b566b..9a4dbcc47ff 100644 --- a/spec/frontend/vue_shared/components/code_block_spec.js +++ b/spec/frontend/vue_shared/components/code_block_spec.js @@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue'; describe('Code Block', () => { let wrapper; - const defaultProps = { - code: 'test-code', - }; + const code = 'test-code'; - const createComponent = (props = {}) => { + const createComponent = (propsData, slots = {}) => { wrapper = shallowMount(CodeBlock, { - propsData: { - ...defaultProps, - ...props, - }, + slots, + propsData, }); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); - describe('with default props', () => { - beforeEach(() => { - createComponent(); - }); + it('overwrites the default slot', () => { + createComponent({}, { default: 'DEFAULT SLOT' }); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + DEFAULT SLOT + </pre> + `); }); - describe('with maxHeight set to "200px"', () => { - beforeEach(() => { - createComponent({ maxHeight: '200px' }); - }); + it('renders with empty code prop', () => { + createComponent({}); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + <code + class="d-block" + > + + </code> + </pre> + `); + }); + + it('renders code prop when provided', () => { + createComponent({ code }); + + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + <code + class="d-block" + > + test-code + </code> + </pre> + `); + }); + + it('sets maxHeight properly when provided', () => { + createComponent({ code, maxHeight: '200px' }); + + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + style="max-height: 200px; overflow-y: auto;" + > + <code + class="d-block" + > + test-code + </code> + </pre> + `); }); }); diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js index 04f63b4bd45..68684004b82 100644 --- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js @@ -66,7 +66,7 @@ describe('Diff Stats Dropdown', () => { createComponent({ files: mockFiles }); }); - it('when no file name provided ', () => { + it('when no file name provided', () => { expect(findChangedFiles().at(0).text()).toContain(i18n.noFileNameAvailable); }); @@ -153,7 +153,7 @@ describe('Diff Stats Dropdown', () => { createComponent({ files: mockFiles }); }); - it('updates the URL ', () => { + it('updates the URL', () => { findChangedFiles().at(0).vm.$emit('click'); expect(window.location.hash).toBe(mockFiles[0].href); findChangedFiles().at(1).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js index 2dcd91f737f..6dc018797a6 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -157,13 +157,13 @@ describe('GlModalVuex', () => { const handler = modalFooterSlotContent.mock.calls[0][0][handlerName]; - expect(wrapper.emitted(handlerName)).toBeFalsy(); + expect(wrapper.emitted(handlerName)).toBeUndefined(); expect(actions.hide).not.toHaveBeenCalled(); handler(); expect(actions.hide).toHaveBeenCalledTimes(1); - expect(wrapper.emitted(handlerName)).toBeTruthy(); + expect(wrapper.emitted(handlerName)).toHaveLength(1); }, ); }); diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js index 9f819cc4e94..ae9c920ebd2 100644 --- a/spec/frontend/vue_shared/components/paginated_list_spec.js +++ b/spec/frontend/vue_shared/components/paginated_list_spec.js @@ -49,7 +49,7 @@ describe('Pagination links component', () => { }); describe('rendering', () => { - it('it renders the gl-paginated-list', () => { + it('renders the gl-paginated-list', () => { expect(wrapper.find('ul.list-group').exists()).toBe(true); expect(wrapper.findAll('li.list-group-item').length).toBe(2); }); diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js index 70f4693ae81..fa7fabfaef6 100644 --- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js +++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js @@ -108,7 +108,7 @@ describe('Registry Search', () => { ]); }); - it('on sort item click emits sorting:changed event ', () => { + it('on sort item click emits sorting:changed event', () => { mountComponent(); findSortingItems().at(1).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index afad9314ace..48530a0261f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -56,7 +56,7 @@ export const mockSuggestedColors = { '#013220': 'Dark green', '#6699cc': 'Blue-gray', '#0000ff': 'Blue', - '#e6e6fa': 'Lavendar', + '#e6e6fa': 'Lavender', '#9400d3': 'Dark violet', '#330066': 'Deep violet', '#808080': 'Gray', diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 4fbc907a813..e020d9a557e 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -110,6 +110,13 @@ describe('Source Viewer component', () => { expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default); }); + it('correctly maps languages starting with uppercase', async () => { + await createComponent({ language: 'Python3' }); + const languageDefinition = await import(`highlight.js/lib/languages/python`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default); + }); + it('highlights the first chunk', () => { expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); }); @@ -149,7 +156,7 @@ describe('Source Viewer component', () => { it('emits showBlobInteractionZones on the eventHub when chunk appears', () => { findChunks().at(0).vm.$emit('appear'); - expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path); + expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path); }); describe('LineHighlighter', () => { diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap index 1798ca5ccde..f9d615d4f68 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap +++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap @@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div @@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" type="button" > <div diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js index 70dec42ab32..521744154ba 100644 --- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js +++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js @@ -84,7 +84,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith(initialSlotProps()); + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(initialSlotProps()); }); }); @@ -98,7 +98,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isDismissed: true, isLoadingQuery: false, @@ -117,7 +117,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isLoadingQuery: false, shouldShowCallout: true, @@ -136,7 +136,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isLoadingQuery: false, queryError: expect.any(Error), @@ -155,7 +155,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isAnonUser: true, isLoadingQuery: false, @@ -186,7 +186,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isLoadingQuery: false, shouldShowCallout: true, @@ -217,7 +217,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', async () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isLoadingQuery: false, shouldShowCallout: true, @@ -229,7 +229,7 @@ describe('UserCalloutDismisser', () => { // Wait for Vue re-render due to prop change await nextTick(); - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isDismissed: true, isLoadingMutation: true, @@ -240,7 +240,7 @@ describe('UserCalloutDismisser', () => { // Wait for mutation to resolve await waitForPromises(); - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isDismissed: true, isLoadingQuery: false, @@ -270,7 +270,7 @@ describe('UserCalloutDismisser', () => { }); it('passes expected slot props to child', async () => { - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isLoadingQuery: false, shouldShowCallout: true, @@ -282,7 +282,7 @@ describe('UserCalloutDismisser', () => { // Wait for Vue re-render due to prop change await nextTick(); - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isDismissed: true, isLoadingMutation: true, @@ -293,7 +293,7 @@ describe('UserCalloutDismisser', () => { // Wait for mutation to resolve await waitForPromises(); - expect(defaultScopedSlotSpy).lastCalledWith( + expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith( initialSlotProps({ isDismissed: true, isLoadingQuery: 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 b7ce3e47cef..6d48000beb0 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,8 +1,15 @@ import { GlSkeletonLoader, GlIcon } from '@gitlab/ui'; import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { sprintf } from '~/locale'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; +import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; +import { + I18N_USER_BLOCKED, + I18N_USER_LEARN, + I18N_USER_FOLLOW, + I18N_USER_UNFOLLOW, +} from '~/vue_shared/components/user_popover/constants'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import { followUser, unfollowUser } from '~/api/user_api'; @@ -310,7 +317,9 @@ describe('User Popover Component', () => { const securityBotDocsLink = findSecurityBotDocsLink(); expect(securityBotDocsLink.exists()).toBe(true); expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl); - expect(securityBotDocsLink.text()).toBe('Learn more about GitLab Security Bot'); + expect(securityBotDocsLink.text()).toBe( + sprintf(I18N_USER_LEARN, { name: SECURITY_BOT_USER.name }), + ); }); it("does not show a link to the bot's documentation if there is no website_url", () => { @@ -320,9 +329,10 @@ describe('User Popover Component', () => { }); it("doesn't escape user's name", () => { - createWrapper({ user: { ...SECURITY_BOT_USER, name: '%<>\';"' } }); + const name = '%<>\';"'; + createWrapper({ user: { ...SECURITY_BOT_USER, name } }); const securityBotDocsLink = findSecurityBotDocsLink(); - expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"'); + expect(securityBotDocsLink.text()).toBe(sprintf(I18N_USER_LEARN, { name }, false)); }); it('does not display local time', () => { @@ -336,7 +346,7 @@ describe('User Popover Component', () => { beforeEach(() => createWrapper()); it('renders the Follow button with the correct variant', () => { - expect(findToggleFollowButton().text()).toBe('Follow'); + expect(findToggleFollowButton().text()).toBe(I18N_USER_FOLLOW); expect(findToggleFollowButton().props('variant')).toBe('confirm'); }); @@ -387,7 +397,7 @@ describe('User Popover Component', () => { beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } })); it('renders the Unfollow button with the correct variant', () => { - expect(findToggleFollowButton().text()).toBe('Unfollow'); + expect(findToggleFollowButton().text()).toBe(I18N_USER_UNFOLLOW); expect(findToggleFollowButton().props('variant')).toBe('default'); }); @@ -441,6 +451,25 @@ describe('User Popover Component', () => { }); }); + describe('when the user is blocked', () => { + const bio = 'My super interesting bio'; + const status = 'My status'; + beforeEach(() => + createWrapper({ + user: { ...DEFAULT_PROPS.user, state: 'blocked', bio, status: { message_html: status } }, + }), + ); + + it('renders warning', () => { + expect(wrapper.text()).toContain(I18N_USER_BLOCKED); + }); + + it("doesn't show other information", () => { + expect(wrapper.text()).not.toContain(bio); + expect(wrapper.text()).not.toContain(status); + }); + }); + describe('when API does not support `isFollowed`', () => { beforeEach(() => { const user = { |