diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/frontend/jobs | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) | |
download | gitlab-ce-b595cb0c1dec83de5bdee18284abe86614bed33b.tar.gz |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/frontend/jobs')
19 files changed, 357 insertions, 1441 deletions
diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js deleted file mode 100644 index 210dcfa364b..00000000000 --- a/spec/frontend/jobs/bridge/app_spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import { shallowMount } from '@vue/test-utils'; - -import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; -import { GlLoadingIcon } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import getPipelineQuery from '~/jobs/bridge/graphql/queries/pipeline.query.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import BridgeApp from '~/jobs/bridge/app.vue'; -import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue'; -import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue'; -import CiHeader from '~/vue_shared/components/header_ci_component.vue'; -import { - MOCK_BUILD_ID, - MOCK_PIPELINE_IID, - MOCK_PROJECT_FULL_PATH, - mockPipelineQueryResponse, -} from './mock_data'; - -describe('Bridge Show Page', () => { - let wrapper; - let mockApollo; - let mockPipelineQuery; - - const createComponent = (options) => { - wrapper = shallowMount(BridgeApp, { - provide: { - buildId: MOCK_BUILD_ID, - projectFullPath: MOCK_PROJECT_FULL_PATH, - pipelineIid: MOCK_PIPELINE_IID, - }, - mocks: { - $apollo: { - queries: { - pipeline: { - loading: true, - }, - }, - }, - }, - ...options, - }); - }; - - const createComponentWithApollo = () => { - const handlers = [[getPipelineQuery, mockPipelineQuery]]; - Vue.use(VueApollo); - mockApollo = createMockApollo(handlers); - - createComponent({ - apolloProvider: mockApollo, - mocks: {}, - }); - }; - - const findCiHeader = () => wrapper.findComponent(CiHeader); - const findEmptyState = () => wrapper.findComponent(BridgeEmptyState); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findSidebar = () => wrapper.findComponent(BridgeSidebar); - - beforeEach(() => { - mockPipelineQuery = jest.fn(); - }); - - afterEach(() => { - mockPipelineQuery.mockReset(); - wrapper.destroy(); - }); - - describe('while pipeline query is loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('after pipeline query is loaded', () => { - beforeEach(async () => { - mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse); - createComponentWithApollo(); - await waitForPromises(); - }); - - it('query is called with correct variables', async () => { - expect(mockPipelineQuery).toHaveBeenCalledTimes(1); - expect(mockPipelineQuery).toHaveBeenCalledWith({ - fullPath: MOCK_PROJECT_FULL_PATH, - iid: MOCK_PIPELINE_IID, - }); - }); - - it('renders CI header state', () => { - expect(findCiHeader().exists()).toBe(true); - }); - - it('renders empty state', () => { - expect(findEmptyState().exists()).toBe(true); - }); - - it('renders sidebar', () => { - expect(findSidebar().exists()).toBe(true); - }); - }); - - describe('sidebar expansion', () => { - beforeEach(async () => { - mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse); - createComponentWithApollo(); - await waitForPromises(); - }); - - describe('on resize', () => { - it.each` - breakpoint | isSidebarExpanded - ${'xs'} | ${false} - ${'sm'} | ${false} - ${'md'} | ${true} - ${'lg'} | ${true} - ${'xl'} | ${true} - `( - 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"', - async ({ breakpoint, isSidebarExpanded }) => { - jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint); - - window.dispatchEvent(new Event('resize')); - await nextTick(); - - expect(findSidebar().exists()).toBe(isSidebarExpanded); - }, - ); - }); - - it('toggles expansion on button click', async () => { - expect(findSidebar().exists()).toBe(true); - - wrapper.vm.toggleSidebar(); - await nextTick(); - - expect(findSidebar().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/components/empty_state_spec.js b/spec/frontend/jobs/bridge/components/empty_state_spec.js deleted file mode 100644 index 38c55b296f0..00000000000 --- a/spec/frontend/jobs/bridge/components/empty_state_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue'; -import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_data'; - -describe('Bridge Empty State', () => { - let wrapper; - - const createComponent = ({ downstreamPipelinePath }) => { - wrapper = shallowMount(BridgeEmptyState, { - provide: { - emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH, - }, - propsData: { - downstreamPipelinePath, - }, - }); - }; - - const findSvg = () => wrapper.find('img'); - const findTitle = () => wrapper.find('h1'); - const findLinkBtn = () => wrapper.findComponent(GlButton); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('template', () => { - beforeEach(() => { - createComponent({ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM }); - }); - - it('renders illustration', () => { - expect(findSvg().exists()).toBe(true); - }); - - it('renders title', () => { - expect(findTitle().exists()).toBe(true); - expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title); - }); - - it('renders CTA button', () => { - expect(findLinkBtn().exists()).toBe(true); - expect(findLinkBtn().text()).toBe(wrapper.vm.$options.i18n.linkBtnText); - expect(findLinkBtn().attributes('href')).toBe(MOCK_PATH_TO_DOWNSTREAM); - }); - }); - - describe('without downstream pipeline', () => { - beforeEach(() => { - createComponent({ downstreamPipelinePath: undefined }); - }); - - it('does not render CTA button', () => { - expect(findLinkBtn().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/components/sidebar_spec.js b/spec/frontend/jobs/bridge/components/sidebar_spec.js deleted file mode 100644 index 5006d4f08a6..00000000000 --- a/spec/frontend/jobs/bridge/components/sidebar_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { GlButton, GlDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue'; -import CommitBlock from '~/jobs/components/commit_block.vue'; -import { mockCommit, mockJob } from '../mock_data'; - -describe('Bridge Sidebar', () => { - let wrapper; - - const MockHeaderEl = { - getBoundingClientRect() { - return { - bottom: '40', - }; - }, - }; - - const createComponent = ({ featureFlag } = {}) => { - wrapper = shallowMount(BridgeSidebar, { - provide: { - glFeatures: { - triggerJobRetryAction: featureFlag, - }, - }, - propsData: { - bridgeJob: mockJob, - commit: mockCommit, - }, - }); - }; - - const findJobTitle = () => wrapper.find('h4'); - const findCommitBlock = () => wrapper.findComponent(CommitBlock); - const findRetryDropdown = () => wrapper.find(GlDropdown); - const findToggleBtn = () => wrapper.findComponent(GlButton); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('template', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders job name', () => { - expect(findJobTitle().text()).toBe(mockJob.name); - }); - - it('renders commit information', () => { - expect(findCommitBlock().exists()).toBe(true); - }); - }); - - describe('styles', () => { - beforeEach(async () => { - jest.spyOn(document, 'querySelector').mockReturnValue(MockHeaderEl); - createComponent(); - }); - - it('calculates root styles correctly', () => { - expect(wrapper.attributes('style')).toBe('width: 290px; top: 40px;'); - }); - }); - - describe('sidebar expansion', () => { - beforeEach(() => { - createComponent(); - }); - - it('emits toggle sidebar event on button click', async () => { - expect(wrapper.emitted('toggleSidebar')).toBe(undefined); - - findToggleBtn().vm.$emit('click'); - - expect(wrapper.emitted('toggleSidebar')).toHaveLength(1); - }); - }); - - describe('retry action', () => { - describe('when feature flag is ON', () => { - beforeEach(() => { - createComponent({ featureFlag: true }); - }); - - it('renders retry dropdown', () => { - expect(findRetryDropdown().exists()).toBe(true); - }); - }); - - describe('when feature flag is OFF', () => { - it('does not render retry dropdown', () => { - createComponent({ featureFlag: false }); - - expect(findRetryDropdown().exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/mock_data.js b/spec/frontend/jobs/bridge/mock_data.js deleted file mode 100644 index 4084bb54163..00000000000 --- a/spec/frontend/jobs/bridge/mock_data.js +++ /dev/null @@ -1,102 +0,0 @@ -export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg'; -export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline'; -export const MOCK_BUILD_ID = '1331'; -export const MOCK_PIPELINE_IID = '174'; -export const MOCK_PROJECT_FULL_PATH = '/root/project/'; -export const MOCK_SHA = '38f3d89147765427a7ce58be28cd76d14efa682a'; - -export const mockCommit = { - id: `gid://gitlab/CommitPresenter/${MOCK_SHA}`, - shortId: '38f3d891', - title: 'Update .gitlab-ci.yml file', - webPath: `/root/project/-/commit/${MOCK_SHA}`, - __typename: 'Commit', -}; - -export const mockJob = { - createdAt: '2021-12-10T09:05:45Z', - id: 'gid://gitlab/Ci::Build/1331', - name: 'triggerJobName', - scheduledAt: null, - startedAt: '2021-12-10T09:13:43Z', - status: 'SUCCESS', - triggered: null, - detailedStatus: { - id: '1', - detailsPath: '/root/project/-/jobs/1331', - icon: 'status_success', - group: 'success', - text: 'passed', - tooltip: 'passed', - __typename: 'DetailedStatus', - }, - downstreamPipeline: { - id: '1', - path: '/root/project/-/pipelines/175', - }, - stage: { - id: '1', - name: 'build', - __typename: 'CiStage', - }, - __typename: 'CiJob', -}; - -export const mockUser = { - id: 'gid://gitlab/User/1', - avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - name: 'Administrator', - username: 'root', - webPath: '/root', - webUrl: 'http://gdk.test:3000/root', - status: { - message: 'making great things', - __typename: 'UserStatus', - }, - __typename: 'UserCore', -}; - -export const mockStage = { - id: '1', - name: 'build', - jobs: { - nodes: [mockJob], - __typename: 'CiJobConnection', - }, - __typename: 'CiStage', -}; - -export const mockPipelineQueryResponse = { - data: { - project: { - id: '1', - pipeline: { - commit: mockCommit, - id: 'gid://gitlab/Ci::Pipeline/174', - iid: '88', - path: '/root/project/-/pipelines/174', - sha: MOCK_SHA, - ref: 'main', - refPath: 'path/to/ref', - user: mockUser, - detailedStatus: { - id: '1', - icon: 'status_failed', - group: 'failed', - __typename: 'DetailedStatus', - }, - stages: { - edges: [ - { - node: mockStage, - __typename: 'CiStageEdge', - }, - ], - __typename: 'CiStageConnection', - }, - __typename: 'Pipeline', - }, - __typename: 'Project', - }, - }, -}; diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js index fc308766ab9..b4b5bc4669d 100644 --- a/spec/frontend/jobs/components/job_app_spec.js +++ b/spec/frontend/jobs/components/job_app_spec.js @@ -22,7 +22,6 @@ describe('Job App', () => { let store; let wrapper; let mock; - let origGon; const initSettings = { endpoint: `${TEST_HOST}jobs/123.json`, @@ -80,17 +79,11 @@ describe('Job App', () => { beforeEach(() => { mock = new MockAdapter(axios); store = createStore(); - - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: All of this passes with the feature flag }); afterEach(() => { wrapper.destroy(); mock.restore(); - - window.gon = origGon; }); describe('while loading', () => { diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js index cd3ee734466..cc97d111c06 100644 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -1,6 +1,11 @@ +import { GlSearchBoxByClick } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import JobLogControllers from '~/jobs/components/job_log_controllers.vue'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import { mockJobLog } from '../mock_data'; + +const mockToastShow = jest.fn(); describe('Job log controllers', () => { let wrapper; @@ -19,14 +24,30 @@ describe('Job log controllers', () => { isScrollBottomDisabled: false, isScrollingDown: true, isJobLogSizeVisible: true, + jobLog: mockJobLog, }; - const createWrapper = (props) => { + const createWrapper = (props, jobLogSearch = false) => { wrapper = mount(JobLogControllers, { propsData: { ...defaultProps, ...props, }, + provide: { + glFeatures: { + jobLogSearch, + }, + }, + data() { + return { + searchTerm: '82', + }; + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, }); }; @@ -35,6 +56,8 @@ describe('Job log controllers', () => { const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); + const findJobLogSearch = () => wrapper.findComponent(GlSearchBoxByClick); + const findSearchHelp = () => wrapper.findComponent(HelpPopover); describe('Truncate information', () => { describe('with isJobLogSizeVisible', () => { @@ -179,4 +202,40 @@ describe('Job log controllers', () => { }); }); }); + + describe('Job log search', () => { + describe('with feature flag off', () => { + it('does not display job log search', () => { + createWrapper(); + + expect(findJobLogSearch().exists()).toBe(false); + expect(findSearchHelp().exists()).toBe(false); + }); + }); + + describe('with feature flag on', () => { + beforeEach(() => { + createWrapper({}, { jobLogSearch: true }); + }); + + it('displays job log search', () => { + expect(findJobLogSearch().exists()).toBe(true); + expect(findSearchHelp().exists()).toBe(true); + }); + + it('emits search results', () => { + const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; + + findJobLogSearch().vm.$emit('submit'); + + expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); + }); + + it('clears search results', () => { + findJobLogSearch().vm.$emit('clear'); + + expect(wrapper.emitted('searchResults')).toEqual([[[]]]); + }); + }); + }); }); diff --git a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js index cc9a5e4ee25..4046f0269dd 100644 --- a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js +++ b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js @@ -42,7 +42,7 @@ describe('Job Sidebar Details Container', () => { expect(wrapper.html()).toBe(''); }); - it.each(['duration', 'erased_at', 'finished_at', 'queued', 'runner', 'coverage'])( + it.each(['duration', 'erased_at', 'finished_at', 'queued_at', 'runner', 'coverage'])( 'should not render %s details when missing', async (detail) => { await store.dispatch('receiveJobSuccess', { [detail]: undefined }); @@ -59,7 +59,7 @@ describe('Job Sidebar Details Container', () => { ['duration', 'Elapsed time: 6 seconds'], ['erased_at', 'Erased: 3 weeks ago'], ['finished_at', 'Finished: 3 weeks ago'], - ['queued', 'Queued: 9 seconds'], + ['queued_duration', 'Queued: 9 seconds'], ['runner', 'Runner: #1 (ABCDEFGH) local ci runner'], ['coverage', 'Coverage: 20%'], ])('uses %s to render job-%s', async (detail, value) => { diff --git a/spec/frontend/jobs/components/jobs_container_spec.js b/spec/frontend/jobs/components/jobs_container_spec.js index 1cde72682a2..127570b8184 100644 --- a/spec/frontend/jobs/components/jobs_container_spec.js +++ b/spec/frontend/jobs/components/jobs_container_spec.js @@ -106,7 +106,7 @@ describe('Jobs List block', () => { }); expect(findJob().text()).toBe(job.name); - expect(findJob().text()).not.toContain(job.id); + expect(findJob().text()).not.toContain(job.id.toString()); }); it('renders job id when job name is not available', () => { diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js index 2ab7f5fe22d..646935568b1 100644 --- a/spec/frontend/jobs/components/log/collapsible_section_spec.js +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -5,7 +5,6 @@ import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data' describe('Job Log Collapsible Section', () => { let wrapper; - let origGon; const jobLogEndpoint = 'jobs/335'; @@ -20,16 +19,8 @@ describe('Job Log Collapsible Section', () => { }); }; - beforeEach(() => { - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: This also works with true - }); - afterEach(() => { wrapper.destroy(); - - window.gon = origGon; }); describe('with closed section', () => { diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js index d184696cd1f..bf80d90e299 100644 --- a/spec/frontend/jobs/components/log/line_spec.js +++ b/spec/frontend/jobs/components/log/line_spec.js @@ -179,4 +179,46 @@ describe('Job Log Line', () => { expect(findLink().exists()).toBe(false); }); }); + + describe('job log search', () => { + const mockSearchResults = [ + { + offset: 1533, + content: [{ text: '$ echo "82.71"', style: 'term-fg-l-green term-bold' }], + section: 'step-script', + lineNumber: 20, + }, + { offset: 1560, content: [{ text: '82.71' }], section: 'step-script', lineNumber: 21 }, + ]; + + it('applies highlight class to search result elements', () => { + createComponent({ + line: { + offset: 1560, + content: [{ text: '82.71' }], + section: 'step-script', + lineNumber: 21, + }, + path: '/root/ci-project/-/jobs/1089', + searchResults: mockSearchResults, + }); + + expect(wrapper.classes()).toContain('gl-bg-gray-500'); + }); + + it('does not apply highlight class to search result elements', () => { + createComponent({ + line: { + offset: 1560, + content: [{ text: 'docker' }], + section: 'step-script', + lineNumber: 29, + }, + path: '/root/ci-project/-/jobs/1089', + searchResults: mockSearchResults, + }); + + expect(wrapper.classes()).not.toContain('gl-bg-gray-500'); + }); + }); }); diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js index 9cc56cce9b3..c933ed5c3e1 100644 --- a/spec/frontend/jobs/components/log/log_spec.js +++ b/spec/frontend/jobs/components/log/log_spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import Log from '~/jobs/components/log/log.vue'; -import { logLinesParserLegacy, logLinesParser } from '~/jobs/store/utils'; +import { logLinesParser } from '~/jobs/store/utils'; import { jobLog } from './mock_data'; describe('Job Log', () => { @@ -10,7 +10,6 @@ describe('Job Log', () => { let actions; let state; let store; - let origGon; Vue.use(Vuex); @@ -25,12 +24,8 @@ describe('Job Log', () => { toggleCollapsibleLine: () => {}, }; - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: false } }; - state = { - jobLog: logLinesParserLegacy(jobLog), + jobLog: logLinesParser(jobLog), jobLogEndpoint: 'jobs/id', }; @@ -44,88 +39,6 @@ describe('Job Log', () => { afterEach(() => { wrapper.destroy(); - - window.gon = origGon; - }); - - const findCollapsibleLine = () => wrapper.find('.collapsible-line'); - - describe('line numbers', () => { - it('renders a line number for each open line', () => { - expect(wrapper.find('#L1').text()).toBe('1'); - expect(wrapper.find('#L2').text()).toBe('2'); - expect(wrapper.find('#L3').text()).toBe('3'); - }); - - it('links to the provided path and correct line number', () => { - expect(wrapper.find('#L1').attributes('href')).toBe(`${state.jobLogEndpoint}#L1`); - }); - }); - - describe('collapsible sections', () => { - it('renders a clickable header section', () => { - expect(findCollapsibleLine().attributes('role')).toBe('button'); - }); - - it('renders an icon with the open state', () => { - expect(findCollapsibleLine().find('[data-testid="chevron-lg-down-icon"]').exists()).toBe( - true, - ); - }); - - describe('on click header section', () => { - it('calls toggleCollapsibleLine', () => { - jest.spyOn(wrapper.vm, 'toggleCollapsibleLine'); - - findCollapsibleLine().trigger('click'); - - expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled(); - }); - }); - }); -}); - -describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => { - let wrapper; - let actions; - let state; - let store; - let origGon; - - Vue.use(Vuex); - - const createComponent = () => { - wrapper = mount(Log, { - store, - }); - }; - - beforeEach(() => { - actions = { - toggleCollapsibleLine: () => {}, - }; - - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: true } }; - - state = { - jobLog: logLinesParser(jobLog).parsedLines, - jobLogEndpoint: 'jobs/id', - }; - - store = new Vuex.Store({ - actions, - state, - }); - - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - - window.gon = origGon; }); const findCollapsibleLine = () => wrapper.find('.collapsible-line'); diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js index 3ff0bd73581..eb8c4fe8bc9 100644 --- a/spec/frontend/jobs/components/log/mock_data.js +++ b/spec/frontend/jobs/components/log/mock_data.js @@ -58,80 +58,6 @@ export const utilsMockData = [ }, ]; -export const multipleCollapsibleSectionsMockData = [ - { - offset: 1001, - content: [{ text: ' on docker-auto-scale-com 8a6210b8' }], - }, - { - offset: 1002, - content: [ - { - text: 'Executing "step_script" stage of the job script', - }, - ], - section: 'step-script', - section_header: true, - }, - { - offset: 1003, - content: [{ text: 'sleep 60' }], - section: 'step-script', - }, - { - offset: 1004, - content: [ - { - text: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', - }, - ], - section: 'step-script', - }, - { - offset: 1005, - content: [{ text: 'executing...' }], - section: 'step-script', - }, - { - offset: 1006, - content: [{ text: '1st collapsible section' }], - section: 'collapsible-1', - section_header: true, - }, - { - offset: 1007, - content: [ - { - text: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', - }, - ], - section: 'collapsible-1', - }, - { - offset: 1008, - content: [], - section: 'collapsible-1', - section_duration: '01:00', - }, - { - offset: 1009, - content: [], - section: 'step-script', - section_duration: '10:00', - }, -]; - -export const backwardsCompatibilityTrace = [ - { - offset: 2365, - content: [], - section: 'download-artifacts', - section_duration: '00:01', - }, -]; - export const originalTrace = [ { offset: 1, diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js index 976b128532d..7cc008f332d 100644 --- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js @@ -12,17 +12,12 @@ import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retr import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql'; import JobCancelMutation from '~/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql'; import { - playableJob, - retryableJob, - cancelableJob, - scheduledJob, - cannotRetryJob, - cannotPlayJob, - cannotPlayScheduledJob, - retryMutationResponse, + mockJobsNodes, + mockJobsNodesAsGuest, playMutationResponse, - cancelMutationResponse, + retryMutationResponse, unscheduleMutationResponse, + cancelMutationResponse, } from '../../../mock_data'; jest.mock('~/lib/utils/url_utility'); @@ -32,6 +27,22 @@ Vue.use(VueApollo); describe('Job actions cell', () => { let wrapper; + const findMockJob = (jobName, nodes = mockJobsNodes) => { + const job = nodes.find(({ name }) => name === jobName); + expect(job).toBeDefined(); // ensure job is present + return job; + }; + + const mockJob = findMockJob('build'); + const cancelableJob = findMockJob('cancelable'); + const playableJob = findMockJob('playable'); + const retryableJob = findMockJob('retryable'); + const scheduledJob = findMockJob('scheduled'); + const jobWithArtifact = findMockJob('with_artifact'); + const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest); + const cannotRetryJob = findMockJob('retryable', mockJobsNodesAsGuest); + const cannotPlayScheduledJob = findMockJob('scheduled', mockJobsNodesAsGuest); + const findRetryButton = () => wrapper.findByTestId('retry'); const findPlayButton = () => wrapper.findByTestId('play'); const findCancelButton = () => wrapper.findByTestId('cancel-button'); @@ -55,10 +66,10 @@ describe('Job actions cell', () => { return createMockApollo(requestHandlers); }; - const createComponent = (jobType, requestHandlers, props = {}) => { + const createComponent = (job, requestHandlers, props = {}) => { wrapper = shallowMountExtended(ActionsCell, { propsData: { - job: jobType, + job, ...props, }, apolloProvider: createMockApolloProvider(requestHandlers), @@ -73,15 +84,15 @@ describe('Job actions cell', () => { }); it('displays the artifacts download button with correct link', () => { - createComponent(playableJob); + createComponent(jobWithArtifact); expect(findDownloadArtifactsButton().attributes('href')).toBe( - playableJob.artifacts.nodes[0].downloadPath, + jobWithArtifact.artifacts.nodes[0].downloadPath, ); }); it('does not display an artifacts download button', () => { - createComponent(retryableJob); + createComponent(mockJob); expect(findDownloadArtifactsButton().exists()).toBe(false); }); @@ -101,7 +112,7 @@ describe('Job actions cell', () => { button | action | jobType ${findPlayButton} | ${'play'} | ${playableJob} ${findRetryButton} | ${'retry'} | ${retryableJob} - ${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob} + ${findDownloadArtifactsButton} | ${'download artifacts'} | ${jobWithArtifact} ${findCancelButton} | ${'cancel'} | ${cancelableJob} `('displays the $action button', ({ button, jobType }) => { createComponent(jobType); diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js index fc4e5586349..ddc196129a7 100644 --- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js @@ -2,16 +2,22 @@ import { shallowMount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import JobCell from '~/jobs/components/table/cells/job_cell.vue'; -import { mockJobsInTable } from '../../../mock_data'; - -const mockJob = mockJobsInTable[0]; -const mockJobCreatedByTag = mockJobsInTable[1]; -const mockJobLimitedAccess = mockJobsInTable[2]; -const mockStuckJob = mockJobsInTable[3]; +import { mockJobsNodes, mockJobsNodesAsGuest } from '../../../mock_data'; describe('Job Cell', () => { let wrapper; + const findMockJob = (jobName, nodes = mockJobsNodes) => { + const job = nodes.find(({ name }) => name === jobName); + expect(job).toBeDefined(); // ensure job is present + return job; + }; + + const mockJob = findMockJob('build'); + const jobCreatedByTag = findMockJob('created_by_tag'); + const pendingJob = findMockJob('pending'); + const jobAsGuest = findMockJob('build', mockJobsNodesAsGuest); + const findJobIdLink = () => wrapper.findByTestId('job-id-link'); const findJobIdNoLink = () => wrapper.findByTestId('job-id-limited-access'); const findJobRef = () => wrapper.findByTestId('job-ref'); @@ -23,11 +29,11 @@ describe('Job Cell', () => { const findBadgeById = (id) => wrapper.findByTestId(id); - const createComponent = (jobData = mockJob) => { + const createComponent = (job = mockJob) => { wrapper = extendedWrapper( shallowMount(JobCell, { propsData: { - job: jobData, + job, }, }), ); @@ -49,9 +55,9 @@ describe('Job Cell', () => { }); it('display the job id with no link', () => { - createComponent(mockJobLimitedAccess); + createComponent(jobAsGuest); - const expectedJobId = `#${getIdFromGraphQLId(mockJobLimitedAccess.id)}`; + const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}`; expect(findJobIdNoLink().text()).toBe(expectedJobId); expect(findJobIdNoLink().exists()).toBe(true); @@ -75,7 +81,7 @@ describe('Job Cell', () => { }); it('displays label icon when job is created by a tag', () => { - createComponent(mockJobCreatedByTag); + createComponent(jobCreatedByTag); expect(findLabelIcon().exists()).toBe(true); expect(findForkIcon().exists()).toBe(false); @@ -130,8 +136,8 @@ describe('Job Cell', () => { expect(findStuckIcon().exists()).toBe(false); }); - it('stuck icon is shown if job is stuck', () => { - createComponent(mockStuckJob); + it('stuck icon is shown if job is pending', () => { + createComponent(pendingJob); expect(findStuckIcon().exists()).toBe(true); expect(findStuckIcon().attributes('name')).toBe('warning'); 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 986fba21fb9..374768c3ee4 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -6,7 +6,7 @@ import { GlLoadingIcon, } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { s__ } from '~/locale'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -18,8 +18,8 @@ import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue'; import { - mockJobsQueryResponse, - mockJobsQueryEmptyResponse, + mockJobsResponsePaginated, + mockJobsResponseEmpty, mockFailedSearchToken, } from '../../mock_data'; @@ -30,11 +30,10 @@ jest.mock('~/flash'); describe('Job table app', () => { let wrapper; - let jobsTableVueSearch = true; - const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse); + const successHandler = jest.fn().mockResolvedValue(mockJobsResponsePaginated); const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); - const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse); + const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); @@ -66,7 +65,6 @@ describe('Job table app', () => { }, provide: { fullPath: projectPath, - glFeatures: { jobsTableVueSearch }, }, apolloProvider: createMockApolloProvider(handler), }); @@ -77,17 +75,17 @@ describe('Job table app', () => { }); describe('loading state', () => { - beforeEach(() => { + it('should display skeleton loader when loading', () => { createComponent(); - }); - it('should display skeleton loader when loading', () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findTable().exists()).toBe(false); expect(findLoadingSpinner().exists()).toBe(false); }); it('when switching tabs only the skeleton loader should show', () => { + createComponent(); + findTabs().vm.$emit('fetchJobsByStatus', null); expect(findSkeletonLoader().exists()).toBe(true); @@ -119,24 +117,29 @@ describe('Job table app', () => { }); describe('when infinite scrolling is triggered', () => { - beforeEach(() => { + it('does not display a skeleton loader', () => { triggerInfiniteScroll(); - }); - it('does not display a skeleton loader', () => { expect(findSkeletonLoader().exists()).toBe(false); }); it('handles infinite scrolling by calling fetch more', async () => { + triggerInfiniteScroll(); + + await nextTick(); + + const pageSize = 30; + expect(findLoadingSpinner().exists()).toBe(true); await waitForPromises(); expect(findLoadingSpinner().exists()).toBe(false); - expect(successHandler).toHaveBeenCalledWith({ - after: 'eyJpZCI6IjIzMTcifQ', - fullPath: 'gitlab-org/gitlab', + expect(successHandler).toHaveBeenLastCalledWith({ + first: pageSize, + fullPath: projectPath, + after: mockJobsResponsePaginated.data.project.jobs.pageInfo.endCursor, }); }); }); @@ -227,13 +230,5 @@ describe('Job table app', () => { expect(createFlash).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); }); - - it('should not display filtered search', () => { - jobsTableVueSearch = false; - - createComponent(); - - expect(findFilteredSearch().exists()).toBe(false); - }); }); }); diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js index ac8bef675f8..803df3df37f 100644 --- a/spec/frontend/jobs/components/table/jobs_table_spec.js +++ b/spec/frontend/jobs/components/table/jobs_table_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import JobsTable from '~/jobs/components/table/jobs_table.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; -import { mockJobsInTable } from '../../mock_data'; +import { mockJobsNodes } from '../../mock_data'; describe('Jobs Table', () => { let wrapper; @@ -19,7 +19,7 @@ describe('Jobs Table', () => { wrapper = extendedWrapper( mount(JobsTable, { propsData: { - jobs: mockJobsInTable, + jobs: mockJobsNodes, ...props, }, }), @@ -39,7 +39,7 @@ describe('Jobs Table', () => { }); it('displays correct number of job rows', () => { - expect(findTableRows()).toHaveLength(mockJobsInTable.length); + expect(findTableRows()).toHaveLength(mockJobsNodes.length); }); it('displays job status', () => { @@ -47,14 +47,14 @@ describe('Jobs Table', () => { }); it('displays the job stage and name', () => { - const firstJob = mockJobsInTable[0]; + const firstJob = mockJobsNodes[0]; expect(findJobStage().text()).toBe(firstJob.stage.name); expect(findJobName().text()).toBe(firstJob.name); }); it('displays the coverage for only jobs that have coverage', () => { - const jobsThatHaveCoverage = mockJobsInTable.filter((job) => job.coverage !== null); + const jobsThatHaveCoverage = mockJobsNodes.filter((job) => job.coverage !== null); jobsThatHaveCoverage.forEach((job, index) => { expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`); diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 4676635cce0..bf238b2e39a 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1,8 +1,18 @@ +import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json'; +import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json'; +import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json'; +import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json'; import { TEST_HOST } from 'spec/test_constants'; const threeWeeksAgo = new Date(); threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); +// Fixtures generated at spec/frontend/fixtures/jobs.rb +export const mockJobsResponsePaginated = mockJobsPaginated; +export const mockJobsResponseEmpty = mockJobsEmpty; +export const mockJobsNodes = mockJobs.data.project.jobs.nodes; +export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes; + export const stages = [ { name: 'build', @@ -924,7 +934,7 @@ export default { created_at: threeWeeksAgo.toISOString(), updated_at: threeWeeksAgo.toISOString(), finished_at: threeWeeksAgo.toISOString(), - queued: 9.54, + queued_duration: 9.54, status: { icon: 'status_success', text: 'passed', @@ -1283,602 +1293,6 @@ export const mockPipelineDetached = { }, }; -export const mockJobsInTable = [ - { - detailedStatus: { - icon: 'status_manual', - label: 'manual play action', - text: 'manual', - tooltip: 'manual action', - action: { - buttonTitle: 'Trigger this manual action', - icon: 'play', - method: 'post', - path: '/root/ci-project/-/jobs/2004/play', - title: 'Play', - __typename: 'StatusAction', - }, - detailsPath: '/root/ci-project/-/jobs/2004', - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/2004', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '2d5d8323', - commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/423', - path: '/root/ci-project/-/pipelines/423', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'User', - }, - __typename: 'Pipeline', - }, - stage: { name: 'test', __typename: 'CiStage' }, - name: 'test_manual_job', - duration: null, - finishedAt: null, - coverage: null, - createdByTag: false, - retryable: false, - playable: true, - cancelable: false, - active: false, - stuck: false, - userPermissions: { readBuild: true, __typename: 'JobPermissions' }, - __typename: 'CiJob', - }, - { - detailedStatus: { - icon: 'status_skipped', - label: 'skipped', - text: 'skipped', - tooltip: 'skipped', - action: null, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/2021', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '2d5d8323', - commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/425', - path: '/root/ci-project/-/pipelines/425', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'User', - }, - __typename: 'Pipeline', - }, - stage: { name: 'test', __typename: 'CiStage' }, - name: 'coverage_job', - duration: null, - finishedAt: null, - coverage: null, - createdByTag: true, - retryable: false, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { readBuild: true, __typename: 'JobPermissions' }, - __typename: 'CiJob', - }, - { - detailedStatus: { - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - action: { - buttonTitle: 'Retry this job', - icon: 'retry', - method: 'post', - path: '/root/ci-project/-/jobs/2015/retry', - title: 'Retry', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/2015', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '2d5d8323', - commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/424', - path: '/root/ci-project/-/pipelines/424', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'User', - }, - __typename: 'Pipeline', - }, - stage: { name: 'deploy', __typename: 'CiStage' }, - name: 'artifact_job', - duration: 2, - finishedAt: '2021-04-01T17:36:18Z', - coverage: 82.71, - createdByTag: false, - retryable: true, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { readBuild: false, __typename: 'JobPermissions' }, - __typename: 'CiJob', - }, - { - artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' }, - allowFailure: false, - status: 'PENDING', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - detailsPath: '/root/ci-project/-/jobs/2391', - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - tooltip: 'pending', - action: { - buttonTitle: 'Cancel this job', - icon: 'cancel', - method: 'post', - path: '/root/ci-project/-/jobs/2391/cancel', - title: 'Cancel', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/2391', - refName: 'master', - refPath: '/root/ci-project/-/commits/master', - tags: [], - shortSha: '916330b4', - commitPath: '/root/ci-project/-/commit/916330b4fda5dae226524ceb51c756c0ed26679d', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/482', - path: '/root/ci-project/-/pipelines/482', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - stage: { name: 'build', __typename: 'CiStage' }, - name: 'build_job', - duration: null, - finishedAt: null, - coverage: null, - retryable: false, - playable: false, - cancelable: true, - active: true, - stuck: true, - userPermissions: { readBuild: true, __typename: 'JobPermissions' }, - __typename: 'CiJob', - }, -]; - -export const mockJobsQueryResponse = { - data: { - project: { - id: '1', - jobs: { - count: 1, - pageInfo: { - endCursor: 'eyJpZCI6IjIzMTcifQ', - hasNextPage: true, - hasPreviousPage: false, - startCursor: 'eyJpZCI6IjIzMzYifQ', - __typename: 'PageInfo', - }, - nodes: [ - { - artifacts: { - nodes: [ - { - downloadPath: '/root/ci-project/-/jobs/2336/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/root/ci-project/-/jobs/2336/artifacts/download?file_type=metadata', - fileType: 'METADATA', - __typename: 'CiJobArtifact', - }, - { - downloadPath: '/root/ci-project/-/jobs/2336/artifacts/download?file_type=archive', - fileType: 'ARCHIVE', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'SUCCESS', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - id: 'status-1', - detailsPath: '/root/ci-project/-/jobs/2336', - group: 'success', - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - action: { - id: 'action-1', - buttonTitle: 'Retry this job', - icon: 'retry', - method: 'post', - path: '/root/ci-project/-/jobs/2336/retry', - title: 'Retry', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/2336', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '4408fa2a', - commitPath: '/root/ci-project/-/commit/4408fa2a27aaadfdf42d8dda3d6a9c01ce6cad78', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/473', - path: '/root/ci-project/-/pipelines/473', - user: { - id: 'user-1', - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - stage: { - id: 'stage-1', - name: 'deploy', - __typename: 'CiStage', - }, - name: 'artifact_job', - duration: 3, - finishedAt: '2021-04-29T14:19:50Z', - coverage: null, - retryable: true, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: true, - __typename: 'JobPermissions', - }, - __typename: 'CiJob', - }, - ], - __typename: 'CiJobConnection', - }, - __typename: 'Project', - }, - }, -}; - -export const mockJobsQueryEmptyResponse = { - data: { - project: { - id: '1', - jobs: [], - }, - }, -}; - -export const retryableJob = { - artifacts: { - nodes: [ - { - downloadPath: '/root/ci-project/-/jobs/847/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'SUCCESS', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - detailsPath: '/root/test-job-artifacts/-/jobs/1981', - group: 'success', - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - action: { - buttonTitle: 'Retry this job', - icon: 'retry', - method: 'post', - path: '/root/test-job-artifacts/-/jobs/1981/retry', - title: 'Retry', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/1981', - refName: 'main', - refPath: '/root/test-job-artifacts/-/commits/main', - tags: [], - shortSha: '75daf01b', - commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/288', - path: '/root/test-job-artifacts/-/pipelines/288', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - stage: { name: 'test', __typename: 'CiStage' }, - name: 'hello_world', - duration: 7, - finishedAt: '2021-08-30T20:33:56Z', - coverage: null, - retryable: true, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' }, - __typename: 'CiJob', -}; - -export const cancelableJob = { - artifacts: { - nodes: [], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'PENDING', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - id: 'pending-1305-1305', - detailsPath: '/root/lots-of-jobs-project/-/jobs/1305', - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - tooltip: 'pending', - action: { - id: 'Ci::Build-pending-1305', - buttonTitle: 'Cancel this job', - icon: 'cancel', - method: 'post', - path: '/root/lots-of-jobs-project/-/jobs/1305/cancel', - title: 'Cancel', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/1305', - refName: 'main', - refPath: '/root/lots-of-jobs-project/-/commits/main', - tags: [], - shortSha: '750605f2', - commitPath: '/root/lots-of-jobs-project/-/commit/750605f29530778cf0912779eba6d073128962a5', - stage: { - id: 'gid://gitlab/Ci::Stage/181', - name: 'deploy', - __typename: 'CiStage', - }, - name: 'job_212', - duration: null, - finishedAt: null, - coverage: null, - retryable: false, - playable: false, - cancelable: true, - active: true, - stuck: false, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: true, - __typename: 'JobPermissions', - }, - __typename: 'CiJob', -}; - -export const cannotRetryJob = { - ...retryableJob, - userPermissions: { readBuild: true, updateBuild: false, __typename: 'JobPermissions' }, -}; - -export const playableJob = { - artifacts: { - nodes: [ - { - downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=archive', - fileType: 'ARCHIVE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=metadata', - fileType: 'METADATA', - __typename: 'CiJobArtifact', - }, - { - downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'SUCCESS', - scheduledAt: null, - manualJob: true, - triggered: null, - createdByTag: false, - detailedStatus: { - detailsPath: '/root/test-job-artifacts/-/jobs/1982', - group: 'success', - icon: 'status_success', - label: 'manual play action', - text: 'passed', - tooltip: 'passed', - action: { - buttonTitle: 'Trigger this manual action', - icon: 'play', - method: 'post', - path: '/root/test-job-artifacts/-/jobs/1982/play', - title: 'Play', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/1982', - refName: 'main', - refPath: '/root/test-job-artifacts/-/commits/main', - tags: [], - shortSha: '75daf01b', - commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/288', - path: '/root/test-job-artifacts/-/pipelines/288', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - stage: { name: 'test', __typename: 'CiStage' }, - name: 'hello_world_delayed', - duration: 6, - finishedAt: '2021-08-30T20:36:12Z', - coverage: null, - retryable: true, - playable: true, - cancelable: false, - active: false, - stuck: false, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: true, - __typename: 'JobPermissions', - }, - __typename: 'CiJob', -}; - -export const cannotPlayJob = { - ...playableJob, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: false, - __typename: 'JobPermissions', - }, -}; - -export const scheduledJob = { - artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' }, - allowFailure: false, - status: 'SCHEDULED', - scheduledAt: '2021-08-31T22:36:05Z', - manualJob: true, - triggered: null, - createdByTag: false, - detailedStatus: { - detailsPath: '/root/test-job-artifacts/-/jobs/1986', - group: 'scheduled', - icon: 'status_scheduled', - label: 'unschedule action', - text: 'delayed', - tooltip: 'delayed manual action (%{remainingTime})', - action: { - buttonTitle: 'Unschedule job', - icon: 'time-out', - method: 'post', - path: '/root/test-job-artifacts/-/jobs/1986/unschedule', - title: 'Unschedule', - __typename: 'StatusAction', - }, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/1986', - refName: 'main', - refPath: '/root/test-job-artifacts/-/commits/main', - tags: [], - shortSha: '75daf01b', - commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/290', - path: '/root/test-job-artifacts/-/pipelines/290', - user: { - webPath: '/root', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - stage: { name: 'test', __typename: 'CiStage' }, - name: 'hello_world_delayed', - duration: null, - finishedAt: null, - coverage: null, - retryable: false, - playable: true, - cancelable: false, - active: false, - stuck: false, - userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' }, - __typename: 'CiJob', -}; - -export const cannotPlayScheduledJob = { - ...scheduledJob, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: false, - __typename: 'JobPermissions', - }, -}; - export const CIJobConnectionIncomingCache = { __typename: 'CiJobConnection', pageInfo: { @@ -2000,3 +1414,167 @@ export const unscheduleMutationResponse = { }, }, }; + +export const mockJobLog = [ + { offset: 0, content: [{ text: 'Running with gitlab-runner 15.0.0 (febb2a09)' }], lineNumber: 0 }, + { offset: 54, content: [{ text: ' on colima-docker EwM9WzgD' }], lineNumber: 1 }, + { + isClosed: false, + isHeader: true, + line: { + offset: 91, + content: [{ text: 'Resolving secrets', style: 'term-fg-l-cyan term-bold' }], + section: 'resolve-secrets', + section_header: true, + lineNumber: 2, + section_duration: '00:00', + }, + lines: [], + }, + { + isClosed: false, + isHeader: true, + line: { + offset: 218, + content: [{ text: 'Preparing the "docker" executor', style: 'term-fg-l-cyan term-bold' }], + section: 'prepare-executor', + section_header: true, + lineNumber: 4, + section_duration: '00:01', + }, + lines: [ + { + offset: 317, + content: [{ text: 'Using Docker executor with image ruby:2.7 ...' }], + section: 'prepare-executor', + lineNumber: 5, + }, + { + offset: 372, + content: [{ text: 'Pulling docker image ruby:2.7 ...' }], + section: 'prepare-executor', + lineNumber: 6, + }, + { + offset: 415, + content: [ + { + text: + 'Using docker image sha256:55106bf6ba7f452c38d01ea760affc6ceb67d4b60068ffadab98d1b7b007668c for ruby:2.7 with digest ruby@sha256:23d08a4bae1a12ee3fce017f83204fcf9a02243443e4a516e65e5ff73810a449 ...', + }, + ], + section: 'prepare-executor', + lineNumber: 7, + }, + ], + }, + { + isClosed: false, + isHeader: true, + line: { + offset: 665, + content: [{ text: 'Preparing environment', style: 'term-fg-l-cyan term-bold' }], + section: 'prepare-script', + section_header: true, + lineNumber: 9, + section_duration: '00:01', + }, + lines: [ + { + offset: 752, + content: [ + { text: 'Running on runner-ewm9wzgd-project-20-concurrent-0 via 8ea689ec6969...' }, + ], + section: 'prepare-script', + lineNumber: 10, + }, + ], + }, + { + isClosed: false, + isHeader: true, + line: { + offset: 865, + content: [{ text: 'Getting source from Git repository', style: 'term-fg-l-cyan term-bold' }], + section: 'get-sources', + section_header: true, + lineNumber: 12, + section_duration: '00:01', + }, + lines: [ + { + offset: 962, + content: [ + { + text: 'Fetching changes with git depth set to 20...', + style: 'term-fg-l-green term-bold', + }, + ], + section: 'get-sources', + lineNumber: 13, + }, + { + offset: 1019, + content: [ + { text: 'Reinitialized existing Git repository in /builds/root/ci-project/.git/' }, + ], + section: 'get-sources', + lineNumber: 14, + }, + { + offset: 1090, + content: [{ text: 'Checking out e0f63d76 as main...', style: 'term-fg-l-green term-bold' }], + section: 'get-sources', + lineNumber: 15, + }, + { + offset: 1136, + content: [{ text: 'Skipping Git submodules setup', style: 'term-fg-l-green term-bold' }], + section: 'get-sources', + lineNumber: 16, + }, + ], + }, + { + isClosed: false, + isHeader: true, + line: { + offset: 1217, + content: [ + { + text: 'Executing "step_script" stage of the job script', + style: 'term-fg-l-cyan term-bold', + }, + ], + section: 'step-script', + section_header: true, + lineNumber: 18, + section_duration: '00:00', + }, + lines: [ + { + offset: 1327, + content: [ + { + text: + 'Using docker image sha256:55106bf6ba7f452c38d01ea760affc6ceb67d4b60068ffadab98d1b7b007668c for ruby:2.7 with digest ruby@sha256:23d08a4bae1a12ee3fce017f83204fcf9a02243443e4a516e65e5ff73810a449 ...', + }, + ], + section: 'step-script', + lineNumber: 19, + }, + { + offset: 1533, + content: [{ text: '$ echo "82.71"', style: 'term-fg-l-green term-bold' }], + section: 'step-script', + lineNumber: 20, + }, + { offset: 1560, content: [{ text: '82.71' }], section: 'step-script', lineNumber: 21 }, + ], + }, + { + offset: 1605, + content: [{ text: 'Job succeeded', style: 'term-fg-l-green term-bold' }], + lineNumber: 23, + }, +]; diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index b73aa8abf4e..ea1ec383d6e 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -4,21 +4,12 @@ import state from '~/jobs/store/state'; describe('Jobs Store Mutations', () => { let stateCopy; - let origGon; const html = 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I'; beforeEach(() => { stateCopy = state(); - - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: false } }; - }); - - afterEach(() => { - window.gon = origGon; }); describe('SET_JOB_ENDPOINT', () => { @@ -276,88 +267,3 @@ describe('Jobs Store Mutations', () => { }); }); }); - -describe('Job Store mutations, feature flag ON', () => { - let stateCopy; - let origGon; - - const html = - 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I'; - - beforeEach(() => { - stateCopy = state(); - - origGon = window.gon; - - window.gon = { features: { infinitelyCollapsibleSections: true } }; - }); - - afterEach(() => { - window.gon = origGon; - }); - - describe('RECEIVE_JOB_LOG_SUCCESS', () => { - describe('with new job log', () => { - describe('log.lines', () => { - describe('when append is true', () => { - it('sets the parsed log ', () => { - mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { - append: true, - size: 511846, - complete: true, - lines: [ - { - offset: 1, - content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], - }, - ], - }); - - expect(stateCopy.jobLog).toEqual([ - { - offset: 1, - content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], - lineNumber: 1, - }, - ]); - }); - }); - - describe('when lines are defined', () => { - it('sets the parsed log ', () => { - mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { - append: false, - size: 511846, - complete: true, - lines: [ - { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] }, - ], - }); - - expect(stateCopy.jobLog).toEqual([ - { - offset: 0, - content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], - lineNumber: 1, - }, - ]); - }); - }); - - describe('when lines are null', () => { - it('sets the default value', () => { - mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { - append: true, - html, - size: 511846, - complete: false, - lines: null, - }); - - expect(stateCopy.jobLog).toEqual([]); - }); - }); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js index 92ac33c8792..9458c2184f5 100644 --- a/spec/frontend/jobs/store/utils_spec.js +++ b/spec/frontend/jobs/store/utils_spec.js @@ -1,6 +1,5 @@ import { logLinesParser, - logLinesParserLegacy, updateIncrementalJobLog, parseHeaderLine, parseLine, @@ -18,8 +17,6 @@ import { headerTraceIncremental, collapsibleTrace, collapsibleTraceIncremental, - multipleCollapsibleSectionsMockData, - backwardsCompatibilityTrace, } from '../components/log/mock_data'; describe('Jobs Store Utils', () => { @@ -178,11 +175,11 @@ describe('Jobs Store Utils', () => { expect(isCollapsibleSection()).toEqual(false); }); }); - describe('logLinesParserLegacy', () => { + describe('logLinesParser', () => { let result; beforeEach(() => { - result = logLinesParserLegacy(utilsMockData); + result = logLinesParser(utilsMockData); }); describe('regular line', () => { @@ -219,102 +216,6 @@ describe('Jobs Store Utils', () => { }); }); - describe('logLinesParser', () => { - let result; - - beforeEach(() => { - result = logLinesParser(utilsMockData); - }); - - describe('regular line', () => { - it('adds a lineNumber property with correct index', () => { - expect(result.parsedLines[0].lineNumber).toEqual(1); - expect(result.parsedLines[1].line.lineNumber).toEqual(2); - }); - }); - - describe('collapsible section', () => { - it('adds a `isClosed` property', () => { - expect(result.parsedLines[1].isClosed).toEqual(false); - }); - - it('adds a `isHeader` property', () => { - expect(result.parsedLines[1].isHeader).toEqual(true); - }); - - it('creates a lines array property with the content of the collapsible section', () => { - expect(result.parsedLines[1].lines.length).toEqual(2); - expect(result.parsedLines[1].lines[0].content).toEqual(utilsMockData[2].content); - expect(result.parsedLines[1].lines[1].content).toEqual(utilsMockData[3].content); - }); - }); - - describe('section duration', () => { - it('adds the section information to the header section', () => { - expect(result.parsedLines[1].line.section_duration).toEqual( - utilsMockData[4].section_duration, - ); - }); - - it('does not add section duration as a line', () => { - expect(result.parsedLines[1].lines.includes(utilsMockData[4])).toEqual(false); - }); - }); - - describe('multiple collapsible sections', () => { - beforeEach(() => { - result = logLinesParser(multipleCollapsibleSectionsMockData); - }); - - it('should contain a section inside another section', () => { - const innerSection = [ - { - isClosed: false, - isHeader: true, - line: { - content: [{ text: '1st collapsible section' }], - lineNumber: 6, - offset: 1006, - section: 'collapsible-1', - section_duration: '01:00', - section_header: true, - }, - lines: [ - { - content: [ - { - text: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', - }, - ], - lineNumber: 7, - offset: 1007, - section: 'collapsible-1', - }, - ], - }, - ]; - - expect(result.parsedLines[1].lines).toEqual(expect.arrayContaining(innerSection)); - }); - }); - - describe('backwards compatibility', () => { - beforeEach(() => { - result = logLinesParser(backwardsCompatibilityTrace); - }); - - it('should return an object with a parsedLines prop', () => { - expect(result).toEqual( - expect.objectContaining({ - parsedLines: expect.any(Array), - }), - ); - expect(result.parsedLines).toHaveLength(1); - }); - }); - }); - describe('findOffsetAndRemove', () => { describe('when last item is header', () => { const existingLog = [ @@ -490,7 +391,7 @@ describe('Jobs Store Utils', () => { describe('updateIncrementalJobLog', () => { describe('without repeated section', () => { it('concats and parses both arrays', () => { - const oldLog = logLinesParserLegacy(originalTrace); + const oldLog = logLinesParser(originalTrace); const result = updateIncrementalJobLog(regularIncremental, oldLog); expect(result).toEqual([ @@ -518,7 +419,7 @@ describe('Jobs Store Utils', () => { describe('with regular line repeated offset', () => { it('updates the last line and formats with the incremental part', () => { - const oldLog = logLinesParserLegacy(originalTrace); + const oldLog = logLinesParser(originalTrace); const result = updateIncrementalJobLog(regularIncrementalRepeated, oldLog); expect(result).toEqual([ @@ -537,7 +438,7 @@ describe('Jobs Store Utils', () => { describe('with header line repeated', () => { it('updates the header line and formats with the incremental part', () => { - const oldLog = logLinesParserLegacy(headerTrace); + const oldLog = logLinesParser(headerTrace); const result = updateIncrementalJobLog(headerTraceIncremental, oldLog); expect(result).toEqual([ @@ -563,7 +464,7 @@ describe('Jobs Store Utils', () => { describe('with collapsible line repeated', () => { it('updates the collapsible line and formats with the incremental part', () => { - const oldLog = logLinesParserLegacy(collapsibleTrace); + const oldLog = logLinesParser(collapsibleTrace); const result = updateIncrementalJobLog(collapsibleTraceIncremental, oldLog); expect(result).toEqual([ |