diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue index c167019861a012e49884b34cf0e5f0da38398497..88ed6afb0ea28c3b6da01bcaf18f17928cbfd78d 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue @@ -85,7 +85,8 @@ export default { }, update({ project: { jobs: { nodes = [], pageInfo = {} } = {} } }) { this.pageInfo = pageInfo; - return nodes + + const jobNodes = nodes .map(mapArchivesToJobNodes) .map(mapBooleansToJobNodes) .map((jobNode) => { @@ -96,6 +97,12 @@ export default { _showDetails: this.expandedJobs.includes(jobNode.id), }; }); + + if (jobNodes.some((jobNode) => !jobNode.hasArtifacts)) { + this.$apollo.queries.jobArtifacts.refetch(); + } + + return jobNodes; }, error() { createAlert({ @@ -367,6 +374,9 @@ export default { createdLabel: I18N_CREATED, artifactsCount: I18N_ARTIFACTS_COUNT, }, + TBODY_TR_ATTR: { + 'data-testid': 'job-artifact-table-row', + }, }; </script> <template> @@ -391,6 +401,7 @@ export default { :busy="$apollo.queries.jobArtifacts.loading" stacked="sm" details-td-class="gl-bg-gray-10! gl-p-0! gl-overflow-auto" + :tbody-tr-attr="$options.TBODY_TR_ATTR" > <template #table-busy> <gl-skeleton-loader v-for="i in 20" :key="i" :width="1000" :height="75"> diff --git a/qa/qa/page/project/artifacts/index.rb b/qa/qa/page/project/artifacts/index.rb index 19285be430ba58de51542d986abe27f5e60696a4..b0e301c6866bb331d28425b21b054a8674d3b1a9 100644 --- a/qa/qa/page/project/artifacts/index.rb +++ b/qa/qa/page/project/artifacts/index.rb @@ -7,6 +7,7 @@ module Artifacts class Index < QA::Page::Base view 'app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue' do element 'select-all-artifacts-checkbox' + element 'job-artifact-table-row' end view 'app/assets/javascripts/ci/artifacts/components/artifacts_bulk_delete.vue' do @@ -17,11 +18,6 @@ class Index < QA::Page::Base element 'artifacts-bulk-delete-modal' end - view 'app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue' do - element 'job-artifacts-count' - element 'job-artifacts-size' - end - def select_all check_element('select-all-artifacts-checkbox', true) end @@ -34,12 +30,8 @@ def delete_selected_artifacts end end - def job_artifacts_count_by_row(row: 1) - all_elements('job-artifacts-count', minimum: row)[row - 1].text.gsub(/[^0-9]/, '').to_i - end - - def job_artifacts_size_by_row(row: 1) - all_elements('job-artifacts-size', minimum: row)[row - 1].text.gsub(/[^0-9]/, '').to_f + def has_no_artifacts? + has_no_element?('job-artifact-table-row') end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb index 1f341fe7751aeeeb5a55a6fdc6a1a43c3389db3c..54677ee2ac1d47a1f8ffa3cf6b079f8cd6a0c36a 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb @@ -31,14 +31,8 @@ module QA testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/425725' do Page::Project::Artifacts::Index.perform do |index| index.delete_selected_artifacts - position = rand(1..20) - artifacts_count = index.job_artifacts_count_by_row(row: position) - artifacts_size = index.job_artifacts_size_by_row(row: position) - aggregate_failures 'job artifacts count and size' do - expect(artifacts_count).to eq(0), 'Failed to delete artifact' - expect(artifacts_size).to eq(0), 'Failed to delete artifact' - end + expect(index).to have_no_artifacts end end end diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js index 8d1f72d2767507e017cf50deb029829f5bc4d197..f8583cd2eabc1e4c406bf7c35b5d225f4a43a88e 100644 --- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js @@ -118,6 +118,24 @@ describe('JobArtifactsTable component', () => { }; const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; + const emptyJob = { + ...job, + artifacts: { nodes: [] }, + }; + + const getJobArtifactsResponseWithEmptyJob = { + data: { + ...getJobArtifactsResponse.data, + project: { + ...getJobArtifactsResponse.data.project, + jobs: { + nodes: [emptyJob], + pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo }, + }, + }, + }, + }; + const archiveArtifact = job.artifacts.nodes.find( (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE, ); @@ -810,6 +828,47 @@ describe('JobArtifactsTable component', () => { }); }); + describe('refetch behavior', () => { + describe('without no empty jobs', () => { + const query = jest.fn().mockResolvedValue(getJobArtifactsResponse); + + beforeEach(async () => { + createComponent({ + handlers: { + getJobArtifactsQuery: query, + }, + }); + + await waitForPromises(); + }); + + it('only fetches artifacts once', () => { + expect(query).toHaveBeenCalledTimes(1); + }); + }); + + describe('with an empty job', () => { + const query = jest + .fn() + .mockResolvedValueOnce(getJobArtifactsResponseWithEmptyJob) + .mockResolvedValue(getJobArtifactsResponse); + + beforeEach(async () => { + createComponent({ + handlers: { + getJobArtifactsQuery: query, + }, + }); + + await waitForPromises(); + }); + + it('refetches to clear empty jobs', () => { + expect(query).toHaveBeenCalledTimes(2); + }); + }); + }); + describe('pagination', () => { const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs; const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates); diff --git a/spec/frontend/ci/artifacts/utils_spec.js b/spec/frontend/ci/artifacts/utils_spec.js index 17b4a9f162b04ebb4c96eacfa16d82712e744dc9..475fe800df5aedd8a73c77b40c886324f79aa67d 100644 --- a/spec/frontend/ci/artifacts/utils_spec.js +++ b/spec/frontend/ci/artifacts/utils_spec.js @@ -1,8 +1,16 @@ import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils'; +import { + totalArtifactsSizeForJob, + mapArchivesToJobNodes, + mapBooleansToJobNodes, +} from '~/ci/artifacts/utils'; const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; +const emptyJob = { + ...job, + artifacts: { nodes: [] }, +}; const artifacts = job.artifacts.nodes; describe('totalArtifactsSizeForJob', () => { @@ -14,3 +22,21 @@ describe('totalArtifactsSizeForJob', () => { ); }); }); + +describe('mapArchivesToJobNodes', () => { + it('sets archive to the archive artifact for each job node', () => { + expect([job, emptyJob].map(mapArchivesToJobNodes)).toMatchObject([ + { archive: { name: 'ci_build_artifacts.zip' } }, + { archive: {} }, + ]); + }); +}); + +describe('mapBooleansToJobNodes', () => { + it('sets hasArtifacts and hasMetadata for each job node', () => { + expect([job, emptyJob].map(mapBooleansToJobNodes)).toMatchObject([ + { hasArtifacts: true, hasMetadata: true }, + { hasArtifacts: false, hasMetadata: false }, + ]); + }); +});