diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 18:10:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 18:10:58 +0300 |
commit | 049d16d168fdee408b78f5f38619c092fd3b2265 (patch) | |
tree | 22d1db5ab4fae0967a4da4b1a6b097ef9e5d7aa2 /spec/frontend/artifacts | |
parent | bf18f3295b550c564086efd0a32d9a25435ce216 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/artifacts')
4 files changed, 463 insertions, 0 deletions
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js new file mode 100644 index 00000000000..ccde3bbbf98 --- /dev/null +++ b/spec/frontend/artifacts/components/artifact_row_spec.js @@ -0,0 +1,67 @@ +import { GlBadge, GlButton } from '@gitlab/ui'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import ArtifactRow from '~/artifacts/components/artifact_row.vue'; + +describe('ArtifactRow component', () => { + let wrapper; + + const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0]; + + const findName = () => wrapper.findByTestId('job-artifact-row-name'); + const findBadge = () => wrapper.findComponent(GlBadge); + const findSize = () => wrapper.findByTestId('job-artifact-row-size'); + const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button'); + const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button'); + + const createComponent = (mountFn = shallowMountExtended) => { + wrapper = mountFn(ArtifactRow, { + propsData: { + artifact, + isLoading: false, + isLastRow: false, + }, + stubs: { GlBadge, GlButton }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('artifact details', () => { + beforeEach(async () => { + createComponent(); + + await waitForPromises(); + }); + + it('displays the artifact name and type', () => { + expect(findName().text()).toContain(artifact.name); + expect(findBadge().text()).toBe(artifact.fileType.toLowerCase()); + }); + + it('displays the artifact size', () => { + expect(findSize().text()).toBe(numberToHumanSize(artifact.size)); + }); + + it('displays the download button as a link to the download path', () => { + expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath); + }); + + it('displays the delete button', () => { + expect(findDeleteButton().exists()).toBe(true); + }); + + it('emits the delete event when the delete button is clicked', async () => { + expect(wrapper.emitted('delete')).toBeUndefined(); + + findDeleteButton().trigger('click'); + await waitForPromises(); + + expect(wrapper.emitted('delete')).toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js new file mode 100644 index 00000000000..4834adeea1e --- /dev/null +++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js @@ -0,0 +1,107 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import waitForPromises from 'helpers/wait_for_promises'; +import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue'; +import ArtifactRow from '~/artifacts/components/artifact_row.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql'; +import { I18N_DESTROY_ERROR } from '~/artifacts/constants'; +import { createAlert } from '~/flash'; + +jest.mock('~/flash'); + +const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0]; +const refetchArtifacts = jest.fn(); + +Vue.use(VueApollo); + +describe('ArtifactsTableRowDetails component', () => { + let wrapper; + let requestHandlers; + + const createComponent = ( + handlers = { + destroyArtifactMutation: jest.fn(), + }, + ) => { + requestHandlers = handlers; + wrapper = mountExtended(ArtifactsTableRowDetails, { + apolloProvider: createMockApollo([ + [destroyArtifactMutation, requestHandlers.destroyArtifactMutation], + ]), + propsData: { + artifacts, + refetchArtifacts, + queryVariables: {}, + }, + data() { + return { deletingArtifactId: null }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('passes correct props', () => { + beforeEach(() => { + createComponent(); + }); + + it('to the artifact rows', () => { + [0, 1, 2].forEach((index) => { + expect(wrapper.findAllComponents(ArtifactRow).at(index).props()).toMatchObject({ + artifact: artifacts.nodes[index], + isLoading: false, + }); + }); + }); + }); + + describe('when an artifact row emits the delete event', () => { + it('sets isLoading to true for that row', async () => { + createComponent(); + await waitForPromises(); + + wrapper.findComponent(ArtifactRow).vm.$emit('delete'); + + await nextTick(); + + [ + { index: 0, expectedLoading: true }, + { index: 1, expectedLoading: false }, + ].forEach(({ index, expectedLoading }) => { + expect(wrapper.findAllComponents(ArtifactRow).at(index).props('isLoading')).toBe( + expectedLoading, + ); + }); + }); + + it('triggers the destroyArtifact GraphQL mutation', async () => { + createComponent(); + await waitForPromises(); + + wrapper.findComponent(ArtifactRow).vm.$emit('delete'); + + expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalled(); + }); + + it('displays a flash message and refetches artifacts when the mutation fails', async () => { + createComponent({ + destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')), + }); + await waitForPromises(); + + expect(wrapper.emitted('refetch')).toBeUndefined(); + + wrapper.findComponent(ArtifactRow).vm.$emit('delete'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message: I18N_DESTROY_ERROR }); + expect(wrapper.emitted('refetch')).toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js new file mode 100644 index 00000000000..6c3a56e5d5c --- /dev/null +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -0,0 +1,222 @@ +import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; +import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants'; +import { totalArtifactsSizeForJob } from '~/artifacts/utils'; +import { createAlert } from '~/flash'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +describe('JobArtifactsTable component', () => { + let wrapper; + let requestHandlers; + + const findLoadingState = () => wrapper.findComponent(GlLoadingIcon); + const findTable = () => wrapper.findComponent(GlTable); + const findCount = () => wrapper.findByTestId('job-artifacts-count'); + + const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status'); + const findSuccessfulJobStatus = () => findStatuses().at(0); + const findFailedJobStatus = () => findStatuses().at(1); + + const findLinks = () => wrapper.findAllComponents(GlLink); + const findJobLink = () => findLinks().at(0); + const findPipelineLink = () => findLinks().at(1); + const findRefLink = () => findLinks().at(2); + const findCommitLink = () => findLinks().at(3); + + const findSize = () => wrapper.findByTestId('job-artifacts-size'); + const findCreated = () => wrapper.findByTestId('job-artifacts-created'); + + const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button'); + const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button'); + const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button'); + + const findPagination = () => wrapper.findComponent(GlPagination); + const setPage = async (page) => { + findPagination().vm.$emit('input', page); + await waitForPromises(); + }; + + let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes]; + while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) { + enoughJobsToPaginate = [ + ...enoughJobsToPaginate, + ...getJobArtifactsResponse.data.project.jobs.nodes, + ]; + } + const getJobArtifactsResponseThatPaginates = { + data: { project: { jobs: { nodes: enoughJobsToPaginate } } }, + }; + + const createComponent = ( + handlers = { + getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse), + destroyArtifactMutation: jest.fn(), + }, + data = {}, + ) => { + requestHandlers = handlers; + wrapper = mountExtended(JobArtifactsTable, { + apolloProvider: createMockApollo([ + [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery], + [destroyArtifactMutation, requestHandlers.destroyArtifactMutation], + ]), + provide: { projectPath: 'project/path' }, + data() { + return data; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('when loading, shows a loading state', () => { + createComponent(); + + expect(findLoadingState().exists()).toBe(true); + }); + + it('on error, shows an alert', async () => { + createComponent({ + getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')), + }); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR }); + }); + + it('with data, renders the table', async () => { + createComponent(); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + }); + + describe('job details', () => { + const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; + const archiveArtifact = job.artifacts.nodes.find( + (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE, + ); + + beforeEach(async () => { + createComponent(); + + await waitForPromises(); + }); + + it('shows the artifact count', () => { + expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`); + }); + + it('expands to show the list of artifacts', async () => { + jest.spyOn(wrapper.vm, 'handleRowToggle'); + + findCount().trigger('click'); + + expect(wrapper.vm.handleRowToggle).toHaveBeenCalled(); + }); + + it('shows the job status as an icon for a successful job', () => { + expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true); + expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false); + }); + + it('shows the job status as a badge for other job statuses', () => { + expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true); + expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false); + }); + + it('shows links to the job, pipeline, ref, and commit', () => { + expect(findJobLink().text()).toBe(job.name); + expect(findJobLink().attributes('href')).toBe(job.webPath); + + expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`); + expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path); + + expect(findRefLink().text()).toBe(job.refName); + expect(findRefLink().attributes('href')).toBe(job.refPath); + + expect(findCommitLink().text()).toBe(job.shortSha); + expect(findCommitLink().attributes('href')).toBe(job.commitPath); + }); + + it('shows the total size of artifacts', () => { + expect(findSize().text()).toBe(totalArtifactsSizeForJob(job)); + }); + + it('shows the created time', () => { + expect(findCreated().text()).toBe('5 years ago'); + }); + + it('shows the download, browse, and delete buttons', () => { + expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath); + expect(findBrowseButton().attributes('disabled')).toBe('disabled'); + expect(findDeleteButton().attributes('disabled')).toBe('disabled'); + }); + }); + + describe('pagination', () => { + const { pageInfo } = getJobArtifactsResponse.data.project.jobs; + + beforeEach(async () => { + createComponent( + { + getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates), + }, + { + jobArtifacts: { + count: enoughJobsToPaginate.length, + pageInfo, + }, + }, + ); + + await waitForPromises(); + }); + + it('renders pagination and passes page props', () => { + expect(findPagination().exists()).toBe(true); + expect(findPagination().props()).toMatchObject({ + value: wrapper.vm.pagination.currentPage, + prevPage: wrapper.vm.prevPage, + nextPage: wrapper.vm.nextPage, + }); + }); + + it('updates query variables when going to previous page', () => { + return setPage(1).then(() => { + expect(wrapper.vm.queryVariables).toMatchObject({ + projectPath: 'project/path', + nextPageCursor: undefined, + prevPageCursor: pageInfo.startCursor, + }); + }); + }); + + it('updates query variables when going to next page', () => { + return setPage(2).then(() => { + expect(wrapper.vm.queryVariables).toMatchObject({ + lastPageSize: null, + nextPageCursor: pageInfo.endCursor, + prevPageCursor: '', + }); + }); + }); + }); +}); diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/artifacts/graphql/cache_update_spec.js new file mode 100644 index 00000000000..4d610328298 --- /dev/null +++ b/spec/frontend/artifacts/graphql/cache_update_spec.js @@ -0,0 +1,67 @@ +import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; +import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update'; + +describe('Artifact table cache updates', () => { + let store; + + const cacheMock = { + project: { + jobs: { + nodes: [ + { artifacts: { nodes: [{ id: 'foo' }] } }, + { artifacts: { nodes: [{ id: 'bar' }] } }, + ], + }, + }, + }; + + const query = getJobArtifactsQuery; + const variables = { fullPath: 'path/to/project' }; + + beforeEach(() => { + store = { + readQuery: jest.fn().mockReturnValue(cacheMock), + writeQuery: jest.fn(), + }; + }); + + describe('removeArtifactFromStore', () => { + it('calls readQuery', () => { + removeArtifactFromStore(store, 'foo', query, variables); + expect(store.readQuery).toHaveBeenCalledWith({ query, variables }); + }); + + it('writes the correct result in the cache', () => { + removeArtifactFromStore(store, 'foo', query, variables); + expect(store.writeQuery).toHaveBeenCalledWith({ + query, + variables, + data: { + project: { + jobs: { + nodes: [{ artifacts: { nodes: [] } }, { artifacts: { nodes: [{ id: 'bar' }] } }], + }, + }, + }, + }); + }); + + it('does not remove an unknown artifact', () => { + removeArtifactFromStore(store, 'baz', query, variables); + expect(store.writeQuery).toHaveBeenCalledWith({ + query, + variables, + data: { + project: { + jobs: { + nodes: [ + { artifacts: { nodes: [{ id: 'foo' }] } }, + { artifacts: { nodes: [{ id: 'bar' }] } }, + ], + }, + }, + }, + }); + }); + }); +}); |