diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /spec/frontend/pipeline_editor/components | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/frontend/pipeline_editor/components')
13 files changed, 805 insertions, 126 deletions
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js new file mode 100644 index 00000000000..8a4f07c4d88 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js @@ -0,0 +1,47 @@ +import { getByRole } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; +import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue'; + +describe('First pipeline card', () => { + let wrapper; + + const defaultProvide = { + ciExamplesHelpPagePath: '/pipelines/examples', + runnerHelpPagePath: '/help/runners', + }; + + const createComponent = () => { + wrapper = mount(FirstPipelineCard, { + provide: { + ...defaultProvide, + }, + }); + }; + + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const findPipelinesLink = () => getLinkByName(/examples and templates/i); + const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i); + const findVisualReference = () => wrapper.findComponent(PipelineVisualReference); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(findVisualReference().exists()).toBe(true); + }); + + it('renders the links', () => { + expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath); + expect(findPipelinesLink()).toContain(defaultProvide.ciExamplesHelpPagePath); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js new file mode 100644 index 00000000000..c592e959068 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils'; +import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; + +describe('Getting started card', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(GettingStartedCard); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js new file mode 100644 index 00000000000..3c8821d05a7 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js @@ -0,0 +1,51 @@ +import { getByRole } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue'; + +describe('Pipeline config reference card', () => { + let wrapper; + + const defaultProvide = { + ciExamplesHelpPagePath: 'help/ci/examples/', + ciHelpPagePath: 'help/ci/introduction', + needsHelpPagePath: 'help/ci/yaml#needs', + ymlHelpPagePath: 'help/ci/yaml', + }; + + const createComponent = () => { + wrapper = mount(PipelineConfigReferenceCard, { + provide: { + ...defaultProvide, + }, + }); + }; + + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const findCiExamplesLink = () => getLinkByName(/CI\/CD examples and templates/i); + const findCiIntroLink = () => getLinkByName(/GitLab CI\/CD concepts/i); + const findNeedsLink = () => getLinkByName(/Needs keyword/i); + const findYmlSyntaxLink = () => getLinkByName(/.gitlab-ci.yml syntax reference/i); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); + + it('renders the links', () => { + expect(findCiExamplesLink()).toContain(defaultProvide.ciExamplesHelpPagePath); + expect(findCiIntroLink()).toContain(defaultProvide.ciHelpPagePath); + expect(findNeedsLink()).toContain(defaultProvide.needsHelpPagePath); + expect(findYmlSyntaxLink()).toContain(defaultProvide.ymlHelpPagePath); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js new file mode 100644 index 00000000000..bebd2484c1d --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils'; +import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; + +describe('Visual and Lint card', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(VisualizeAndLintCard); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js new file mode 100644 index 00000000000..1b68cd3dc43 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js @@ -0,0 +1,142 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; +import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; +import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue'; +import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue'; +import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; +import { DRAWER_EXPANDED_KEY } from '~/pipeline_editor/constants'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +describe('Pipeline editor drawer', () => { + useLocalStorageSpy(); + + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineEditorDrawer, { + stubs: { LocalStorageSync }, + }); + }; + + const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard); + const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard); + const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard); + const findToggleBtn = () => wrapper.findComponent(GlButton); + const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard); + + const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]'); + const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]'); + const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]'); + + const clickToggleBtn = async () => findToggleBtn().vm.$emit('click'); + + afterEach(() => { + wrapper.destroy(); + localStorage.clear(); + }); + + it('it sets the drawer to be opened by default', async () => { + createComponent(); + + expect(findDrawerContent().exists()).toBe(false); + + await nextTick(); + + expect(findDrawerContent().exists()).toBe(true); + }); + + describe('when the drawer is collapsed', () => { + beforeEach(async () => { + createComponent(); + await clickToggleBtn(); + }); + + it('shows the left facing arrow icon', () => { + expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left'); + }); + + it('does not show the collapse text', () => { + expect(findCollapseText().exists()).toBe(false); + }); + + it('does not show the drawer content', () => { + expect(findDrawerContent().exists()).toBe(false); + }); + + it('can open the drawer by clicking on the toggle button', async () => { + expect(findDrawerContent().exists()).toBe(false); + + await clickToggleBtn(); + + expect(findDrawerContent().exists()).toBe(true); + }); + }); + + describe('when the drawer is expanded', () => { + beforeEach(async () => { + createComponent(); + }); + + it('shows the right facing arrow icon', () => { + expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right'); + }); + + it('shows the collapse text', () => { + expect(findCollapseText().exists()).toBe(true); + }); + + it('shows the drawer content', () => { + expect(findDrawerContent().exists()).toBe(true); + }); + + it('shows all the introduction cards', () => { + expect(findFirstPipelineCard().exists()).toBe(true); + expect(findGettingStartedCard().exists()).toBe(true); + expect(findPipelineConfigReferenceCard().exists()).toBe(true); + expect(findVisualizeAndLintCard().exists()).toBe(true); + }); + + it('can close the drawer by clicking on the toggle button', async () => { + expect(findDrawerContent().exists()).toBe(true); + + await clickToggleBtn(); + + expect(findDrawerContent().exists()).toBe(false); + }); + }); + + describe('local storage', () => { + it('saves the drawer expanded value to local storage', async () => { + localStorage.setItem(DRAWER_EXPANDED_KEY, 'false'); + + createComponent(); + await clickToggleBtn(); + + expect(localStorage.setItem.mock.calls).toEqual([ + [DRAWER_EXPANDED_KEY, 'false'], + [DRAWER_EXPANDED_KEY, 'true'], + ]); + }); + + it('loads the drawer collapsed when local storage is set to `false`, ', async () => { + localStorage.setItem(DRAWER_EXPANDED_KEY, false); + createComponent(); + + await nextTick(); + + expect(findDrawerContent().exists()).toBe(false); + }); + + it('loads the drawer expanded when local storage is set to `true`, ', async () => { + localStorage.setItem(DRAWER_EXPANDED_KEY, true); + createComponent(); + + await nextTick(); + + expect(findDrawerContent().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js new file mode 100644 index 00000000000..edd2b45569a --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue'; + +describe('Demo job pill', () => { + let wrapper; + const jobName = 'my-build-job'; + + const createComponent = () => { + wrapper = shallowMount(DemoJobPill, { + propsData: { + jobName, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the jobName', () => { + expect(wrapper.text()).toContain(jobName); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js new file mode 100644 index 00000000000..e4834544484 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js @@ -0,0 +1,31 @@ +import { shallowMount } from '@vue/test-utils'; +import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue'; +import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue'; + +describe('Demo job pill', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineVisualReference); + }; + + const findAllDemoJobPills = () => wrapper.findAllComponents(DemoJobPill); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all stage names', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.build); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.test); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.deploy); + }); + + it('renders all job pills', () => { + expect(findAllDemoJobPills()).toHaveLength(4); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js index 3bf5a291c69..7a5b01fb04a 100644 --- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { EDITOR_READY_EVENT } from '~/editor/constants'; +import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import { mockCiConfigPath, @@ -59,6 +60,10 @@ describe('Pipeline Editor | Text editor component', () => { const findEditor = () => wrapper.findComponent(MockEditorLite); + beforeEach(() => { + EditorLiteExtension.deferRerender = jest.fn(); + }); + afterEach(() => { wrapper.destroy(); wrapper = null; diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index fa937100982..d6763a7de41 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -1,11 +1,28 @@ -import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { + GlDropdown, + GlDropdownItem, + GlInfiniteScroll, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue'; import { DEFAULT_FAILURE } from '~/pipeline_editor/constants'; -import { mockDefaultBranch, mockProjectBranches, mockProjectFullPath } from '../../mock_data'; +import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql'; +import { + mockBranchPaginationLimit, + mockDefaultBranch, + mockEmptySearchBranches, + mockProjectBranches, + mockProjectFullPath, + mockSearchBranches, + mockTotalBranches, + mockTotalBranchResults, + mockTotalSearchResults, +} from '../../mock_data'; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -15,30 +32,64 @@ describe('Pipeline editor branch switcher', () => { let mockApollo; let mockAvailableBranchQuery; - const createComponentWithApollo = () => { - const resolvers = { - Query: { - project: mockAvailableBranchQuery, + const createComponent = ( + { isQueryLoading, mountFn, options } = { + isQueryLoading: false, + mountFn: shallowMount, + options: {}, + }, + ) => { + wrapper = mountFn(BranchSwitcher, { + propsData: { + paginationLimit: mockBranchPaginationLimit, }, - }; - - mockApollo = createMockApollo([], resolvers); - wrapper = shallowMount(BranchSwitcher, { - localVue, - apolloProvider: mockApollo, provide: { projectFullPath: mockProjectFullPath, + totalBranches: mockTotalBranches, + }, + mocks: { + $apollo: { + queries: { + availableBranches: { + loading: isQueryLoading, + }, + }, + }, }, data() { return { + branches: ['main'], currentBranch: mockDefaultBranch, }; }, + ...options, + }); + }; + + const createComponentWithApollo = (mountFn = shallowMount) => { + const handlers = [[getAvailableBranches, mockAvailableBranchQuery]]; + mockApollo = createMockApollo(handlers); + + createComponent({ + mountFn, + options: { + localVue, + apolloProvider: mockApollo, + mocks: {}, + data() { + return { + currentBranch: mockDefaultBranch, + }; + }, + }, }); }; const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll); beforeEach(() => { mockAvailableBranchQuery = jest.fn(); @@ -48,7 +99,7 @@ describe('Pipeline editor branch switcher', () => { wrapper.destroy(); }); - describe('while querying', () => { + describe('when querying for the first time', () => { beforeEach(() => { createComponentWithApollo(); }); @@ -61,41 +112,31 @@ describe('Pipeline editor branch switcher', () => { describe('after querying', () => { beforeEach(async () => { mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); - createComponentWithApollo(); + createComponentWithApollo(mount); await waitForPromises(); }); - it('query is called with correct variables', async () => { - expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1); - expect(mockAvailableBranchQuery).toHaveBeenCalledWith( - expect.anything(), - { - fullPath: mockProjectFullPath, - }, - expect.anything(), - expect.anything(), - ); + it('renders search box', () => { + expect(findSearchBox().exists()).toBe(true); }); it('renders list of branches', () => { expect(findDropdown().exists()).toBe(true); - expect(findDropdownItems()).toHaveLength(mockProjectBranches.repository.branches.length); + expect(findDropdownItems()).toHaveLength(mockTotalBranchResults); }); - it('renders current branch at the top of the list with a check mark', () => { - const firstDropdownItem = findDropdownItems().at(0); - const icon = firstDropdownItem.findComponent(GlIcon); + it('renders current branch with a check mark', () => { + const defaultBranchInDropdown = findDropdownItems().at(0); - expect(firstDropdownItem.text()).toBe(mockDefaultBranch); - expect(icon.exists()).toBe(true); - expect(icon.props('name')).toBe('check'); + expect(defaultBranchInDropdown.text()).toBe(mockDefaultBranch); + expect(defaultBranchInDropdown.props('isChecked')).toBe(true); }); it('does not render check mark for other branches', () => { - const secondDropdownItem = findDropdownItems().at(1); - const icon = secondDropdownItem.findComponent(GlIcon); + const nonDefaultBranch = findDropdownItems().at(1); - expect(icon.classes()).toContain('gl-visibility-hidden'); + expect(nonDefaultBranch.text()).not.toBe(mockDefaultBranch); + expect(nonDefaultBranch.props('isChecked')).toBe(false); }); }); @@ -120,4 +161,186 @@ describe('Pipeline editor branch switcher', () => { ]); }); }); + + describe('when switching branches', () => { + beforeEach(async () => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => {}); + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + createComponentWithApollo(mount); + await waitForPromises(); + }); + + it('updates session history when selecting a different branch', async () => { + const branch = findDropdownItems().at(1); + await branch.vm.$emit('click'); + + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`); + }); + + it('does not update session history when selecting current branch', async () => { + const branch = findDropdownItems().at(0); + await branch.vm.$emit('click'); + + expect(branch.text()).toBe(mockDefaultBranch); + expect(window.history.pushState).not.toHaveBeenCalled(); + }); + + it('emits the refetchContent event when selecting a different branch', async () => { + const branch = findDropdownItems().at(1); + + expect(branch.text()).not.toBe(mockDefaultBranch); + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + + await branch.vm.$emit('click'); + + expect(wrapper.emitted('refetchContent')).toBeDefined(); + expect(wrapper.emitted('refetchContent')).toHaveLength(1); + }); + + it('does not emit the refetchContent event when selecting the current branch', async () => { + const branch = findDropdownItems().at(0); + + expect(branch.text()).toBe(mockDefaultBranch); + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + + await branch.vm.$emit('click'); + + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + }); + }); + + describe('when searching', () => { + beforeEach(async () => { + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + createComponentWithApollo(mount); + await waitForPromises(); + + mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches); + }); + + describe('with a search term', () => { + it('calls query with correct variables', async () => { + findSearchBox().vm.$emit('input', 'te'); + await waitForPromises(); + + expect(mockAvailableBranchQuery).toHaveBeenCalledWith({ + limit: mockTotalBranches, // fetch all branches + offset: 0, + projectFullPath: mockProjectFullPath, + searchPattern: '*te*', + }); + }); + + it('fetches new list of branches', async () => { + expect(findDropdownItems()).toHaveLength(mockTotalBranchResults); + + findSearchBox().vm.$emit('input', 'te'); + await waitForPromises(); + + expect(findDropdownItems()).toHaveLength(mockTotalSearchResults); + }); + + it('does not hide dropdown when search result is empty', async () => { + mockAvailableBranchQuery.mockResolvedValue(mockEmptySearchBranches); + findSearchBox().vm.$emit('input', 'aaaaa'); + await waitForPromises(); + + expect(findDropdown().exists()).toBe(true); + expect(findDropdownItems()).toHaveLength(0); + }); + }); + + describe('without a search term', () => { + beforeEach(async () => { + findSearchBox().vm.$emit('input', 'te'); + await waitForPromises(); + + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + }); + + it('calls query with correct variables', async () => { + findSearchBox().vm.$emit('input', ''); + await waitForPromises(); + + expect(mockAvailableBranchQuery).toHaveBeenCalledWith({ + limit: mockBranchPaginationLimit, // only fetch first n branches first + offset: 0, + projectFullPath: mockProjectFullPath, + searchPattern: '*', + }); + }); + + it('fetches new list of branches', async () => { + expect(findDropdownItems()).toHaveLength(mockTotalSearchResults); + + findSearchBox().vm.$emit('input', ''); + await waitForPromises(); + + expect(findDropdownItems()).toHaveLength(mockTotalBranchResults); + }); + }); + }); + + describe('loading icon', () => { + test.each` + isQueryLoading | isRendered + ${true} | ${true} + ${false} | ${false} + `('checks if query is loading before rendering', ({ isQueryLoading, isRendered }) => { + createComponent({ isQueryLoading, mountFn: mount }); + + expect(findLoadingIcon().exists()).toBe(isRendered); + }); + }); + + describe('when scrolling to the bottom of the list', () => { + beforeEach(async () => { + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + createComponentWithApollo(); + await waitForPromises(); + }); + + afterEach(() => { + mockAvailableBranchQuery.mockClear(); + }); + + describe('when search term is empty', () => { + it('fetches more branches', async () => { + expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1); + + findInfiniteScroll().vm.$emit('bottomReached'); + await waitForPromises(); + + expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2); + }); + + it('calls the query with the correct variables', async () => { + findInfiniteScroll().vm.$emit('bottomReached'); + await waitForPromises(); + + expect(mockAvailableBranchQuery).toHaveBeenCalledWith({ + limit: mockBranchPaginationLimit, + offset: mockBranchPaginationLimit, // offset changed + projectFullPath: mockProjectFullPath, + searchPattern: '*', + }); + }); + }); + + describe('when search term exists', () => { + it('does not fetch more branches', async () => { + findSearchBox().vm.$emit('input', 'te'); + await waitForPromises(); + + expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2); + mockAvailableBranchQuery.mockClear(); + + findInfiniteScroll().vm.$emit('bottomReached'); + await waitForPromises(); + + expect(mockAvailableBranchQuery).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js index 27652bb268b..e1dc08b637f 100644 --- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js @@ -7,16 +7,10 @@ import { mockCiYml, mockLintResponse } from '../../mock_data'; describe('Pipeline editor header', () => { let wrapper; - const mockProvide = { - glFeatures: { - pipelineStatusForPipelineEditor: true, - }, - }; const createComponent = ({ provide = {}, props = {} } = {}) => { wrapper = shallowMount(PipelineEditorHeader, { provide: { - ...mockProvide, ...provide, }, propsData: { @@ -56,18 +50,4 @@ describe('Pipeline editor header', () => { expect(findValidationSegment().exists()).toBe(true); }); }); - - describe('with pipeline status feature flag off', () => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures: { pipelineStatusForPipelineEditor: false }, - }, - }); - }); - - it('does not render the pipeline status', () => { - expect(findPipelineStatus().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index eba853180cd..5cf8d47bc23 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -20,12 +20,6 @@ describe('Pipeline editor tabs component', () => { const MockTextEditor = { template: '<div />', }; - const mockProvide = { - glFeatures: { - ciConfigVisualizationTab: true, - ciConfigMergedTab: true, - }, - }; const createComponent = ({ props = {}, @@ -44,7 +38,7 @@ describe('Pipeline editor tabs component', () => { appStatus, }; }, - provide: { ...mockProvide, ...provide }, + provide: { ...provide }, stubs: { TextEditor: MockTextEditor, EditorTab, @@ -82,41 +76,24 @@ describe('Pipeline editor tabs component', () => { }); describe('visualization tab', () => { - describe('with feature flag on', () => { - describe('while loading', () => { - beforeEach(() => { - createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); - }); - - it('displays a loading icon if the lint query is loading', () => { - expect(findLoadingIcon().exists()).toBe(true); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - describe('after loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('display the tab and visualization', () => { - expect(findVisualizationTab().exists()).toBe(true); - expect(findPipelineGraph().exists()).toBe(true); - }); + describe('while loading', () => { + beforeEach(() => { + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); }); - }); - describe('with feature flag off', () => { + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); + describe('after loading', () => { beforeEach(() => { - createComponent({ - provide: { - glFeatures: { ciConfigVisualizationTab: false }, - }, - }); + createComponent(); }); - it('does not display the tab or component', () => { - expect(findVisualizationTab().exists()).toBe(false); - expect(findPipelineGraph().exists()).toBe(false); + it('display the tab and visualization', () => { + expect(findVisualizationTab().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(true); }); }); }); @@ -148,51 +125,39 @@ describe('Pipeline editor tabs component', () => { }); describe('merged tab', () => { - describe('with feature flag on', () => { - describe('while loading', () => { - beforeEach(() => { - createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); - }); - - it('displays a loading icon if the lint query is loading', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); + describe('while loading', () => { + beforeEach(() => { + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); }); - describe('when there is a fetch error', () => { - beforeEach(() => { - createComponent({ appStatus: EDITOR_APP_STATUS_ERROR }); - }); - - it('show an error message', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts.loadMergedYaml); - }); + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); - it('does not render the `meged_preview` component', () => { - expect(findMergedPreview().exists()).toBe(false); - }); + describe('when there is a fetch error', () => { + beforeEach(() => { + createComponent({ appStatus: EDITOR_APP_STATUS_ERROR }); }); - describe('after loading', () => { - beforeEach(() => { - createComponent(); - }); + it('show an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts.loadMergedYaml); + }); - it('display the tab and the merged preview component', () => { - expect(findMergedTab().exists()).toBe(true); - expect(findMergedPreview().exists()).toBe(true); - }); + it('does not render the `merged_preview` component', () => { + expect(findMergedPreview().exists()).toBe(false); }); }); - describe('with feature flag off', () => { + + describe('after loading', () => { beforeEach(() => { - createComponent({ provide: { glFeatures: { ciConfigMergedTab: false } } }); + createComponent(); }); - it('does not display the merged tab', () => { - expect(findMergedTab().exists()).toBe(false); - expect(findMergedPreview().exists()).toBe(false); + it('display the tab and the merged preview component', () => { + expect(findMergedTab().exists()).toBe(true); + expect(findMergedPreview().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js index b444d9dcfea..76c68e21180 100644 --- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js @@ -1,11 +1,13 @@ import { GlButton, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; describe('Pipeline editor empty state', () => { let wrapper; const defaultProvide = { glFeatures: { + pipelineEditorBranchSwitcher: true, pipelineEditorEmptyStateAction: false, }, emptyStateIllustrationPath: 'my/svg/path', @@ -17,6 +19,7 @@ describe('Pipeline editor empty state', () => { }); }; + const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav); const findSvgImage = () => wrapper.find('img'); const findTitle = () => wrapper.find('h1'); const findConfirmButton = () => wrapper.findComponent(GlButton); @@ -45,6 +48,10 @@ describe('Pipeline editor empty state', () => { expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body); }); + it('renders the file nav', () => { + expect(findFileNav().exists()).toBe(true); + }); + describe('with feature flag off', () => { it('does not renders a CTA button', () => { expect(findConfirmButton().exists()).toBe(false); @@ -75,5 +82,17 @@ describe('Pipeline editor empty state', () => { await findConfirmButton().vm.$emit('click'); expect(wrapper.emitted(expectedEvent)).toHaveLength(1); }); + + describe('with branch switcher feature flag OFF', () => { + it('does not render the file nav', () => { + createComponent({ + provide: { + glFeatures: { pipelineEditorBranchSwitcher: false }, + }, + }); + + expect(findFileNav().exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js new file mode 100644 index 00000000000..93ebbc648fe --- /dev/null +++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js @@ -0,0 +1,137 @@ +import { GlAlert } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { TEST_HOST } from 'helpers/test_constants'; +import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue'; +import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants'; +import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue'; +import { + COMMIT_FAILURE, + COMMIT_SUCCESS, + DEFAULT_FAILURE, + DEFAULT_SUCCESS, + LOAD_FAILURE_UNKNOWN, +} from '~/pipeline_editor/constants'; + +describe('Pipeline Editor messages', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(PipelineEditorMessages, { + propsData: props, + }); + }; + + const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert); + const findAlert = () => wrapper.findComponent(GlAlert); + + describe('success alert', () => { + it('shows a message for successful commit type', () => { + createComponent({ successType: COMMIT_SUCCESS, showSuccess: true }); + + expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]); + }); + + it('does not show alert when there is a successType but visibility is off', () => { + createComponent({ successType: COMMIT_SUCCESS, showSuccess: false }); + + expect(findAlert().exists()).toBe(false); + }); + + it('shows a success alert with default copy if `showSuccess` is true and the `successType` is not valid,', () => { + createComponent({ successType: 'random', showSuccess: true }); + + expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[DEFAULT_SUCCESS]); + }); + + it('emit `hide-success` event when clicking on the dismiss button', async () => { + const expectedEvent = 'hide-success'; + + createComponent({ successType: COMMIT_SUCCESS, showSuccess: true }); + expect(wrapper.emitted(expectedEvent)).not.toBeDefined(); + + await findAlert().vm.$emit('dismiss'); + + expect(wrapper.emitted(expectedEvent)).toBeDefined(); + }); + }); + + describe('failure alert', () => { + it.each` + failureType | message | expectedFailureType + ${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE} + ${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN} + ${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE} + `('shows a message for $message', ({ failureType, expectedFailureType }) => { + createComponent({ failureType, showFailure: true }); + + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[expectedFailureType]); + }); + + it('show failure reasons when there are some', () => { + const failureReasons = ['There was a problem', 'ouppps']; + createComponent({ failureType: COMMIT_FAILURE, failureReasons, showFailure: true }); + + expect(wrapper.html()).toContain(failureReasons[0]); + expect(wrapper.html()).toContain(failureReasons[1]); + }); + + it('does not show a message for error with a disabled visibility', () => { + createComponent({ failureType: 'random', showFailure: false }); + + expect(findAlert().exists()).toBe(false); + }); + + it('emit `hide-failure` event when clicking on the dismiss button', async () => { + const expectedEvent = 'hide-failure'; + + createComponent({ failureType: COMMIT_FAILURE, showFailure: true }); + expect(wrapper.emitted(expectedEvent)).not.toBeDefined(); + + await findAlert().vm.$emit('dismiss'); + + expect(wrapper.emitted(expectedEvent)).toBeDefined(); + }); + }); + + describe('code snippet alert', () => { + const setCodeSnippetUrlParam = (value) => { + global.jsdom.reconfigure({ + url: `${TEST_HOST}/?code_snippet_copied_from=${value}`, + }); + }; + + it('does not show by default', () => { + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(false); + }); + + it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => { + jest.spyOn(window.history, 'replaceState'); + setCodeSnippetUrlParam(source); + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(true); + expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`); + }); + + it('does not show if URL param is invalid', () => { + setCodeSnippetUrlParam('foo_bar'); + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(false); + }); + + it('disappears on dismiss', async () => { + setCodeSnippetUrlParam('api_fuzzing'); + createComponent(); + const alert = findCodeSnippetAlert(); + + expect(alert.exists()).toBe(true); + + await alert.vm.$emit('dismiss'); + + expect(alert.exists()).toBe(false); + }); + }); +}); |