diff options
Diffstat (limited to 'spec/frontend/jobs/components')
10 files changed, 339 insertions, 579 deletions
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js index 98bdfc3fcbc..14613775791 100644 --- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js +++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js @@ -1,6 +1,10 @@ import { GlFilteredSearch } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + OPERATORS_IS, + TOKEN_TITLE_STATUS, + TOKEN_TYPE_STATUS, +} from '~/vue_shared/components/filtered_search_bar/constants'; import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue'; import { mockFailedSearchToken } from '../../mock_data'; @@ -37,11 +41,11 @@ describe('Jobs filtered search', () => { createComponent(); expect(findStatusToken()).toMatchObject({ - type: 'status', + type: TOKEN_TYPE_STATUS, icon: 'status', - title: 'Status', + title: TOKEN_TITLE_STATUS, unique: true, - operators: OPERATOR_IS_ONLY, + operators: OPERATORS_IS, }); }); @@ -65,7 +69,7 @@ describe('Jobs filtered search', () => { createComponent({ queryString: { statuses: value } }); expect(findFilteredSearch().props('value')).toEqual([ - { type: 'status', value: { data: value, operator: '=' } }, + { type: TOKEN_TYPE_STATUS, value: { data: value, operator: '=' } }, ]); }); }); diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js index 92ce3925a90..fbe5f6a2e11 100644 --- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js +++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js @@ -2,6 +2,10 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitl import { shallowMount } from '@vue/test-utils'; import { stubComponent } from 'helpers/stub_component'; import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue'; +import { + TOKEN_TITLE_STATUS, + TOKEN_TYPE_STATUS, +} from '~/vue_shared/components/filtered_search_bar/constants'; describe('Job Status Token', () => { let wrapper; @@ -13,9 +17,9 @@ describe('Job Status Token', () => { const defaultProps = { config: { - type: 'status', + type: TOKEN_TYPE_STATUS, icon: 'status', - title: 'Status', + title: TOKEN_TITLE_STATUS, unique: true, }, value: { diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js index 299b607ad78..c6ab259bf46 100644 --- a/spec/frontend/jobs/components/job/empty_state_spec.js +++ b/spec/frontend/jobs/components/job/empty_state_spec.js @@ -1,5 +1,7 @@ -import { mount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EmptyState from '~/jobs/components/job/empty_state.vue'; +import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; +import { mockFullPath, mockId } from './mock_data'; describe('Empty State', () => { let wrapper; @@ -7,26 +9,31 @@ describe('Empty State', () => { const defaultProps = { illustrationPath: 'illustrations/pending_job_empty.svg', illustrationSizeClass: 'svg-430', + jobId: mockId, title: 'This job has not started yet', playable: false, + isRetryable: true, }; const createWrapper = (props) => { - wrapper = mount(EmptyState, { + wrapper = shallowMountExtended(EmptyState, { propsData: { ...defaultProps, ...props, }, + provide: { + projectPath: mockFullPath, + }, }); }; const content = 'This job is in pending state and is waiting to be picked by a runner'; const findEmptyStateImage = () => wrapper.find('img'); - const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); - const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]'); - const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]'); - const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]'); + const findTitle = () => wrapper.findByTestId('job-empty-state-title'); + const findContent = () => wrapper.findByTestId('job-empty-state-content'); + const findAction = () => wrapper.findByTestId('job-empty-state-action'); + const findManualVarsForm = () => wrapper.findComponent(ManualVariablesForm); afterEach(() => { if (wrapper?.destroy) { diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js index 822528403cf..98f1979db1b 100644 --- a/spec/frontend/jobs/components/job/job_app_spec.js +++ b/spec/frontend/jobs/components/job/job_app_spec.js @@ -1,14 +1,15 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; -import MockAdapter from 'axios-mock-adapter'; import Vuex from 'vuex'; -import delayedJobFixture from 'test_fixtures/jobs/delayed.json'; +import { GlLoadingIcon } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { TEST_HOST } from 'helpers/test_constants'; import EmptyState from '~/jobs/components/job/empty_state.vue'; import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue'; import ErasedBlock from '~/jobs/components/job/erased_block.vue'; import JobApp from '~/jobs/components/job/job_app.vue'; +import JobLog from '~/jobs/components/log/log.vue'; +import JobLogTopBar from '~/jobs/components/job/job_log_controllers.vue'; import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue'; import StuckBlock from '~/jobs/components/job/stuck_block.vue'; import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue'; @@ -40,7 +41,10 @@ describe('Job App', () => { }; const createComponent = () => { - wrapper = mount(JobApp, { propsData: { ...props }, store }); + wrapper = shallowMountExtended(JobApp, { + propsData: { ...props }, + store, + }); }; const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => { @@ -59,22 +63,16 @@ describe('Job App', () => { const findLoadingComponent = () => wrapper.findComponent(GlLoadingIcon); const findSidebar = () => wrapper.findComponent(Sidebar); - const findJobContent = () => wrapper.find('[data-testid="job-content"'); const findStuckBlockComponent = () => wrapper.findComponent(StuckBlock); - const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"'); - const findStuckBlockNoActiveRunners = () => - wrapper.find('[data-testid="job-stuck-no-active-runners"'); const findFailedJobComponent = () => wrapper.findComponent(UnmetPrerequisitesBlock); const findEnvironmentsBlockComponent = () => wrapper.findComponent(EnvironmentsBlock); const findErasedBlock = () => wrapper.findComponent(ErasedBlock); - const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]'); const findEmptyState = () => wrapper.findComponent(EmptyState); - const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]'); - const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); - const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); - const findJobLogScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); - const findJobLogController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); - const findJobLogEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]'); + const findJobLog = () => wrapper.findComponent(JobLog); + const findJobLogTopBar = () => wrapper.findComponent(JobLogTopBar); + + const findJobContent = () => wrapper.findByTestId('job-content'); + const findArchivedJob = () => wrapper.findByTestId('archived-job'); beforeEach(() => { mock = new MockAdapter(axios); @@ -116,36 +114,6 @@ describe('Job App', () => { expect(wrapper.vm.shouldRenderCalloutMessage).toBe(true); })); }); - - describe('triggered job', () => { - beforeEach(() => { - const aYearAgo = new Date(); - aYearAgo.setFullYear(aYearAgo.getFullYear() - 1); - - return setupAndMount({ - jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() }, - }); - }); - - it('should render provided job information', () => { - expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain( - 'passed Job test triggered 1 year ago by Root', - ); - }); - - it('should render new issue link', () => { - expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path); - }); - }); - - describe('created job', () => { - it('should render created key', () => - setupAndMount().then(() => { - expect( - wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(), - ).toContain('passed Job test created 3 weeks ago by Root'); - })); - }); }); describe('stuck block', () => { @@ -169,57 +137,10 @@ describe('Job App', () => { }, }).then(() => { expect(findStuckBlockComponent().exists()).toBe(true); - expect(findStuckBlockNoActiveRunners().exists()).toBe(true); - })); - }); - - describe('when available runners can not run specified tag', () => { - it('renders tags in stuck block when there are no runners', () => - setupAndMount({ - jobData: { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - stuck: true, - runners: { - available: false, - online: false, - }, - }, - }).then(() => { - expect(findStuckBlockComponent().text()).toContain(job.tags[0]); - expect(findStuckBlockWithTags().exists()).toBe(true); - })); - }); - - describe('when runners are offline and build has tags', () => { - it('renders message about job being stuck because of no runners with the specified tags', () => - setupAndMount({ - jobData: { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - stuck: true, - runners: { - available: true, - online: true, - }, - }, - }).then(() => { - expect(findStuckBlockComponent().text()).toContain(job.tags[0]); - expect(findStuckBlockWithTags().exists()).toBe(true); })); }); - it('does not renders stuck block when there are no runners', () => + it('does not render stuck block when there are runners', () => setupAndMount({ jobData: { runners: { available: true }, @@ -351,45 +272,13 @@ describe('Job App', () => { setupAndMount({ jobData: { has_trace: true } }).then(() => { expect(findEmptyState().exists()).toBe(false); })); - - it('displays remaining time for a delayed job', () => { - const oneHourInMilliseconds = 3600000; - jest - .spyOn(Date, 'now') - .mockImplementation( - () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds, - ); - return setupAndMount({ jobData: delayedJobFixture }).then(() => { - expect(findEmptyState().exists()).toBe(true); - - const title = findJobEmptyStateTitle().text(); - - expect(title).toEqual('This is a delayed job to run in 01:00:00'); - }); - }); }); describe('sidebar', () => { - it('has no blank blocks', async () => { - await setupAndMount({ - jobData: { - duration: null, - finished_at: null, - erased_at: null, - queued: null, - runner: null, - coverage: null, - tags: [], - cancel_path: null, - }, - }); + it('renders sidebar', async () => { + await setupAndMount(); - const blocks = wrapper.findAll('.blocks-container > *').wrappers; - expect(blocks.length).toBeGreaterThan(0); - - blocks.forEach((block) => { - expect(block.text().trim()).not.toBe(''); - }); + expect(findSidebar().exists()).toBe(true); }); }); }); @@ -410,31 +299,15 @@ describe('Job App', () => { }); }); - describe('job log controls', () => { - beforeEach(() => - setupAndMount({ - jobLogData: { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 50, - total: 100, - complete: true, - }, - }), - ); - - it('should render scroll buttons', () => { - expect(findJobLogScrollTop().exists()).toBe(true); - expect(findJobLogScrollBottom().exists()).toBe(true); - }); + describe('job log', () => { + beforeEach(() => setupAndMount()); - it('should render link to raw ouput', () => { - expect(findJobLogController().exists()).toBe(true); + it('should render job log header', () => { + expect(findJobLogTopBar().exists()).toBe(true); }); - it('should render link to erase job', () => { - expect(findJobLogEraseLink().exists()).toBe(true); + it('should render job log', () => { + expect(findJobLog().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js index 18d5f35bde4..91821a38a78 100644 --- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js +++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js @@ -16,6 +16,7 @@ describe('Job Sidebar Retry Button', () => { wrapper = shallowMountExtended(JobsSidebarRetryButton, { propsData: { href: job.retry_path, + isManualJob: false, modalId: 'modal-id', ...props, }, diff --git a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js deleted file mode 100644 index 184562b2968..00000000000 --- a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import { GlSprintf, GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue'; - -Vue.use(Vuex); - -describe('Manual Variables Form', () => { - let wrapper; - let store; - - const requiredProps = { - action: { - path: '/play', - method: 'post', - button_title: 'Trigger this manual action', - }, - }; - - const createComponent = (props = {}) => { - store = new Vuex.Store({ - actions: { - triggerManualJob: jest.fn(), - }, - }); - - wrapper = extendedWrapper( - mount(LegacyManualVariablesForm, { - propsData: { ...requiredProps, ...props }, - store, - stubs: { - GlSprintf, - }, - }), - ); - }; - - const findHelpText = () => wrapper.findComponent(GlSprintf); - const findHelpLink = () => wrapper.findComponent(GlLink); - - const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn'); - const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn'); - const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn'); - const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder'); - const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key'); - const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key'); - const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value'); - const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row'); - - const setCiVariableKey = () => { - findCiVariableKey().setValue('new key'); - findCiVariableKey().vm.$emit('change'); - nextTick(); - }; - - const setCiVariableKeyByPosition = (position, value) => { - findAllCiVariableKeys().at(position).setValue(value); - findAllCiVariableKeys().at(position).vm.$emit('change'); - nextTick(); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('creates a new variable when user enters a new key value', async () => { - expect(findAllVariables()).toHaveLength(1); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - }); - - it('does not create extra empty variables', async () => { - expect(findAllVariables()).toHaveLength(1); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - }); - - it('removes the correct variable row', async () => { - const variableKeyNameOne = 'key-one'; - const variableKeyNameThree = 'key-three'; - - await setCiVariableKeyByPosition(0, variableKeyNameOne); - - await setCiVariableKeyByPosition(1, 'key-two'); - - await setCiVariableKeyByPosition(2, variableKeyNameThree); - - expect(findAllVariables()).toHaveLength(4); - - await findAllDeleteVarBtns().at(1).trigger('click'); - - expect(findAllVariables()).toHaveLength(3); - - expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); - expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); - expect(findAllCiVariableKeys().at(2).element.value).toBe(''); - }); - - it('trigger button is disabled after trigger action', async () => { - expect(findTriggerBtn().props('disabled')).toBe(false); - - await findTriggerBtn().trigger('click'); - - expect(findTriggerBtn().props('disabled')).toBe(true); - }); - - it('delete variable button should only show when there is more than one variable', async () => { - expect(findDeleteVarBtn().exists()).toBe(false); - - await setCiVariableKey(); - - expect(findDeleteVarBtn().exists()).toBe(true); - }); - - it('delete variable button placeholder should only exist when a user cannot remove', async () => { - expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); - }); - - it('renders help text with provided link', () => { - expect(findHelpText().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe( - '/help/ci/variables/index#add-a-cicd-variable-to-a-project', - ); - }); - - it('passes variables in correct format', async () => { - jest.spyOn(store, 'dispatch'); - - await setCiVariableKey(); - - await findCiVariableValue().setValue('new value'); - - await findTriggerBtn().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ - { - key: 'new key', - secret_value: 'new value', - }, - ]); - }); -}); diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js deleted file mode 100644 index 95eb10118ee..00000000000 --- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; -import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue'; -import createStore from '~/jobs/store'; -import job, { failedJobStatus } from '../../mock_data'; - -describe('Legacy Sidebar Header', () => { - let store; - let wrapper; - - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findRetryButton = () => wrapper.findComponent(JobRetryButton); - const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - - const createWrapper = (props) => { - store = createStore(); - - wrapper = extendedWrapper( - shallowMount(LegacySidebarHeader, { - propsData: { - job, - ...props, - }, - store, - }), - ); - }; - - afterEach(() => { - 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 the job is retryable', () => { - beforeEach(() => { - createWrapper(); - }); - - it('should render the retry button', () => { - expect(findRetryButton().props('href')).toBe(job.retry_path); - }); - - it('should have a different label when the job status is passed', () => { - expect(findRetryButton().attributes('title')).toBe( - LegacySidebarHeader.i18n.runAgainJobButtonLabel, - ); - }); - }); - - describe('when there is no retry path', () => { - it('should not render a retry button', async () => { - const copy = { ...job, retry_path: null }; - createWrapper({ job: copy }); - - expect(findRetryButton().exists()).toBe(false); - }); - }); - - describe('when the job is cancelable', () => { - beforeEach(() => { - createWrapper(); - }); - - it('should render link to cancel job', () => { - expect(findCancelButton().props('icon')).toBe('cancel'); - expect(findCancelButton().attributes('href')).toBe(job.cancel_path); - }); - }); - - describe('when the job is failed', () => { - describe('retry button', () => { - it('should have a different label when the job status is failed', () => { - createWrapper({ job: { ...job, status: failedJobStatus } }); - - expect(findRetryButton().attributes('title')).toBe( - LegacySidebarHeader.i18n.retryJobButtonLabel, - ); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js index 5806f9f75f9..45a1e9dca76 100644 --- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js +++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js @@ -1,46 +1,71 @@ import { GlSprintf, GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { GRAPHQL_ID_TYPES } from '~/jobs/constants'; +import waitForPromises from 'helpers/wait_for_promises'; import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; - -Vue.use(Vuex); +import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql'; +import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql'; +import { + mockFullPath, + mockId, + mockJobResponse, + mockJobWithVariablesResponse, + mockJobMutationData, +} from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const defaultProvide = { + projectPath: mockFullPath, +}; describe('Manual Variables Form', () => { let wrapper; - let store; - - const requiredProps = { - action: { - path: '/play', - method: 'post', - button_title: 'Trigger this manual action', - }, + let mockApollo; + let getJobQueryResponse; + + const createComponent = ({ options = {}, props = {} } = {}) => { + wrapper = mountExtended(ManualVariablesForm, { + propsData: { + ...props, + jobId: mockId, + isRetryable: true, + }, + provide: { + ...defaultProvide, + }, + ...options, + }); }; - const createComponent = (props = {}) => { - store = new Vuex.Store({ - actions: { - triggerManualJob: jest.fn(), - }, + const createComponentWithApollo = async ({ props = {} } = {}) => { + const requestHandlers = [[getJobQuery, getJobQueryResponse]]; + + mockApollo = createMockApollo(requestHandlers); + + const options = { + localVue, + apolloProvider: mockApollo, + }; + + createComponent({ + props, + options, }); - wrapper = extendedWrapper( - mount(ManualVariablesForm, { - propsData: { ...requiredProps, ...props }, - store, - stubs: { - GlSprintf, - }, - }), - ); + return waitForPromises(); }; const findHelpText = () => wrapper.findComponent(GlSprintf); const findHelpLink = () => wrapper.findComponent(GlLink); - - const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn'); + const findCancelBtn = () => wrapper.findByTestId('cancel-btn'); + const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn'); const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn'); const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn'); const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder'); @@ -62,95 +87,134 @@ describe('Manual Variables Form', () => { }; beforeEach(() => { - createComponent(); + getJobQueryResponse = jest.fn(); }); afterEach(() => { wrapper.destroy(); }); - it('creates a new variable when user enters a new key value', async () => { - expect(findAllVariables()).toHaveLength(1); + describe('when page renders', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobResponse); + await createComponentWithApollo(); + }); + + it('renders help text with provided link', () => { + expect(findHelpText().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe( + '/help/ci/variables/index#add-a-cicd-variable-to-a-project', + ); + }); + + it('renders buttons', () => { + expect(findCancelBtn().exists()).toBe(true); + expect(findRerunBtn().exists()).toBe(true); + }); + }); + + describe('when job has variables', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse); + await createComponentWithApollo(); + }); - await setCiVariableKey(); + it('sets manual job variables', () => { + const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key; + const queryValue = + mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value; - expect(findAllVariables()).toHaveLength(2); + expect(findCiVariableKey().element.value).toBe(queryKey); + expect(findCiVariableValue().element.value).toBe(queryValue); + }); }); - it('does not create extra empty variables', async () => { - expect(findAllVariables()).toHaveLength(1); + describe('when mutation fires', () => { + beforeEach(async () => { + await createComponentWithApollo(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData); + }); - await setCiVariableKey(); + it('passes variables in correct format', async () => { + await setCiVariableKey(); - expect(findAllVariables()).toHaveLength(2); + await findCiVariableValue().setValue('new value'); - await setCiVariableKey(); + await findRerunBtn().vm.$emit('click'); - expect(findAllVariables()).toHaveLength(2); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: retryJobMutation, + variables: { + id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId), + variables: [ + { + key: 'new key', + value: 'new value', + }, + ], + }, + }); + }); }); - it('removes the correct variable row', async () => { - const variableKeyNameOne = 'key-one'; - const variableKeyNameThree = 'key-three'; + describe('updating variables in UI', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobResponse); + await createComponentWithApollo(); + }); - await setCiVariableKeyByPosition(0, variableKeyNameOne); + it('creates a new variable when user enters a new key value', async () => { + expect(findAllVariables()).toHaveLength(1); - await setCiVariableKeyByPosition(1, 'key-two'); + await setCiVariableKey(); - await setCiVariableKeyByPosition(2, variableKeyNameThree); + expect(findAllVariables()).toHaveLength(2); + }); - expect(findAllVariables()).toHaveLength(4); + it('does not create extra empty variables', async () => { + expect(findAllVariables()).toHaveLength(1); - await findAllDeleteVarBtns().at(1).trigger('click'); + await setCiVariableKey(); - expect(findAllVariables()).toHaveLength(3); + expect(findAllVariables()).toHaveLength(2); - expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); - expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); - expect(findAllCiVariableKeys().at(2).element.value).toBe(''); - }); + await setCiVariableKey(); - it('trigger button is disabled after trigger action', async () => { - expect(findTriggerBtn().props('disabled')).toBe(false); + expect(findAllVariables()).toHaveLength(2); + }); - await findTriggerBtn().trigger('click'); + it('removes the correct variable row', async () => { + const variableKeyNameOne = 'key-one'; + const variableKeyNameThree = 'key-three'; - expect(findTriggerBtn().props('disabled')).toBe(true); - }); + await setCiVariableKeyByPosition(0, variableKeyNameOne); - it('delete variable button should only show when there is more than one variable', async () => { - expect(findDeleteVarBtn().exists()).toBe(false); + await setCiVariableKeyByPosition(1, 'key-two'); - await setCiVariableKey(); + await setCiVariableKeyByPosition(2, variableKeyNameThree); - expect(findDeleteVarBtn().exists()).toBe(true); - }); + expect(findAllVariables()).toHaveLength(4); - it('delete variable button placeholder should only exist when a user cannot remove', async () => { - expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); - }); + await findAllDeleteVarBtns().at(1).trigger('click'); - it('renders help text with provided link', () => { - expect(findHelpText().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe( - '/help/ci/variables/index#add-a-cicd-variable-to-a-project', - ); - }); + expect(findAllVariables()).toHaveLength(3); - it('passes variables in correct format', async () => { - jest.spyOn(store, 'dispatch'); + expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); + expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); + expect(findAllCiVariableKeys().at(2).element.value).toBe(''); + }); - await setCiVariableKey(); + it('delete variable button should only show when there is more than one variable', async () => { + expect(findDeleteVarBtn().exists()).toBe(false); - await findCiVariableValue().setValue('new value'); + await setCiVariableKey(); - await findTriggerBtn().trigger('click'); + expect(findDeleteVarBtn().exists()).toBe(true); + }); - expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ - { - key: 'new key', - secret_value: 'new value', - }, - ]); + it('delete variable button placeholder should only exist when a user cannot remove', async () => { + expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); + }); }); }); diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js new file mode 100644 index 00000000000..9596e859475 --- /dev/null +++ b/spec/frontend/jobs/components/job/mock_data.js @@ -0,0 +1,76 @@ +export const mockFullPath = 'Commit451/lab-coat'; +export const mockId = 401; + +export const mockJobResponse = { + data: { + project: { + id: 'gid://gitlab/Project/4', + job: { + id: 'gid://gitlab/Ci::Build/401', + manualJob: true, + manualVariables: { + nodes: [], + __typename: 'CiManualVariableConnection', + }, + name: 'manual_job', + retryable: true, + status: 'SUCCESS', + __typename: 'CiJob', + }, + __typename: 'Project', + }, + }, +}; + +export const mockJobWithVariablesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/4', + job: { + id: 'gid://gitlab/Ci::Build/401', + manualJob: true, + manualVariables: { + nodes: [ + { + id: 'gid://gitlab/Ci::JobVariable/150', + key: 'new key', + value: 'new value', + __typename: 'CiManualVariable', + }, + ], + __typename: 'CiManualVariableConnection', + }, + name: 'manual_job', + retryable: true, + status: 'SUCCESS', + __typename: 'CiJob', + }, + __typename: 'Project', + }, + }, +}; + +export const mockJobMutationData = { + data: { + jobRetry: { + job: { + id: 'gid://gitlab/Ci::Build/401', + manualVariables: { + nodes: [ + { + id: 'gid://gitlab/Ci::JobVariable/151', + key: 'new key', + value: 'new value', + __typename: 'CiManualVariable', + }, + ], + __typename: 'CiManualVariableConnection', + }, + webPath: '/Commit451/lab-coat/-/jobs/401', + __typename: 'CiJob', + }, + errors: [], + __typename: 'JobRetryPayload', + }, + }, +}; diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js index cb32ca9d3dc..da97945f9bf 100644 --- a/spec/frontend/jobs/components/job/sidebar_header_spec.js +++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js @@ -1,91 +1,87 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue'; import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; -import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue'; -import createStore from '~/jobs/store'; -import job from '../../mock_data'; +import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql'; +import { mockFullPath, mockId, mockJobResponse } from './mock_data'; -describe('Legacy Sidebar Header', () => { - let store; +Vue.use(VueApollo); + +const defaultProvide = { + projectPath: mockFullPath, +}; + +describe('Sidebar Header', () => { let wrapper; - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findRetryButton = () => wrapper.findComponent(JobRetryButton); - const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - - const createWrapper = (props) => { - store = createStore(); - - wrapper = extendedWrapper( - shallowMount(LegacySidebarHeader, { - propsData: { - job, - ...props, - }, - store, - }), - ); + const createComponent = ({ options = {}, props = {}, restJob = {} } = {}) => { + wrapper = shallowMountExtended(SidebarHeader, { + propsData: { + ...props, + jobId: mockId, + restJob, + }, + provide: { + ...defaultProvide, + }, + ...options, + }); }; - afterEach(() => { - wrapper.destroy(); - }); + const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => { + const getJobQueryResponse = jest.fn().mockResolvedValue(mockJobResponse); - describe('when job log is erasable', () => { - const path = '/root/ci-project/-/jobs/1447/erase'; + const requestHandlers = [[getJobQuery, getJobQueryResponse]]; - beforeEach(() => { - createWrapper({ - erasePath: path, - }); - }); + const apolloProvider = createMockApollo(requestHandlers); - it('renders erase job link', () => { - expect(findEraseLink().exists()).toBe(true); - }); + const options = { + apolloProvider, + }; - it('erase job link has correct path', () => { - expect(findEraseLink().attributes('href')).toBe(path); + createComponent({ + props, + restJob, + options, }); - }); - describe('when job log is not erasable', () => { - beforeEach(() => { - createWrapper(); - }); + return waitForPromises(); + }; - it('does not render erase button', () => { - expect(findEraseLink().exists()).toBe(false); - }); - }); + const findCancelButton = () => wrapper.findByTestId('cancel-button'); + const findEraseButton = () => wrapper.findByTestId('job-log-erase-link'); + const findJobName = () => wrapper.findByTestId('job-name'); + const findRetryButton = () => wrapper.findComponent(JobRetryButton); - describe('when the job is retryable', () => { - beforeEach(() => { - createWrapper(); + describe('when rendering contents', () => { + it('renders the correct job name', async () => { + await createComponentWithApollo(); + expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name); }); - it('should render the retry button', () => { - expect(findRetryButton().props('href')).toBe(job.retry_path); + it('does not render buttons with no paths', async () => { + await createComponentWithApollo(); + expect(findCancelButton().exists()).toBe(false); + expect(findEraseButton().exists()).toBe(false); + expect(findRetryButton().exists()).toBe(false); }); - }); - - describe('when there is no retry path', () => { - it('should not render a retry button', async () => { - const copy = { ...job, retry_path: null }; - createWrapper({ job: copy }); - expect(findRetryButton().exists()).toBe(false); + it('renders a retry button with a path', async () => { + await createComponentWithApollo({ restJob: { retry_path: 'retry/path' } }); + expect(findRetryButton().exists()).toBe(true); }); - }); - describe('when the job is cancelable', () => { - beforeEach(() => { - createWrapper(); + it('renders a cancel button with a path', async () => { + await createComponentWithApollo({ restJob: { cancel_path: 'cancel/path' } }); + expect(findCancelButton().exists()).toBe(true); }); - it('should render link to cancel job', () => { - expect(findCancelButton().props('icon')).toBe('cancel'); - expect(findCancelButton().attributes('href')).toBe(job.cancel_path); + it('renders an erase button with a path', async () => { + await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } }); + expect(findEraseButton().exists()).toBe(true); }); }); }); |