diff options
Diffstat (limited to 'spec/frontend/jobs/components')
7 files changed, 253 insertions, 163 deletions
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js index d4e1e711777..06ebcd7f134 100644 --- a/spec/frontend/jobs/components/job_app_spec.js +++ b/spec/frontend/jobs/components/job_app_spec.js @@ -34,7 +34,6 @@ describe('Job App', () => { const props = { artifactHelpUrl: 'help/artifact', deploymentHelpUrl: 'help/deployment', - codeQualityHelpPath: '/help/code_quality', runnerSettingsUrl: 'settings/ci-cd/runners', terminalPath: 'jobs/123/terminal', projectPath: 'user-name/project-name', diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js index 226322a2951..cd3ee734466 100644 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -8,7 +8,6 @@ describe('Job log controllers', () => { afterEach(() => { if (wrapper?.destroy) { wrapper.destroy(); - wrapper = null; } }); @@ -34,7 +33,6 @@ describe('Job log controllers', () => { const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]'); const findRawLink = () => wrapper.find('[data-testid="raw-link"]'); const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); - const findEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]'); const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); @@ -76,28 +74,6 @@ describe('Job log controllers', () => { expect(findRawLinkController().exists()).toBe(false); }); }); - - describe('when is erasable', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders erase job link', () => { - expect(findEraseLink().exists()).toBe(true); - }); - }); - - describe('when it is not erasable', () => { - beforeEach(() => { - createWrapper({ - erasePath: null, - }); - }); - - it('does not render erase button', () => { - expect(findEraseLink().exists()).toBe(false); - }); - }); }); describe('scroll buttons', () => { diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js index 6914b8d4fa1..ad72b9be261 100644 --- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js +++ b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js @@ -1,5 +1,4 @@ -import { GlButton, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue'; import createStore from '~/jobs/store'; import job from '../mock_data'; @@ -9,12 +8,12 @@ describe('Job Sidebar Retry Button', () => { let wrapper; const forwardDeploymentFailure = 'forward_deployment_failure'; - const findRetryButton = () => wrapper.find(GlButton); - const findRetryLink = () => wrapper.find(GlLink); + const findRetryButton = () => wrapper.findByTestId('retry-job-button'); + const findRetryLink = () => wrapper.findByTestId('retry-job-link'); const createWrapper = ({ props = {} } = {}) => { store = createStore(); - wrapper = shallowMount(JobsSidebarRetryButton, { + wrapper = shallowMountExtended(JobsSidebarRetryButton, { propsData: { href: job.retry_path, modalId: 'modal-id', @@ -27,7 +26,6 @@ describe('Job Sidebar Retry Button', () => { afterEach(() => { if (wrapper) { wrapper.destroy(); - wrapper = null; } }); @@ -44,7 +42,6 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryButton().exists()).toBe(buttonExists); expect(findRetryLink().exists()).toBe(linkExists); - expect(wrapper.text()).toMatch('Retry'); }, ); @@ -55,6 +52,7 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryButton().attributes()).toMatchObject({ category: 'primary', variant: 'confirm', + icon: 'retry', }); }); }); @@ -64,6 +62,7 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryLink().attributes()).toMatchObject({ 'data-method': 'post', href: job.retry_path, + icon: 'retry', }); }); }); diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js index 6e327725627..39c71986ce4 100644 --- a/spec/frontend/jobs/components/sidebar_spec.js +++ b/spec/frontend/jobs/components/sidebar_spec.js @@ -21,25 +21,54 @@ describe('Sidebar details block', () => { const findNewIssueButton = () => wrapper.findByTestId('job-new-issue'); const findRetryButton = () => wrapper.find(JobRetryButton); const findTerminalLink = () => wrapper.findByTestId('terminal-link'); + const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - const createWrapper = ({ props = {} } = {}) => { + const createWrapper = (props) => { store = createStore(); store.state.job = job; wrapper = extendedWrapper( shallowMount(Sidebar, { - ...props, + propsData: { + ...props, + }, + store, }), ); }; afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } + wrapper.destroy(); + }); + + describe('when job log is erasable', () => { + const path = '/root/ci-project/-/jobs/1447/erase'; + + beforeEach(() => { + createWrapper({ + erasePath: path, + }); + }); + + it('renders erase job link', () => { + expect(findEraseLink().exists()).toBe(true); + }); + + it('erase job link has correct path', () => { + expect(findEraseLink().attributes('href')).toBe(path); + }); + }); + + describe('when job log is not erasable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('does not render erase button', () => { + expect(findEraseLink().exists()).toBe(false); + }); }); describe('when there is no retry path retry', () => { @@ -86,7 +115,7 @@ describe('Sidebar details block', () => { }); it('should render link to cancel job', () => { - expect(findCancelButton().text()).toMatch('Cancel'); + expect(findCancelButton().props('icon')).toBe('cancel'); expect(findCancelButton().attributes('href')).toBe(job.cancel_path); }); }); diff --git a/spec/frontend/jobs/components/stages_dropdown_spec.js b/spec/frontend/jobs/components/stages_dropdown_spec.js index b0e95a2d5b6..f638213ef0c 100644 --- a/spec/frontend/jobs/components/stages_dropdown_spec.js +++ b/spec/frontend/jobs/components/stages_dropdown_spec.js @@ -1,10 +1,12 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { trimText } from 'helpers/text_helper'; +import Mousetrap from 'mousetrap'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import StagesDropdown from '~/jobs/components/stages_dropdown.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import * as copyToClipboard from '~/behaviors/copy_to_clipboard'; import { + mockPipelineWithoutRef, mockPipelineWithoutMR, mockPipelineWithAttachedMR, mockPipelineDetached, @@ -18,20 +20,19 @@ describe('Stages Dropdown', () => { const findStageItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); const findPipelineInfoText = () => wrapper.findByTestId('pipeline-info').text(); - const findPipelinePath = () => wrapper.findByTestId('pipeline-path').attributes('href'); - const findMRLinkPath = () => wrapper.findByTestId('mr-link').attributes('href'); - const findCopySourceBranchBtn = () => wrapper.findByTestId('copy-source-ref-link'); - const findSourceBranchLinkPath = () => - wrapper.findByTestId('source-branch-link').attributes('href'); - const findTargetBranchLinkPath = () => - wrapper.findByTestId('target-branch-link').attributes('href'); const createComponent = (props) => { wrapper = extendedWrapper( shallowMount(StagesDropdown, { propsData: { + stages: [], + selectedStage: 'deploy', ...props, }, + stubs: { + GlSprintf, + GlLink, + }, }), ); }; @@ -45,7 +46,6 @@ describe('Stages Dropdown', () => { createComponent({ pipeline: mockPipelineWithoutMR, stages: [{ name: 'build' }, { name: 'test' }], - selectedStage: 'deploy', }); }); @@ -53,10 +53,6 @@ describe('Stages Dropdown', () => { expect(findStatus().exists()).toBe(true); }); - it('renders pipeline link', () => { - expect(findPipelinePath()).toBe('pipeline/28029444'); - }); - it('renders dropdown with stages', () => { expect(findStageItem(0).text()).toBe('build'); }); @@ -64,84 +60,133 @@ describe('Stages Dropdown', () => { it('rendes selected stage', () => { expect(findSelectedStageText()).toBe('deploy'); }); - - it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => { - const expected = `Pipeline #${mockPipelineWithoutMR.id} for ${mockPipelineWithoutMR.ref.name}`; - const actual = trimText(findPipelineInfoText()); - - expect(actual).toBe(expected); - }); - - it(`renders the source ref copy button`, () => { - expect(findCopySourceBranchBtn().exists()).toBe(true); - }); }); - describe('with an "attached" merge request pipeline', () => { - beforeEach(() => { - createComponent({ - pipeline: mockPipelineWithAttachedMR, - stages: [], - selectedStage: 'deploy', + describe('pipelineInfo', () => { + const allElements = [ + 'pipeline-path', + 'mr-link', + 'source-ref-link', + 'copy-source-ref-link', + 'source-branch-link', + 'copy-source-branch-link', + 'target-branch-link', + 'copy-target-branch-link', + ]; + describe.each([ + [ + 'does not have a ref', + { + pipeline: mockPipelineWithoutRef, + text: `Pipeline #${mockPipelineWithoutRef.id}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutRef.path }] }, + ], + }, + ], + [ + 'hasRef but not triggered by MR', + { + pipeline: mockPipelineWithoutMR, + text: `Pipeline #${mockPipelineWithoutMR.id} for ${mockPipelineWithoutMR.ref.name}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutMR.path }] }, + { testId: 'source-ref-link', props: [{ href: mockPipelineWithoutMR.ref.path }] }, + { testId: 'copy-source-ref-link', props: [{ text: mockPipelineWithoutMR.ref.name }] }, + ], + }, + ], + [ + 'hasRef and MR but not MR pipeline', + { + pipeline: mockPipelineDetached, + text: `Pipeline #${mockPipelineDetached.id} for !${mockPipelineDetached.merge_request.iid} with ${mockPipelineDetached.merge_request.source_branch}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineDetached.path }] }, + { testId: 'mr-link', props: [{ href: mockPipelineDetached.merge_request.path }] }, + { + testId: 'source-branch-link', + props: [{ href: mockPipelineDetached.merge_request.source_branch_path }], + }, + { + testId: 'copy-source-branch-link', + props: [{ text: mockPipelineDetached.merge_request.source_branch }], + }, + ], + }, + ], + [ + 'hasRef and MR and MR pipeline', + { + pipeline: mockPipelineWithAttachedMR, + text: `Pipeline #${mockPipelineWithAttachedMR.id} for !${mockPipelineWithAttachedMR.merge_request.iid} with ${mockPipelineWithAttachedMR.merge_request.source_branch} into ${mockPipelineWithAttachedMR.merge_request.target_branch}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithAttachedMR.path }] }, + { testId: 'mr-link', props: [{ href: mockPipelineWithAttachedMR.merge_request.path }] }, + { + testId: 'source-branch-link', + props: [{ href: mockPipelineWithAttachedMR.merge_request.source_branch_path }], + }, + { + testId: 'copy-source-branch-link', + props: [{ text: mockPipelineWithAttachedMR.merge_request.source_branch }], + }, + { + testId: 'target-branch-link', + props: [{ href: mockPipelineWithAttachedMR.merge_request.target_branch_path }], + }, + { + testId: 'copy-target-branch-link', + props: [{ text: mockPipelineWithAttachedMR.merge_request.target_branch }], + }, + ], + }, + ], + ])('%s', (_, { pipeline, text, foundElements }) => { + beforeEach(() => { + createComponent({ + pipeline, + }); }); - }); - it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => { - const expected = `Pipeline #${mockPipelineWithAttachedMR.id} for !${mockPipelineWithAttachedMR.merge_request.iid} with ${mockPipelineWithAttachedMR.merge_request.source_branch} into ${mockPipelineWithAttachedMR.merge_request.target_branch}`; - const actual = trimText(findPipelineInfoText()); - - expect(actual).toBe(expected); - }); - - it(`renders the correct merge request link`, () => { - expect(findMRLinkPath()).toBe(mockPipelineWithAttachedMR.merge_request.path); - }); - - it(`renders the correct source branch link`, () => { - expect(findSourceBranchLinkPath()).toBe( - mockPipelineWithAttachedMR.merge_request.source_branch_path, - ); - }); - - it(`renders the correct target branch link`, () => { - expect(findTargetBranchLinkPath()).toBe( - mockPipelineWithAttachedMR.merge_request.target_branch_path, - ); - }); - - it(`renders the source ref copy button`, () => { - expect(findCopySourceBranchBtn().exists()).toBe(true); - }); - }); - - describe('with a detached merge request pipeline', () => { - beforeEach(() => { - createComponent({ - pipeline: mockPipelineDetached, - stages: [], - selectedStage: 'deploy', + it('should render the text', () => { + expect(findPipelineInfoText()).toMatchInterpolatedText(text); }); - }); - it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => { - const expected = `Pipeline #${mockPipelineDetached.id} for !${mockPipelineDetached.merge_request.iid} with ${mockPipelineDetached.merge_request.source_branch}`; - const actual = trimText(findPipelineInfoText()); + it('should find components with props', () => { + foundElements.forEach((element) => { + element.props.forEach((prop) => { + const key = Object.keys(prop)[0]; + expect(wrapper.findByTestId(element.testId).attributes(key)).toBe(prop[key]); + }); + }); + }); - expect(actual).toBe(expected); + it('should not find components', () => { + const foundTestIds = foundElements.map((element) => element.testId); + allElements + .filter((testId) => !foundTestIds.includes(testId)) + .forEach((testId) => { + expect(wrapper.findByTestId(testId).exists()).toBe(false); + }); + }); }); + }); - it(`renders the correct merge request link`, () => { - expect(findMRLinkPath()).toBe(mockPipelineDetached.merge_request.path); - }); + describe('mousetrap', () => { + it.each([ + ['copy-source-ref-link', mockPipelineWithoutMR], + ['copy-source-branch-link', mockPipelineWithAttachedMR], + ])( + 'calls clickCopyToClipboardButton with `%s` button when `b` is pressed', + (button, pipeline) => { + const copyToClipboardMock = jest.spyOn(copyToClipboard, 'clickCopyToClipboardButton'); + createComponent({ pipeline }); - it(`renders the correct source branch link`, () => { - expect(findSourceBranchLinkPath()).toBe( - mockPipelineDetached.merge_request.source_branch_path, - ); - }); + Mousetrap.trigger('b'); - it(`renders the source ref copy button`, () => { - expect(findCopySourceBranchBtn().exists()).toBe(true); - }); + expect(copyToClipboardMock).toHaveBeenCalledWith(wrapper.findByTestId(button).element); + }, + ); }); }); diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js new file mode 100644 index 00000000000..ac79186cb46 --- /dev/null +++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js @@ -0,0 +1,67 @@ +import cacheConfig from '~/jobs/components/table/graphql/cache_config'; +import { + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + CIJobConnectionIncomingCacheRunningStatus, +} from '../../../mock_data'; + +const firstLoadArgs = { first: 3, statuses: 'PENDING' }; +const runningArgs = { first: 3, statuses: 'RUNNING' }; + +describe('jobs/components/table/graphql/cache_config', () => { + describe('when fetching data with the same statuses', () => { + it('should contain cache nodes and a status when merging caches on first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, { + args: firstLoadArgs, + }); + + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length); + expect(res.statuses).toBe('PENDING'); + }); + + it('should add to existing caches when merging caches after first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + { + args: firstLoadArgs, + }, + ); + + expect(res.nodes).toHaveLength( + CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length, + ); + }); + + it('should contain the pageInfo key as part of the result', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, { + args: firstLoadArgs, + }); + + expect(res.pageInfo).toEqual( + expect.objectContaining({ + __typename: 'PageInfo', + endCursor: 'eyJpZCI6IjIwNTEifQ', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'eyJpZCI6IjIxNzMifQ', + }), + ); + }); + }); + + describe('when fetching data with different statuses', () => { + it('should reset cache when a cache already exists', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCacheRunningStatus, + { + args: runningArgs, + }, + ); + + expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes); + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length); + }); + }); +}); diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js index 5ccd38af735..4d51624dfff 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -1,4 +1,4 @@ -import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui'; +import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; @@ -8,12 +8,7 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; -import { - mockJobsQueryResponse, - mockJobsQueryEmptyResponse, - mockJobsQueryResponseLastPage, - mockJobsQueryResponseFirstPage, -} from '../../mock_data'; +import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data'; const projectPath = 'gitlab-org/gitlab'; Vue.use(VueApollo); @@ -30,10 +25,9 @@ describe('Job table app', () => { const findTabs = () => wrapper.findComponent(JobsTableTabs); const findAlert = () => wrapper.findComponent(GlAlert); const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const findPagination = () => wrapper.findComponent(GlPagination); - const findPrevious = () => findPagination().findAll('.page-item').at(0); - const findNext = () => findPagination().findAll('.page-item').at(1); + const triggerInfiniteScroll = () => + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); const createMockApolloProvider = (handler) => { const requestHandlers = [[getJobsQuery, handler]]; @@ -53,7 +47,7 @@ describe('Job table app', () => { }; }, provide: { - projectPath, + fullPath: projectPath, }, apolloProvider: createMockApolloProvider(handler), }); @@ -69,7 +63,6 @@ describe('Job table app', () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findTable().exists()).toBe(false); - expect(findPagination().exists()).toBe(false); }); }); @@ -83,7 +76,6 @@ describe('Job table app', () => { it('should display the jobs table with data', () => { expect(findTable().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(false); - expect(findPagination().exists()).toBe(true); }); it('should refetch jobs query on fetchJobsByStatus event', async () => { @@ -95,41 +87,24 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); }); - }); - describe('pagination', () => { - it('should disable the next page button on the last page', async () => { - createComponent({ - handler: jest.fn().mockResolvedValue(mockJobsQueryResponseLastPage), - mountFn: mount, - data: { - pagination: { currentPage: 3 }, - }, + describe('when infinite scrolling is triggered', () => { + beforeEach(() => { + triggerInfiniteScroll(); }); - await waitForPromises(); - - expect(findPrevious().exists()).toBe(true); - expect(findNext().exists()).toBe(true); - expect(findNext().classes('disabled')).toBe(true); - }); - - it('should disable the previous page button on the first page', async () => { - createComponent({ - handler: jest.fn().mockResolvedValue(mockJobsQueryResponseFirstPage), - mountFn: mount, - data: { - pagination: { - currentPage: 1, - }, - }, + it('does not display a skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); }); - await waitForPromises(); + it('handles infinite scrolling by calling fetch more', async () => { + await waitForPromises(); - expect(findPrevious().exists()).toBe(true); - expect(findPrevious().classes('disabled')).toBe(true); - expect(findNext().exists()).toBe(true); + expect(successHandler).toHaveBeenCalledWith({ + after: 'eyJpZCI6IjIzMTcifQ', + fullPath: 'gitlab-org/gitlab', + }); + }); }); }); |