2023-01-13 00:05:48 +05:30
|
|
|
import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } 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';
|
2023-03-17 16:20:25 +05:30
|
|
|
import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
|
2023-01-13 00:05:48 +05:30
|
|
|
import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
|
|
|
|
import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.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 { 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;
|
|
|
|
|
2023-03-17 16:20:25 +05:30
|
|
|
const findBanner = () => wrapper.findComponent(FeedbackBanner);
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
|
|
|
|
const findTable = () => wrapper.findComponent(GlTable);
|
|
|
|
const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
|
|
|
|
const findDetailsInRow = (i) =>
|
|
|
|
findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
|
|
|
|
|
|
|
|
const findCount = () => wrapper.findByTestId('job-artifacts-count');
|
|
|
|
const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
|
|
|
|
|
|
|
|
const findModal = () => wrapper.findComponent(GlModal);
|
|
|
|
|
|
|
|
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 findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-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 job = getJobArtifactsResponse.data.project.jobs.nodes[0];
|
|
|
|
const archiveArtifact = job.artifacts.nodes.find(
|
|
|
|
(artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
|
|
|
|
);
|
|
|
|
|
|
|
|
const createComponent = (
|
|
|
|
handlers = {
|
|
|
|
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
|
|
|
|
},
|
|
|
|
data = {},
|
2023-03-17 16:20:25 +05:30
|
|
|
canDestroyArtifacts = true,
|
2023-01-13 00:05:48 +05:30
|
|
|
) => {
|
|
|
|
requestHandlers = handlers;
|
|
|
|
wrapper = mountExtended(JobArtifactsTable, {
|
|
|
|
apolloProvider: createMockApollo([
|
|
|
|
[getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
|
|
|
|
]),
|
2023-03-17 16:20:25 +05:30
|
|
|
provide: {
|
|
|
|
projectPath: 'project/path',
|
|
|
|
canDestroyArtifacts,
|
|
|
|
artifactsManagementFeedbackImagePath: 'banner/image/path',
|
|
|
|
},
|
2023-01-13 00:05:48 +05:30
|
|
|
data() {
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
wrapper.destroy();
|
|
|
|
});
|
|
|
|
|
2023-03-17 16:20:25 +05:30
|
|
|
it('renders feedback banner', () => {
|
|
|
|
createComponent();
|
|
|
|
|
|
|
|
expect(findBanner().exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
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', () => {
|
|
|
|
beforeEach(async () => {
|
|
|
|
createComponent();
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('shows the artifact count', () => {
|
|
|
|
expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
|
|
|
|
});
|
|
|
|
|
|
|
|
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');
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('row expansion', () => {
|
|
|
|
it('toggles the visibility of the row details', async () => {
|
|
|
|
expect(findDetailsRows().length).toBe(0);
|
|
|
|
|
|
|
|
findCount().trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDetailsRows().length).toBe(1);
|
|
|
|
|
|
|
|
findCount().trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDetailsRows().length).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('expands and collapses jobs', async () => {
|
|
|
|
// both jobs start collapsed
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(false);
|
|
|
|
|
|
|
|
findCountAt(0).trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
// first job is expanded, second row has its details
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(true);
|
|
|
|
expect(findDetailsInRow(2).exists()).toBe(false);
|
|
|
|
|
|
|
|
findCountAt(1).trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
// both jobs are expanded, each has details below it
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(true);
|
|
|
|
expect(findDetailsInRow(2).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(3).exists()).toBe(true);
|
|
|
|
|
|
|
|
findCountAt(0).trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
// first job collapsed, second job expanded
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(2).exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('keeps the job expanded when an artifact is deleted', async () => {
|
|
|
|
findCount().trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(true);
|
|
|
|
|
|
|
|
findArtifactDeleteButton().trigger('click');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findModal().props('visible')).toBe(true);
|
|
|
|
|
|
|
|
wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDetailsInRow(0).exists()).toBe(false);
|
|
|
|
expect(findDetailsInRow(1).exists()).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('download button', () => {
|
|
|
|
it('is a link to the download path for the archive artifact', async () => {
|
|
|
|
createComponent();
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('is disabled when there is no download path', async () => {
|
|
|
|
const jobWithoutDownloadPath = {
|
|
|
|
...job,
|
|
|
|
archive: { downloadPath: null },
|
|
|
|
};
|
|
|
|
|
|
|
|
createComponent(
|
|
|
|
{ getJobArtifactsQuery: jest.fn() },
|
|
|
|
{ jobArtifacts: [jobWithoutDownloadPath] },
|
|
|
|
);
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDownloadButton().attributes('disabled')).toBe('disabled');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('browse button', () => {
|
|
|
|
it('is a link to the browse path for the job', async () => {
|
|
|
|
createComponent();
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('is disabled when there is no browse path', async () => {
|
|
|
|
const jobWithoutBrowsePath = {
|
|
|
|
...job,
|
|
|
|
browseArtifactsPath: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
createComponent(
|
|
|
|
{ getJobArtifactsQuery: jest.fn() },
|
|
|
|
{ jobArtifacts: [jobWithoutBrowsePath] },
|
|
|
|
);
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findBrowseButton().attributes('disabled')).toBe('disabled');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('delete button', () => {
|
2023-03-17 16:20:25 +05:30
|
|
|
it('does not show when user does not have permission', async () => {
|
|
|
|
createComponent({}, {}, false);
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDeleteButton().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
it('shows a disabled delete button for now (coming soon)', async () => {
|
|
|
|
createComponent();
|
|
|
|
|
|
|
|
await waitForPromises();
|
|
|
|
|
|
|
|
expect(findDeleteButton().attributes('disabled')).toBe('disabled');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('pagination', () => {
|
|
|
|
const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
createComponent(
|
|
|
|
{
|
|
|
|
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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: '',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|