diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 23:02:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 23:02:30 +0300 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /spec/frontend/vue_shared | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared')
23 files changed, 1243 insertions, 308 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/content_transition_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/content_transition_spec.js.snap new file mode 100644 index 00000000000..fd804990b5e --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/content_transition_spec.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`~/vue_shared/components/content_transition.vue default shows all transitions and only default is visible 1`] = ` +<div> + <transition-stub + name="test_transition_name" + > + <div + data-testval="default" + > + <p> + Default + </p> + </div> + </transition-stub> + <transition-stub + name="test_transition_name" + > + <div + data-testval="foo" + style="display: none;" + > + <p> + Foo + </p> + </div> + </transition-stub> + <transition-stub + name="test_transition_name" + > + <div + data-testval="bar" + style="display: none;" + > + <p> + Bar + </p> + </div> + </transition-stub> +</div> +`; 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 fef50bdaccc..28b3bf5287a 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 @@ -127,5 +127,18 @@ describe('ColorPicker', () => { expect(wrapper.emitted().input[0]).toStrictEqual([setColor]); }); + + it('shows the suggested colors passed using props', () => { + const customColors = { + '#ff0000': 'Red', + '#808080': 'Gray', + }; + + createComponent(shallowMount, { suggestedColors: customColors }); + expect(description()).toBe('Enter any color or choose one of the suggested colors below.'); + expect(presetColors()).toHaveLength(2); + expect(presetColors().at(0).attributes('title')).toBe('Red'); + expect(presetColors().at(1).attributes('title')).toBe('Gray'); + }); }); }); diff --git a/spec/frontend/vue_shared/components/content_transition_spec.js b/spec/frontend/vue_shared/components/content_transition_spec.js new file mode 100644 index 00000000000..8bb6d31cce7 --- /dev/null +++ b/spec/frontend/vue_shared/components/content_transition_spec.js @@ -0,0 +1,109 @@ +import { groupBy, mapValues } from 'lodash'; +import { shallowMount } from '@vue/test-utils'; +import ContentTransition from '~/vue_shared/components/content_transition.vue'; + +const TEST_CURRENT_SLOT = 'default'; +const TEST_TRANSITION_NAME = 'test_transition_name'; +const TEST_SLOTS = [ + { key: 'default', attributes: { 'data-testval': 'default' } }, + { key: 'foo', attributes: { 'data-testval': 'foo' } }, + { key: 'bar', attributes: { 'data-testval': 'bar' } }, +]; + +describe('~/vue_shared/components/content_transition.vue', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createComponent = (props = {}, slots = {}) => { + wrapper = shallowMount(ContentTransition, { + propsData: { + transitionName: TEST_TRANSITION_NAME, + currentSlot: TEST_CURRENT_SLOT, + slots: TEST_SLOTS, + ...props, + }, + slots: { + default: '<p>Default</p>', + foo: '<p>Foo</p>', + bar: '<p>Bar</p>', + dne: '<p>DOES NOT EXIST</p>', + ...slots, + }, + }); + }; + + const findTransitionsData = () => + wrapper.findAll('transition-stub').wrappers.map((transition) => { + const child = transition.find('[data-testval]'); + const { style, ...attributes } = child.attributes(); + + return { + transitionName: transition.attributes('name'), + isVisible: child.isVisible(), + attributes, + text: transition.text(), + }; + }); + const findVisibleData = () => { + const group = groupBy(findTransitionsData(), (x) => x.attributes['data-testval']); + + return mapValues(group, (x) => x[0].isVisible); + }; + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows all transitions and only default is visible', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('render transitions for each slot', () => { + expect(findTransitionsData()).toEqual([ + { + attributes: { + 'data-testval': 'default', + }, + isVisible: true, + text: 'Default', + transitionName: 'test_transition_name', + }, + { + attributes: { + 'data-testval': 'foo', + }, + isVisible: false, + text: 'Foo', + transitionName: 'test_transition_name', + }, + { + attributes: { + 'data-testval': 'bar', + }, + isVisible: false, + text: 'Bar', + transitionName: 'test_transition_name', + }, + ]); + }); + }); + + describe('with currentSlot=foo', () => { + beforeEach(() => { + createComponent({ currentSlot: 'foo' }); + }); + + it('should only show the foo slot', () => { + expect(findVisibleData()).toEqual({ + default: false, + foo: true, + bar: false, + }); + }); + }); +}); 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 dd9bf2ff598..af8a2a496ea 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 @@ -1,12 +1,24 @@ -import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui'; +import { + GlFilteredSearchToken, + GlLoadingIcon, + GlFilteredSearchSuggestion, + GlDropdownSectionHeader, + GlDropdownDivider, + GlDropdownText, +} from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mockRegularLabel, mockLabels, } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; -import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + DEFAULT_NONE_ANY, + OPERATOR_IS, + OPERATOR_IS_NOT, +} from '~/vue_shared/components/filtered_search_bar/constants'; import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, @@ -32,6 +44,7 @@ const defaultStubs = { <div> <slot name="view-token"></slot> <slot name="view"></slot> + <slot name="suggestions"></slot> </div> `, }, @@ -43,6 +56,7 @@ const defaultStubs = { }, }; +const mockSuggestionListTestId = 'suggestion-list'; const defaultSlots = { 'view-token': ` <div class="js-view-token">${mockRegularLabel.title}</div> @@ -52,6 +66,10 @@ const defaultSlots = { `, }; +const defaultScopedSlots = { + 'suggestions-list': `<div data-testid="${mockSuggestionListTestId}" :data-suggestions="JSON.stringify(props.suggestions)"></div>`, +}; + const mockProps = { config: { ...mockLabelToken, recentSuggestionsStorageKey: mockStorageKey }, value: { data: '' }, @@ -62,8 +80,15 @@ const mockProps = { getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data), }; -function createComponent({ props = {}, stubs = defaultStubs, slots = defaultSlots } = {}) { - return mount(BaseToken, { +function createComponent({ + props = {}, + data = {}, + stubs = defaultStubs, + slots = defaultSlots, + scopedSlots = defaultScopedSlots, + mountFn = mount, +} = {}) { + return mountFn(BaseToken, { propsData: { ...mockProps, ...props, @@ -72,9 +97,17 @@ function createComponent({ props = {}, stubs = defaultStubs, slots = defaultSlot portalName: 'fake target', alignSuggestions: jest.fn(), suggestionsListClass: () => 'custom-class', + filteredSearchSuggestionListInstance: { + register: jest.fn(), + unregister: jest.fn(), + }, + }, + data() { + return data; }, stubs, slots, + scopedSlots, }); } @@ -82,6 +115,9 @@ describe('BaseToken', () => { let wrapper; const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findMockSuggestionList = () => wrapper.findByTestId(mockSuggestionListTestId); + const getMockSuggestionListSuggestions = () => + JSON.parse(findMockSuggestionList().attributes('data-suggestions')); afterEach(() => { wrapper.destroy(); @@ -136,6 +172,187 @@ describe('BaseToken', () => { }); }); + describe('suggestions', () => { + describe('with suggestions disabled', () => { + beforeEach(() => { + wrapper = createComponent({ + props: { + config: { + suggestionsDisabled: true, + }, + suggestions: [{ id: 'Foo' }], + }, + mountFn: shallowMountExtended, + }); + }); + + it('does not render suggestions', () => { + expect(findMockSuggestionList().exists()).toBe(false); + }); + }); + + describe('with available suggestions', () => { + let mockSuggestions; + + describe.each` + hasSuggestions | searchKey | shouldRenderSuggestions + ${true} | ${null} | ${true} + ${true} | ${'foo'} | ${true} + ${false} | ${null} | ${false} + `( + `when hasSuggestions is $hasSuggestions`, + ({ hasSuggestions, searchKey, shouldRenderSuggestions }) => { + beforeEach(async () => { + mockSuggestions = hasSuggestions ? [{ id: 'Foo' }] : []; + const props = { defaultSuggestions: [], suggestions: mockSuggestions }; + + getRecentlyUsedSuggestions.mockReturnValue([]); + wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); + findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); + + await nextTick(); + }); + + it(`${shouldRenderSuggestions ? 'should' : 'should not'} render suggestions`, () => { + expect(findMockSuggestionList().exists()).toBe(shouldRenderSuggestions); + + if (shouldRenderSuggestions) { + expect(getMockSuggestionListSuggestions()).toEqual(mockSuggestions); + } + }); + }, + ); + }); + + describe('with preloaded suggestions', () => { + const mockPreloadedSuggestions = [{ id: 'Foo' }, { id: 'Bar' }]; + + describe.each` + searchKey | shouldRenderPreloadedSuggestions + ${null} | ${true} + ${'foo'} | ${false} + `('when searchKey is $searchKey', ({ shouldRenderPreloadedSuggestions, searchKey }) => { + beforeEach(async () => { + const props = { preloadedSuggestions: mockPreloadedSuggestions }; + wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); + findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); + + await nextTick(); + }); + + it(`${ + shouldRenderPreloadedSuggestions ? 'should' : 'should not' + } render preloaded suggestions`, () => { + expect(findMockSuggestionList().exists()).toBe(shouldRenderPreloadedSuggestions); + + if (shouldRenderPreloadedSuggestions) { + expect(getMockSuggestionListSuggestions()).toEqual(mockPreloadedSuggestions); + } + }); + }); + }); + + describe('with recent suggestions', () => { + let mockSuggestions; + + describe.each` + searchKey | recentEnabled | shouldRenderRecentSuggestions + ${null} | ${true} | ${true} + ${'foo'} | ${true} | ${false} + ${null} | ${false} | ${false} + `( + 'when searchKey is $searchKey and recentEnabled is $recentEnabled', + ({ shouldRenderRecentSuggestions, recentEnabled, searchKey }) => { + beforeEach(async () => { + const props = { value: { data: '', operator: '=' }, defaultSuggestions: [] }; + + if (recentEnabled) { + mockSuggestions = [{ id: 'Foo' }, { id: 'Bar' }]; + getRecentlyUsedSuggestions.mockReturnValue(mockSuggestions); + } + + props.config = { recentSuggestionsStorageKey: recentEnabled ? mockStorageKey : null }; + + wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); + findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); + + await nextTick(); + }); + + it(`${ + shouldRenderRecentSuggestions ? 'should' : 'should not' + } render recent suggestions`, () => { + expect(findMockSuggestionList().exists()).toBe(shouldRenderRecentSuggestions); + expect(wrapper.findComponent(GlDropdownSectionHeader).exists()).toBe( + shouldRenderRecentSuggestions, + ); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe( + shouldRenderRecentSuggestions, + ); + + if (shouldRenderRecentSuggestions) { + expect(getMockSuggestionListSuggestions()).toEqual(mockSuggestions); + } + }); + }, + ); + }); + + describe('with default suggestions', () => { + describe.each` + operator | shouldRenderFilteredSearchSuggestion + ${OPERATOR_IS} | ${true} + ${OPERATOR_IS_NOT} | ${false} + `('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => { + beforeEach(() => { + const props = { + defaultSuggestions: DEFAULT_NONE_ANY, + value: { data: '', operator }, + }; + + wrapper = createComponent({ props, mountFn: shallowMountExtended }); + }); + + it(`${ + shouldRenderFilteredSearchSuggestion ? 'should' : 'should not' + } render GlFilteredSearchSuggestion`, () => { + const filteredSearchSuggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion) + .wrappers; + + if (shouldRenderFilteredSearchSuggestion) { + expect(filteredSearchSuggestions.map((c) => c.props())).toMatchObject( + DEFAULT_NONE_ANY.map((opt) => ({ value: opt.value })), + ); + } else { + expect(filteredSearchSuggestions).toHaveLength(0); + } + }); + }); + }); + + describe('with no suggestions', () => { + it.each` + data | expected + ${{ searchKey: 'search' }} | ${'No matches found'} + ${{ hasFetched: true }} | ${'No suggestions found'} + `('shows $expected text', ({ data, expected }) => { + wrapper = createComponent({ + props: { + config: { recentSuggestionsStorageKey: null }, + defaultSuggestions: [], + preloadedSuggestions: [], + suggestions: [], + suggestionsLoading: false, + }, + data, + mountFn: shallowMountExtended, + }); + + expect(wrapper.findComponent(GlDropdownText).text()).toBe(expected); + }); + }); + }); + describe('methods', () => { describe('handleTokenValueSelected', () => { const mockTokenValue = mockLabels[0]; diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index c7ad47b6ef7..b5daa389fc6 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -4,6 +4,7 @@ import $ from 'jquery'; 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 { mountExtended } from 'helpers/vue_test_utils_helper'; const markdownPreviewPath = `${TEST_HOST}/preview`; @@ -32,7 +33,7 @@ describe('Markdown field component', () => { axiosMock.restore(); }); - function createSubject(lines = []) { + function createSubject({ lines = [], enablePreview = true } = {}) { // We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression // caused by mixing Vanilla JS and Vue. subject = mountExtended( @@ -61,6 +62,7 @@ describe('Markdown field component', () => { isSubmitting: false, textareaValue, lines, + enablePreview, }, provide: { glFeatures: { @@ -74,7 +76,7 @@ describe('Markdown field component', () => { const getPreviewLink = () => subject.findByTestId('preview-tab'); const getWriteLink = () => subject.findByTestId('write-tab'); const getMarkdownButton = () => subject.find('.js-md'); - const getAllMarkdownButtons = () => subject.findAll('.js-md'); + const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]'); const getVideo = () => subject.find('video'); const getAttachButton = () => subject.find('.button-attach-file'); const clickAttachButton = () => getAttachButton().trigger('click'); @@ -183,7 +185,7 @@ describe('Markdown field component', () => { it('converts a line', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 0); - const markdownButton = getAllMarkdownButtons().wrappers[5]; + const markdownButton = getListBulletedButton(); markdownButton.trigger('click'); await nextTick(); @@ -193,7 +195,7 @@ describe('Markdown field component', () => { it('converts multiple lines', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 50); - const markdownButton = getAllMarkdownButtons().wrappers[5]; + const markdownButton = getListBulletedButton(); markdownButton.trigger('click'); await nextTick(); @@ -266,17 +268,46 @@ describe('Markdown field component', () => { 'You are about to add 11 people to the discussion. They will all receive a notification.', ); }); + + it('removes warning when all mention is removed while endpoint is loading', async () => { + axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } }); + jest.spyOn(axios, 'post'); + + subject.setProps({ textareaValue: 'hello @all' }); + + await nextTick(); + + subject.setProps({ textareaValue: 'hello @allan' }); + + await axios.waitFor(markdownPreviewPath); + + expect(axios.post).toHaveBeenCalled(); + expect(subject.text()).not.toContain( + 'You are about to add 11 people to the discussion. They will all receive a notification.', + ); + }); }); }); }); describe('suggestions', () => { it('escapes new line characters', () => { - createSubject([{ rich_text: 'hello world\\n' }]); + createSubject({ lines: [{ rich_text: 'hello world\\n' }] }); expect(subject.find('[data-testid="markdownHeader"]').props('lineContent')).toBe( 'hello world%br', ); }); }); + + it('allows enabling and disabling Markdown Preview', () => { + createSubject({ enablePreview: false }); + + expect(subject.findComponent(MarkdownFieldHeader).props('enablePreview')).toBe(false); + + subject.destroy(); + createSubject({ enablePreview: true }); + + expect(subject.findComponent(MarkdownFieldHeader).props('enablePreview')).toBe(true); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 700ec75fcee..9ffb9c6a541 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -46,6 +46,7 @@ describe('Markdown field header component', () => { const buttons = [ 'Add bold text (⌘B)', 'Add italic text (⌘I)', + 'Add strikethrough text (⌘⇧X)', 'Insert a quote', 'Insert suggestion', 'Insert code', @@ -157,4 +158,12 @@ describe('Markdown field header component', () => { expect(wrapper.find('.js-suggestion-btn').exists()).toBe(false); }); + + it('hides preview tab when previewMarkdown property is false', () => { + createWrapper({ + enablePreview: false, + }); + + expect(wrapper.findByTestId('preview-tab').exists()).toBe(false); + }); }); diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap index 573bc9abe4d..f878d685b6d 100644 --- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap +++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap @@ -34,21 +34,19 @@ exports[`Issue Warning Component when noteable is locked and confidential render <span> <span> This issue is - <a + <gl-link-stub href="" - rel="noopener noreferrer" target="_blank" > confidential - </a> + </gl-link-stub> and - <a + <gl-link-stub href="" - rel="noopener noreferrer" target="_blank" > locked - </a> + </gl-link-stub> . </span> diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js index accbf14572d..99b65ca6937 100644 --- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js +++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js @@ -1,4 +1,4 @@ -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; @@ -16,6 +16,9 @@ describe('Issue Warning Component', () => { propsData: { ...props, }, + stubs: { + GlSprintf, + }, }); afterEach(() => { 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 36050a42da7..8270ff31574 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 @@ -221,7 +221,7 @@ describe('AlertManagementEmptyState', () => { findPagination().vm.$emit('input', 3); await nextTick(); - expect(findPagination().findAll('.page-item').at(0).text()).toBe('Prev'); + expect(findPagination().findAll('.page-item').at(0).text()).toBe('Previous'); }); it('returns prevPage number', async () => { 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 b2906973dbd..6954bd5ccff 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 @@ -2,6 +2,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-modal-stub + actionprimary="[object Object]" actionsecondary="[object Object]" dismisslabel="Close" modalclass="" @@ -11,100 +12,161 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` titletag="h4" > <p> - For each solution, you will choose a capacity. 1 enables warm HA through Auto Scaling group re-spawn. 2 enables hot HA because the service is available even when a node is lost. 3 or more enables hot HA and manual scaling of runner fleet. + Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console. </p> - <ul - class="gl-list-style-none gl-p-0 gl-mb-0" + <gl-form-radio-group-stub + checked="[object Object]" + disabledfield="disabled" + htmlfield="html" + label="Choose your preferred GitLab Runner" + label-sr-only="" + options="" + textfield="text" + valuefield="value" > - <li> - <gl-link-stub - class="gl-display-flex gl-font-weight-bold" - href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml&stackName=linux-docker-nonspot¶m_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host" - target="_blank" + <gl-form-radio-stub + class="gl-py-5 gl-pl-8 gl-border-b" + value="[object Object]" + > + <div + class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold" > - <img - alt="linux-docker-nonspot" - class="gl-mt-2 gl-mr-5 gl-mb-6" - height="46" - src="/assets/aws-cloud-formation.png" - title="linux-docker-nonspot" - width="46" - /> - Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot. Default choice for Linux Docker executor. - - </gl-link-stub> - </li> - <li> - <gl-link-stub - class="gl-display-flex gl-font-weight-bold" - href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml&stackName=linux-docker-spotonly¶m_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host" - target="_blank" + Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot. + + <gl-accordion-stub + class="gl-pt-3" + headerlevel="3" + > + <gl-accordion-item-stub + class="gl-font-weight-normal" + title="More Details" + title-visible="Less Details" + > + <p + class="gl-pt-2" + > + No spot. This is the default choice for Linux Docker executor. + </p> + + <p + class="gl-m-0" + > + A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet. + </p> + </gl-accordion-item-stub> + </gl-accordion-stub> + </div> + </gl-form-radio-stub> + <gl-form-radio-stub + class="gl-py-5 gl-pl-8 gl-border-b" + value="[object Object]" + > + <div + class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold" > - <img - alt="linux-docker-spotonly" - class="gl-mt-2 gl-mr-5 gl-mb-6" - height="46" - src="/assets/aws-cloud-formation.png" - title="linux-docker-spotonly" - width="46" - /> Amazon Linux 2 Docker HA with manual scaling and optional scheduling. 100% spot. - - </gl-link-stub> - </li> - <li> - <gl-link-stub - class="gl-display-flex gl-font-weight-bold" - href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml&stackName=win2019-shell-non-spot¶m_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host" - target="_blank" + + <gl-accordion-stub + class="gl-pt-3" + headerlevel="3" + > + <gl-accordion-item-stub + class="gl-font-weight-normal" + title="More Details" + title-visible="Less Details" + > + <p + class="gl-pt-2" + > + 100% spot. + </p> + + <p + class="gl-m-0" + > + Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet. + </p> + </gl-accordion-item-stub> + </gl-accordion-stub> + </div> + </gl-form-radio-stub> + <gl-form-radio-stub + class="gl-py-5 gl-pl-8 gl-border-b" + value="[object Object]" + > + <div + class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold" > - <img - alt="win2019-shell-non-spot" - class="gl-mt-2 gl-mr-5 gl-mb-6" - height="46" - src="/assets/aws-cloud-formation.png" - title="win2019-shell-non-spot" - width="46" - /> - Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor. - - </gl-link-stub> - </li> - <li> - <gl-link-stub - class="gl-display-flex gl-font-weight-bold" - href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml&stackName=win2019-shell-spot¶m_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host" - target="_blank" + Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. + + <gl-accordion-stub + class="gl-pt-3" + headerlevel="3" + > + <gl-accordion-item-stub + class="gl-font-weight-normal" + title="More Details" + title-visible="Less Details" + > + <p + class="gl-pt-2" + > + No spot. Default choice for Windows Shell executor. + </p> + + <p + class="gl-m-0" + > + Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet. + </p> + </gl-accordion-item-stub> + </gl-accordion-stub> + </div> + </gl-form-radio-stub> + <gl-form-radio-stub + class="gl-py-5 gl-pl-8" + value="[object Object]" + > + <div + class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold" > - <img - alt="win2019-shell-spot" - class="gl-mt-2 gl-mr-5 gl-mb-6" - height="46" - src="/assets/aws-cloud-formation.png" - title="win2019-shell-spot" - width="46" - /> Windows 2019 Shell with manual scaling and optional scheduling. 100% spot. - - </gl-link-stub> - </li> - </ul> + + <gl-accordion-stub + class="gl-pt-3" + headerlevel="3" + > + <gl-accordion-item-stub + class="gl-font-weight-normal" + title="More Details" + title-visible="Less Details" + > + <p + class="gl-pt-2" + > + 100% spot. + </p> + + <p + class="gl-m-0" + > + Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet. + </p> + </gl-accordion-item-stub> + </gl-accordion-stub> + </div> + </gl-form-radio-stub> + </gl-form-radio-group-stub> <p> <gl-sprintf-stub - message="Don't see what you are looking for? See the full list of options, including a fully customizable option, %{linkStart}here%{linkEnd}." + message="Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}." /> </p> - - <p - class="gl-font-sm gl-mb-0" - > - If you do not select an AWS VPC, the runner will deploy to the Default VPC in the AWS Region you select. Please consult with your AWS administrator to understand if there are any security risks to deploying into the Default VPC in any given region in your AWS account. - </p> </gl-modal-stub> `; diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js index ad692a38e65..a9ba4946358 100644 --- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js @@ -1,27 +1,29 @@ -import { GlLink } from '@gitlab/ui'; +import { GlModal, GlFormRadio } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import ExperimentTracking from '~/experimentation/experiment_tracking'; -import { getBaseURL } from '~/lib/utils/url_utility'; +import { getBaseURL, visitUrl } from '~/lib/utils/url_utility'; +import { mockTracking } from 'helpers/tracking_helper'; import { - EXPERIMENT_NAME, CF_BASE_URL, TEMPLATES_BASE_URL, EASY_BUTTONS, } from '~/vue_shared/components/runner_aws_deployments/constants'; import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue'; -jest.mock('~/experimentation/experiment_tracking'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); describe('RunnerAwsDeploymentsModal', () => { let wrapper; - const findEasyButtons = () => wrapper.findAllComponents(GlLink); + const findModal = () => wrapper.findComponent(GlModal); + const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio); const createComponent = () => { wrapper = shallowMount(RunnerAwsDeploymentsModal, { propsData: { modalId: 'runner-aws-deployments-modal', - imgSrc: '/assets/aws-cloud-formation.png', }, }); }; @@ -43,34 +45,30 @@ describe('RunnerAwsDeploymentsModal', () => { }); describe('first easy button', () => { - const findFirstButton = () => findEasyButtons().at(0); - it('should contain the correct description', () => { - expect(findFirstButton().text()).toBe(EASY_BUTTONS[0].description); + expect(findEasyButtons().at(0).text()).toContain(EASY_BUTTONS[0].description); }); it('should contain the correct link', () => { - const link = findFirstButton().attributes('href'); + const templateUrl = encodeURIComponent(TEMPLATES_BASE_URL + EASY_BUTTONS[0].templateName); + const { stackName } = EASY_BUTTONS[0]; + const instanceUrl = encodeURIComponent(getBaseURL()); + const url = `${CF_BASE_URL}templateURL=${templateUrl}&stackName=${stackName}¶m_3GITLABRunnerInstanceURL=${instanceUrl}`; + + findModal().vm.$emit('primary'); - expect(link.startsWith(CF_BASE_URL)).toBe(true); - expect( - link.includes( - `templateURL=${encodeURIComponent(TEMPLATES_BASE_URL + EASY_BUTTONS[0].templateName)}`, - ), - ).toBe(true); - expect(link.includes(`stackName=${EASY_BUTTONS[0].stackName}`)).toBe(true); - expect( - link.includes(`param_3GITLABRunnerInstanceURL=${encodeURIComponent(getBaseURL())}`), - ).toBe(true); + expect(visitUrl).toHaveBeenCalledWith(url, true); }); it('should track an event when clicked', () => { - findFirstButton().vm.$emit('click'); + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + findModal().vm.$emit('primary'); - expect(ExperimentTracking).toHaveBeenCalledWith(EXPERIMENT_NAME); - expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( - `template_clicked_${EASY_BUTTONS[0].stackName}`, - ); + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', { + label: EASY_BUTTONS[0].stackName, + }); }); }); }); 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 2010bac7060..ab579945e22 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 @@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; +import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils'; jest.mock('highlight.js/lib/core'); Vue.use(VueRouter); @@ -36,6 +37,7 @@ describe('Source Viewer component', () => { beforeEach(() => { hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); + jest.spyOn(sourceViewerUtils, 'wrapLines'); return createComponent(); }); @@ -73,6 +75,10 @@ describe('Source Viewer component', () => { expect(findLoadingIcon().exists()).toBe(true); }); + it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => { + expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage); + }); + it('renders Line Numbers', () => { expect(findLineNumbers().props('lines')).toBe(1); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js index 937c3b26c67..0631e7efd54 100644 --- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js @@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; describe('Wrap lines', () => { it.each` - input | output - ${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'} - ${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`} - ${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`} - ${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'} - `('returns lines wrapped in spans containing line numbers', ({ input, output }) => { - expect(wrapLines(input)).toBe(output); + content | language | output + ${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'} + ${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`} + ${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`} + ${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'} + ${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'} + ${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'} + `('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => { + expect(wrapLines(content, language)).toBe(output); + }); + + it.each` + language + ${'invalidLanguage>'} + ${'"invalidLanguage"'} + ${'<invalidLanguage'} + `('returns lines safely without XSS language is not valid', ({ language }) => { + expect(wrapLines('<span class="hljs-code">```bash', language)).toBe( + '<span id="LC1" lang="" class="hljs-code">```bash', + ); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js new file mode 100644 index 00000000000..f624f84eabd --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js @@ -0,0 +1,127 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAvatar, GlTooltip } from '@gitlab/ui'; +import defaultAvatarUrl from 'images/no_avatar.png'; +import { placeholderImage } from '~/lazy_loader'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue'; + +jest.mock('images/no_avatar.png', () => 'default-avatar-url'); + +const PROVIDED_PROPS = { + size: 32, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', +}; + +describe('User Avatar Image Component', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + }); + + it('should render `GlAvatar` and provide correct properties to it', () => { + const avatar = wrapper.findComponent(GlAvatar); + + expect(avatar.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(avatar.props()).toMatchObject({ + src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + alt: PROVIDED_PROPS.imgAlt, + size: PROVIDED_PROPS.size, + }); + }); + + it('should add correct CSS classes', () => { + const classes = wrapper.findComponent(GlAvatar).classes(); + expect(classes).toContain(PROVIDED_PROPS.cssClasses); + expect(classes).not.toContain('lazy'); + }); + }); + + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + lazy: true, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + }); + + it('should add lazy attributes', () => { + const avatar = wrapper.findComponent(GlAvatar); + + expect(avatar.classes()).toContain('lazy'); + expect(avatar.attributes()).toMatchObject({ + src: placeholderImage, + 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + }); + }); + }); + + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + imgSrc: null, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + }); + + it('should have default avatar image', () => { + const avatar = wrapper.findComponent(GlAvatar); + + expect(avatar.props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`); + }); + }); + + describe('Dynamic tooltip content', () => { + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: PROVIDED_PROPS, + slots, + }); + }); + + it('renders the tooltip slot', () => { + expect(wrapper.findComponent(GlTooltip).exists()).toBe(true); + }); + + it('renders the tooltip content', () => { + expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js new file mode 100644 index 00000000000..5051b2b9cae --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js @@ -0,0 +1,122 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlTooltip } from '@gitlab/ui'; +import defaultAvatarUrl from 'images/no_avatar.png'; +import { placeholderImage } from '~/lazy_loader'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue'; + +jest.mock('images/no_avatar.png', () => 'default-avatar-url'); + +const PROVIDED_PROPS = { + size: 32, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', +}; + +const DEFAULT_PROPS = { + size: 20, +}; + +describe('User Avatar Image Component', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + }, + }); + }); + + it('should have <img> as a child element', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.exists()).toBe(true); + expect(imageElement.attributes('src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(imageElement.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt); + }); + + it('should properly render img css', () => { + const classes = wrapper.find('img').classes(); + expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]); + expect(classes).not.toContain('lazy'); + }); + }); + + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + lazy: true, + }, + }); + }); + + it('should add lazy attributes', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.classes()).toContain('lazy'); + expect(imageElement.attributes('src')).toBe(placeholderImage); + expect(imageElement.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + }); + }); + + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage); + }); + + it('should have default avatar image', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.attributes('src')).toBe( + `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`, + ); + }); + }); + + describe('dynamic tooltip content', () => { + const props = PROVIDED_PROPS; + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { props }, + slots, + }); + }); + + it('renders the tooltip slot', () => { + expect(wrapper.findComponent(GlTooltip).exists()).toBe(true); + }); + + it('renders the tooltip content', () => { + expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); + }); + + it('does not render tooltip data attributes on avatar image', () => { + const avatarImg = wrapper.find('img'); + + expect(avatarImg.attributes('title')).toBeFalsy(); + expect(avatarImg.attributes('data-placement')).not.toBeDefined(); + expect(avatarImg.attributes('data-container')).not.toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js index 2c3fc70e116..75d2a936b34 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -1,12 +1,10 @@ import { shallowMount } from '@vue/test-utils'; -import defaultAvatarUrl from 'images/no_avatar.png'; -import { placeholderImage } from '~/lazy_loader'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import UserAvatarImageNew from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue'; +import UserAvatarImageOld from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue'; -jest.mock('images/no_avatar.png', () => 'default-avatar-url'); - -const DEFAULT_PROPS = { - size: 99, +const PROVIDED_PROPS = { + size: 32, imgSrc: 'myavatarurl.com', imgAlt: 'mydisplayname', cssClasses: 'myextraavatarclass', @@ -21,89 +19,43 @@ describe('User Avatar Image Component', () => { wrapper.destroy(); }); - describe('Initialization', () => { + describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => { beforeEach(() => { wrapper = shallowMount(UserAvatarImage, { propsData: { - ...DEFAULT_PROPS, + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, }, }); }); - it('should have <img> as a child element', () => { - const imageElement = wrapper.find('img'); - - expect(imageElement.exists()).toBe(true); - expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); - }); - - it('should properly render img css', () => { - const classes = wrapper.find('img').classes(); - expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); - expect(classes).not.toContain('lazy'); + it('should render `UserAvatarImageNew` component', () => { + expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(true); + expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(false); }); }); - describe('Initialization when lazy', () => { + describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => { beforeEach(() => { wrapper = shallowMount(UserAvatarImage, { propsData: { - ...DEFAULT_PROPS, - lazy: true, + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: false, + }, }, }); }); - it('should add lazy attributes', () => { - const imageElement = wrapper.find('img'); - - expect(imageElement.classes()).toContain('lazy'); - expect(imageElement.attributes('src')).toBe(placeholderImage); - expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - }); - }); - - describe('Initialization without src', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage); - }); - - it('should have default avatar image', () => { - const imageElement = wrapper.find('img'); - - expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`); - }); - }); - - describe('dynamic tooltip content', () => { - const props = DEFAULT_PROPS; - const slots = { - default: ['Action!'], - }; - - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { props }, - slots, - }); - }); - - it('renders the tooltip slot', () => { - expect(wrapper.find('.js-user-avatar-image-tooltip').exists()).toBe(true); - }); - - it('renders the tooltip content', () => { - expect(wrapper.find('.js-user-avatar-image-tooltip').text()).toContain(slots.default[0]); - }); - - it('does not render tooltip data attributes for on avatar image', () => { - const avatarImg = wrapper.find('img'); - - expect(avatarImg.attributes('title')).toBeFalsy(); - expect(avatarImg.attributes('data-placement')).not.toBeDefined(); - expect(avatarImg.attributes('data-container')).not.toBeDefined(); + it('should render `UserAvatarImageOld` component', () => { + expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(false); + expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js new file mode 100644 index 00000000000..5ba80b31b99 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js @@ -0,0 +1,102 @@ +import { GlAvatarLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue'; + +describe('User Avatar Link Component', () => { + let wrapper; + + const findUserName = () => wrapper.findByTestId('user-avatar-link-username'); + + const defaultProps = { + linkHref: `${TEST_HOST}/myavatarurl.com`, + imgSize: 32, + imgSrc: `${TEST_HOST}/myavatarurl.com`, + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + username: 'username', + }; + + const createWrapper = (props, slots) => { + wrapper = shallowMountExtended(UserAvatarLink, { + propsData: { + ...defaultProps, + ...props, + ...slots, + }, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render GlLink with correct props', () => { + const link = wrapper.findComponent(GlAvatarLink); + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.linkHref); + }); + + it('should render UserAvatarImage and provide correct props to it', () => { + expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true); + expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({ + cssClasses: defaultProps.imgCssClasses, + imgAlt: defaultProps.imgAlt, + imgSrc: defaultProps.imgSrc, + lazy: false, + size: defaultProps.imgSize, + tooltipPlacement: defaultProps.tooltipPlacement, + tooltipText: '', + }); + }); + + describe('when username provided', () => { + beforeEach(() => { + createWrapper({ username: defaultProps.username }); + }); + + it('should render provided username', () => { + expect(findUserName().text()).toBe(defaultProps.username); + }); + + it('should provide the tooltip data for the username', () => { + expect(findUserName().attributes()).toEqual( + expect.objectContaining({ + title: defaultProps.tooltipText, + 'tooltip-placement': defaultProps.tooltipPlacement, + }), + ); + }); + }); + + describe('when username is NOT provided', () => { + beforeEach(() => { + createWrapper({ username: '' }); + }); + + it('should NOT render username', () => { + expect(findUserName().exists()).toBe(false); + }); + }); + + describe('avatar-badge slot', () => { + const badge = '<span>User badge</span>'; + + beforeEach(() => { + createWrapper(defaultProps, { + 'avatar-badge': badge, + }); + }); + + it('should render provided `avatar-badge` slot content', () => { + expect(wrapper.html()).toContain(badge); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js new file mode 100644 index 00000000000..2d513c46e77 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js @@ -0,0 +1,102 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue'; + +describe('User Avatar Link Component', () => { + let wrapper; + + const findUserName = () => wrapper.find('[data-testid="user-avatar-link-username"]'); + + const defaultProps = { + linkHref: `${TEST_HOST}/myavatarurl.com`, + imgSize: 32, + imgSrc: `${TEST_HOST}/myavatarurl.com`, + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + username: 'username', + }; + + const createWrapper = (props, slots) => { + wrapper = shallowMountExtended(UserAvatarLink, { + propsData: { + ...defaultProps, + ...props, + ...slots, + }, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render GlLink with correct props', () => { + const link = wrapper.findComponent(GlLink); + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.linkHref); + }); + + it('should render UserAvatarImage and povide correct props to it', () => { + expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true); + expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({ + cssClasses: defaultProps.imgCssClasses, + imgAlt: defaultProps.imgAlt, + imgSrc: defaultProps.imgSrc, + lazy: false, + size: defaultProps.imgSize, + tooltipPlacement: defaultProps.tooltipPlacement, + tooltipText: '', + }); + }); + + describe('when username provided', () => { + beforeEach(() => { + createWrapper({ username: defaultProps.username }); + }); + + it('should render provided username', () => { + expect(findUserName().text()).toBe(defaultProps.username); + }); + + it('should provide the tooltip data for the username', () => { + expect(findUserName().attributes()).toEqual( + expect.objectContaining({ + title: defaultProps.tooltipText, + 'tooltip-placement': defaultProps.tooltipPlacement, + }), + ); + }); + }); + + describe('when username is NOT provided', () => { + beforeEach(() => { + createWrapper({ username: '' }); + }); + + it('should NOT render username', () => { + expect(findUserName().exists()).toBe(false); + }); + }); + + describe('avatar-badge slot', () => { + const badge = '<span>User badge</span>'; + + beforeEach(() => { + createWrapper(defaultProps, { + 'avatar-badge': badge, + }); + }); + + it('should render provided `avatar-badge` slot content', () => { + expect(wrapper.html()).toContain(badge); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js index d3fec680b54..b36b83d1fea 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,118 +1,61 @@ -import { GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { each } from 'lodash'; -import { trimText } from 'helpers/text_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import UserAvatarLinkNew from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue'; +import UserAvatarLinkOld from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue'; + +const PROVIDED_PROPS = { + size: 32, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', +}; describe('User Avatar Link Component', () => { let wrapper; - const defaultProps = { - linkHref: `${TEST_HOST}/myavatarurl.com`, - imgSize: 99, - imgSrc: `${TEST_HOST}/myavatarurl.com`, - imgAlt: 'mydisplayname', - imgCssClasses: 'myextraavatarclass', - tooltipText: 'tooltip text', - tooltipPlacement: 'bottom', - username: 'username', - }; - - const createWrapper = (props) => { - wrapper = shallowMount(UserAvatarLink, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - beforeEach(() => { - createWrapper(); - }); - afterEach(() => { wrapper.destroy(); - wrapper = null; - }); - - it('should have user-avatar-image registered as child component', () => { - expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined(); - }); - - it('user-avatar-link should have user-avatar-image as child component', () => { - expect(wrapper.find(UserAvatarImage).exists()).toBe(true); - }); - - it('should render GlLink as a child element', () => { - const link = wrapper.find(GlLink); - - expect(link.exists()).toBe(true); - expect(link.attributes('href')).toBe(defaultProps.linkHref); - }); - - it('should return necessary props as defined', () => { - each(defaultProps, (val, key) => { - expect(wrapper.vm[key]).toBeDefined(); - }); }); - describe('no username', () => { + describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => { beforeEach(() => { - createWrapper({ - username: '', + wrapper = shallowMount(UserAvatarLink, { + propsData: { + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, }); }); - it('should only render image tag in link', () => { - const childElements = wrapper.vm.$el.childNodes; - - expect(wrapper.find('img')).not.toBe('null'); - - // Vue will render the hidden component as <!----> - expect(childElements[1].tagName).toBeUndefined(); - }); - - it('should render avatar image tooltip', () => { - expect(wrapper.vm.shouldShowUsername).toBe(false); - expect(wrapper.vm.avatarTooltipText).toEqual(defaultProps.tooltipText); + it('should render `UserAvatarLinkNew` component', () => { + expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(true); + expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(false); }); }); - describe('username', () => { - it('should not render avatar image tooltip', () => { - expect(wrapper.find('.js-user-avatar-image-tooltip').exists()).toBe(false); - }); - - it('should render username prop in <span>', () => { - expect(trimText(wrapper.find('.js-user-avatar-link-username').text())).toEqual( - defaultProps.username, - ); - }); - - it('should render text tooltip for <span>', () => { - expect(wrapper.find('.js-user-avatar-link-username').attributes('title')).toEqual( - defaultProps.tooltipText, - ); - }); - - it('should render text tooltip placement for <span>', () => { - expect(wrapper.find('.js-user-avatar-link-username').attributes('tooltip-placement')).toBe( - defaultProps.tooltipPlacement, - ); - }); - }); - - describe('lazy', () => { - it('passes lazy prop to avatar image', () => { - createWrapper({ - username: '', - lazy: true, + describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarLink, { + propsData: { + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: false, + }, + }, }); + }); - expect(wrapper.find(UserAvatarImage).props('lazy')).toBe(true); + it('should render `UserAvatarLinkOld` component', () => { + expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(false); + expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(true); }); }); }); 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 09633daf587..3329199a46b 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 @@ -271,6 +271,12 @@ describe('User Popover Component', () => { expect(securityBotDocsLink.text()).toBe('Learn more about GitLab Security Bot'); }); + it("does not show a link to the bot's documentation if there is no website_url", () => { + createWrapper({ user: { ...SECURITY_BOT_USER, websiteUrl: null } }); + const securityBotDocsLink = findSecurityBotDocsLink(); + expect(securityBotDocsLink.exists()).toBe(false); + }); + it("doesn't escape user's name", () => { createWrapper({ user: { ...SECURITY_BOT_USER, name: '%<>\';"' } }); const securityBotDocsLink = findSecurityBotDocsLink(); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index 411a15e1c74..cb476910944 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -1,4 +1,4 @@ -import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; +import { GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { cloneDeep } from 'lodash'; import Vue, { nextTick } from 'vue'; @@ -6,11 +6,14 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql'; -import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; +import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; +import { IssuableType } from '~/issues/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import { searchResponse, + searchResponseOnMR, projectMembersResponse, participantsQueryResponse, } from '../../sidebar/mock_data'; @@ -28,7 +31,7 @@ const assignee = { const mockError = jest.fn().mockRejectedValue('Error!'); const waitForSearch = async () => { - jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); await nextTick(); await waitForPromises(); }; @@ -58,6 +61,7 @@ describe('User select dropdown', () => { } = {}) => { fakeApollo = createMockApollo([ [searchUsersQuery, searchQueryHandler], + [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)], [getIssueParticipantsQuery, participantsQueryHandler], ]); wrapper = shallowMount(UserSelect, { @@ -76,7 +80,18 @@ describe('User select dropdown', () => { ...props, }, stubs: { - GlDropdown, + GlDropdown: { + template: ` + <div> + <slot name="header"></slot> + <slot></slot> + <slot name="footer"></slot> + </div> + `, + methods: { + hide: jest.fn(), + }, + }, }, }); }; @@ -132,11 +147,19 @@ describe('User select dropdown', () => { expect(findSelectedParticipants()).toHaveLength(1); }); + it('does not render a `Cannot merge` tooltip', async () => { + createComponent(); + await waitForPromises(); + + expect(findUnselectedParticipants().at(0).attributes('title')).toBe(''); + }); + describe('when search is empty', () => { it('renders a merged list of participants and project members', async () => { createComponent(); await waitForPromises(); - expect(findUnselectedParticipants()).toHaveLength(3); + + expect(findUnselectedParticipants()).toHaveLength(4); }); it('renders `Unassigned` link with the checkmark when there are no selected users', async () => { @@ -162,7 +185,7 @@ describe('User select dropdown', () => { }, }); await waitForPromises(); - findUnassignLink().vm.$emit('click'); + findUnassignLink().trigger('click'); expect(wrapper.emitted('input')).toEqual([[[]]]); }); @@ -175,7 +198,7 @@ describe('User select dropdown', () => { }); await waitForPromises(); - findSelectedParticipants().at(0).vm.$emit('click', new Event('click')); + findSelectedParticipants().at(0).trigger('click'); expect(wrapper.emitted('input')).toEqual([[[]]]); }); @@ -187,8 +210,9 @@ describe('User select dropdown', () => { }); await waitForPromises(); - findUnselectedParticipants().at(0).vm.$emit('click'); - expect(wrapper.emitted('input')).toEqual([ + findUnselectedParticipants().at(0).trigger('click'); + + expect(wrapper.emitted('input')).toMatchObject([ [ [ { @@ -214,7 +238,7 @@ describe('User select dropdown', () => { }); await waitForPromises(); - findUnselectedParticipants().at(0).vm.$emit('click'); + findUnselectedParticipants().at(0).trigger('click'); expect(wrapper.emitted('input')[0][0]).toHaveLength(2); }); }); @@ -232,7 +256,7 @@ describe('User select dropdown', () => { createComponent(); await waitForPromises(); findSearchField().vm.$emit('input', 'roo'); - jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); await nextTick(); expect(findParticipantsLoading().exists()).toBe(true); @@ -273,4 +297,19 @@ describe('User select dropdown', () => { expect(findEmptySearchResults().exists()).toBe(true); }); }); + + describe('when on merge request sidebar', () => { + beforeEach(() => { + createComponent({ props: { issuableType: IssuableType.MergeRequest, issuableId: 1 } }); + return waitForPromises(); + }); + + it('does not render a `Cannot merge` tooltip for a user that has merge permission', () => { + expect(findUnselectedParticipants().at(0).attributes('title')).toBe(''); + }); + + it('renders a `Cannot merge` tooltip for a user that does not have merge permission', () => { + expect(findUnselectedParticipants().at(1).attributes('title')).toBe('Cannot merge'); + }); + }); }); 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 5589cbfd08f..e79935f8fa6 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -12,6 +12,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help 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_PIPELINE_EDITOR_URL = '/-/ci/editor?branch_name="main"'; 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 forkPath = '/some/fork/path'; @@ -66,6 +67,16 @@ const ACTION_GITPOD_ENABLE = { href: undefined, handle: expect.any(Function), }; +const ACTION_PIPELINE_EDITOR = { + href: TEST_PIPELINE_EDITOR_URL, + key: 'pipeline_editor', + secondaryText: 'Edit, lint, and visualize your pipeline.', + tooltip: 'Edit, lint, and visualize your pipeline.', + text: 'Edit in pipeline editor', + attrs: { + 'data-qa-selector': 'pipeline_editor_button', + }, +}; describe('Web IDE link component', () => { let wrapper; @@ -76,6 +87,7 @@ describe('Web IDE link component', () => { editUrl: TEST_EDIT_URL, webIdeUrl: TEST_WEB_IDE_URL, gitpodUrl: TEST_GITPOD_URL, + pipelineEditorUrl: TEST_PIPELINE_EDITOR_URL, forkPath, ...props, }, @@ -107,6 +119,10 @@ describe('Web IDE link component', () => { expectedActions: [ACTION_WEB_IDE, ACTION_EDIT], }, { + props: { showPipelineEditorButton: true }, + expectedActions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_EDIT], + }, + { props: { webIdeText: 'Test Web IDE' }, expectedActions: [{ ...ACTION_WEB_IDE_EDIT_FORK, text: 'Test Web IDE' }, ACTION_EDIT], }, @@ -193,12 +209,34 @@ describe('Web IDE link component', () => { expect(findActionsButton().props('actions')).toEqual(expectedActions); }); + describe('when pipeline editor action is available', () => { + beforeEach(() => { + createComponent({ + showEditButton: false, + showWebIdeButton: true, + showGitpodButton: true, + showPipelineEditorButton: true, + userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, + userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, + gitpodEnabled: true, + }); + }); + + it('selected Pipeline Editor by default', () => { + expect(findActionsButton().props()).toMatchObject({ + actions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD], + selectedKey: ACTION_PIPELINE_EDITOR.key, + }); + }); + }); + describe('with multiple actions', () => { beforeEach(() => { createComponent({ showEditButton: false, showWebIdeButton: true, showGitpodButton: true, + showPipelineEditorButton: false, userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, gitpodEnabled: true, @@ -240,6 +278,7 @@ describe('Web IDE link component', () => { props: { showWebIdeButton: true, showEditButton: false, + showPipelineEditorButton: false, forkPath, forkModalId: 'edit-modal', }, @@ -249,6 +288,7 @@ describe('Web IDE link component', () => { props: { showWebIdeButton: false, showEditButton: true, + showPipelineEditorButton: false, forkPath, forkModalId: 'webide-modal', }, 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 93de6dbe306..11e3302d409 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 @@ -66,10 +66,12 @@ describe('IssuableTitle', () => { }); await nextTick(); - const titleEl = wrapperWithTitle.find('h2'); + const titleEl = wrapperWithTitle.find('[data-testid="title"]'); expect(titleEl.exists()).toBe(true); - expect(titleEl.html()).toBe('<h2 dir="auto" class="title qa-title"><b>Sample</b> title</h2>'); + expect(titleEl.html()).toBe( + '<h1 dir="auto" data-testid="title" class="title qa-title"><b>Sample</b> title</h1>', + ); wrapperWithTitle.destroy(); }); |