Commit fb193190 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch...

Merge branch '229516-dast-site-profile-library-implementation-iteration-1-delete-profile-frontend' into 'master'

Add capability to delete site profile from DAST on-demand library

See merge request gitlab-org/gitlab!38555
parents 21d6deca 78ac3eaf
<script> <script>
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { GlButton, GlTab, GlTabs } from '@gitlab/ui'; import { GlButton, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import ProfilesList from './dast_profiles_list.vue'; import ProfilesList from './dast_profiles_list.vue';
import dastSiteProfilesQuery from '../graphql/dast_site_profiles.query.graphql'; import dastSiteProfilesQuery from '../graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from '../graphql/dast_site_profiles_delete.mutation.graphql';
import * as cacheUtils from '../graphql/cache_utils';
export default { export default {
components: { components: {
...@@ -25,17 +28,17 @@ export default { ...@@ -25,17 +28,17 @@ export default {
return { return {
siteProfiles: [], siteProfiles: [],
siteProfilesPageInfo: {}, siteProfilesPageInfo: {},
hasSiteProfilesLoadingError: false, errorMessage: '',
errorDetails: [],
}; };
}, },
apollo: { apollo: {
siteProfiles: { siteProfiles() {
query: dastSiteProfilesQuery,
variables() {
return { return {
query: dastSiteProfilesQuery,
variables: {
fullPath: this.projectFullPath, fullPath: this.projectFullPath,
first: this.$options.profilesPerPage, first: this.$options.profilesPerPage,
};
}, },
result({ data, error }) { result({ data, error }) {
if (!error) { if (!error) {
...@@ -47,9 +50,13 @@ export default { ...@@ -47,9 +50,13 @@ export default {
return siteProfileEdges.map(({ node }) => node); return siteProfileEdges.map(({ node }) => node);
}, },
error(e) { error(error) {
this.handleLoadingError(e); this.handleError({
exception: error,
message: this.$options.i18n.errorMessages.fetchNetworkError,
});
}, },
};
}, },
}, },
computed: { computed: {
...@@ -61,34 +68,100 @@ export default { ...@@ -61,34 +68,100 @@ export default {
}, },
}, },
methods: { methods: {
handleLoadingError(e) { handleError({ exception, message = '', details = [] }) {
Sentry.captureException(e); Sentry.captureException(exception);
this.hasSiteProfilesLoadingError = true; this.errorMessage = message;
this.errorDetails = details;
},
resetErrors() {
this.errorMessage = '';
this.errorDetails = [];
}, },
fetchMoreProfiles() { fetchMoreProfiles() {
const { $apollo, siteProfilesPageInfo } = this; const {
$apollo,
siteProfilesPageInfo,
$options: { i18n },
} = this;
this.hasSiteProfilesLoadingError = false; this.resetErrors();
$apollo.queries.siteProfiles $apollo.queries.siteProfiles
.fetchMore({ .fetchMore({
variables: { after: siteProfilesPageInfo.endCursor }, variables: { after: siteProfilesPageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => { updateQuery: cacheUtils.appendToPreviousResult,
const newResult = { ...fetchMoreResult }; })
const previousEdges = previousResult.project.siteProfiles.edges; .catch(error => {
const newEdges = newResult.project.siteProfiles.edges; this.handleError({ exception: error, message: i18n.errorMessages.fetchNetworkError });
});
},
deleteSiteProfile(profileToBeDeletedId) {
const {
projectFullPath,
handleError,
$options: { i18n },
$apollo: {
queries: {
siteProfiles: { options: siteProfilesQueryOptions },
},
},
} = this;
newResult.project.siteProfiles.edges = [...previousEdges, ...newEdges]; this.resetErrors();
return newResult; this.$apollo
.mutate({
mutation: dastSiteProfilesDelete,
variables: {
projectFullPath,
profileId: profileToBeDeletedId,
},
update(
store,
{
data: {
dastSiteProfileDelete: { errors = [] },
},
},
) {
if (errors.length === 0) {
cacheUtils.removeProfile({
store,
queryBody: {
query: siteProfilesQueryOptions.query,
variables: siteProfilesQueryOptions.variables,
},
profileToBeDeletedId,
});
} else {
handleError({
message: i18n.errorMessages.deletionBackendError,
details: errors,
});
}
}, },
optimisticResponse: cacheUtils.dastSiteProfilesDeleteResponse(),
}) })
.catch(e => { .catch(error => {
this.handleLoadingError(e); this.handleError({
exception: error,
message: i18n.errorMessages.deletionNetworkError,
});
}); });
}, },
}, },
profilesPerPage: 10, profilesPerPage: 10,
i18n: {
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete site profile. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete site profiles:'),
},
},
}; };
</script> </script>
...@@ -124,12 +197,14 @@ export default { ...@@ -124,12 +197,14 @@ export default {
</template> </template>
<profiles-list <profiles-list
:has-error="hasSiteProfilesLoadingError" :error-message="errorMessage"
:error-details="errorDetails"
:has-more-profiles-to-load="hasMoreSiteProfiles" :has-more-profiles-to-load="hasMoreSiteProfiles"
:is-loading="isLoadingSiteProfiles" :is-loading="isLoadingSiteProfiles"
:profiles-per-page="$options.profilesPerPage" :profiles-per-page="$options.profilesPerPage"
:profiles="siteProfiles" :profiles="siteProfiles"
@loadMoreProfiles="fetchMoreProfiles" @loadMoreProfiles="fetchMoreProfiles"
@deleteProfile="deleteSiteProfile"
/> />
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
......
<script> <script>
import { uniqueId } from 'lodash';
import { import {
GlAlert, GlAlert,
GlButton, GlButton,
GlIcon, GlIcon,
GlModal,
GlSkeletonLoader, GlSkeletonLoader,
GlTable, GlTable,
GlTooltipDirective, GlTooltipDirective,
...@@ -13,6 +15,7 @@ export default { ...@@ -13,6 +15,7 @@ export default {
GlAlert, GlAlert,
GlButton, GlButton,
GlIcon, GlIcon,
GlModal,
GlSkeletonLoader, GlSkeletonLoader,
GlTable, GlTable,
}, },
...@@ -24,10 +27,15 @@ export default { ...@@ -24,10 +27,15 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
hasError: { errorMessage: {
type: Boolean, type: String,
required: false, required: false,
default: false, default: '',
},
errorDetails: {
type: Array,
required: false,
default: () => [],
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -46,10 +54,16 @@ export default { ...@@ -46,10 +54,16 @@ export default {
}, },
data() { data() {
return { return {
isErrorDismissed: false, toBeDeletedProfileId: null,
}; };
}, },
computed: { computed: {
hasError() {
return this.errorMessage !== '';
},
hasErrorDetails() {
return this.errorDetails.length > 0;
},
hasProfiles() { hasProfiles() {
return this.profiles.length > 0; return this.profiles.length > 0;
}, },
...@@ -59,6 +73,21 @@ export default { ...@@ -59,6 +73,21 @@ export default {
shouldShowTable() { shouldShowTable() {
return this.isLoadingInitialProfiles || this.hasProfiles || this.hasError; return this.isLoadingInitialProfiles || this.hasProfiles || this.hasError;
}, },
modalId() {
return `dast-profiles-list-${uniqueId()}`;
},
},
methods: {
handleDelete() {
this.$emit('deleteProfile', this.toBeDeletedProfileId);
},
prepareProfileDeletion(profileId) {
this.toBeDeletedProfileId = profileId;
this.$refs[this.modalId].show();
},
handleCancel() {
this.toBeDeletedProfileId = null;
},
}, },
tableFields: [ tableFields: [
{ {
...@@ -73,7 +102,7 @@ export default { ...@@ -73,7 +102,7 @@ export default {
key: 'validationStatus', key: 'validationStatus',
// NOTE: hidden for now, since the site validation is still WIP and will be finished in an upcoming iteration // NOTE: hidden for now, since the site validation is still WIP and will be finished in an upcoming iteration
// roadmap: https://gitlab.com/groups/gitlab-org/-/epics/2912#ui-configuration // roadmap: https://gitlab.com/groups/gitlab-org/-/epics/2912#ui-configuration
class: 'gl-display-none', class: 'gl-display-none!',
}, },
{ {
key: 'actions', key: 'actions',
...@@ -92,6 +121,21 @@ export default { ...@@ -92,6 +121,21 @@ export default {
stacked="md" stacked="md"
thead-class="gl-display-none" thead-class="gl-display-none"
> >
<template v-if="hasError" #top-row>
<td :colspan="$options.tableFields.length">
<gl-alert class="gl-my-4" variant="danger" :dismissible="false">
{{ errorMessage }}
<ul
v-if="hasErrorDetails"
:aria-label="__('DastProfiles|Error Details')"
class="gl-p-0 gl-m-0"
>
<li v-for="errorDetail in errorDetails" :key="errorDetail">{{ errorDetail }}</li>
</ul>
</gl-alert>
</td>
</template>
<template #cell(profileName)="{ value }"> <template #cell(profileName)="{ value }">
<strong>{{ value }}</strong> <strong>{{ value }}</strong>
</template> </template>
...@@ -107,7 +151,16 @@ export default { ...@@ -107,7 +151,16 @@ export default {
</span> </span>
</template> </template>
<template #cell(actions)> <template #cell(actions)="{ item }">
<div class="gl-text-right">
<gl-button
icon="remove"
variant="danger"
category="secondary"
class="gl-mr-3"
:aria-label="__('Delete')"
@click="prepareProfileDeletion(item.id)"
/>
<!-- <!--
NOTE: The tooltip and `disable` on the button is temporary until the edit feature has been implemented NOTE: The tooltip and `disable` on the button is temporary until the edit feature has been implemented
further details: https://gitlab.com/gitlab-org/gitlab/-/issues/222479#proposal further details: https://gitlab.com/gitlab-org/gitlab/-/issues/222479#proposal
...@@ -122,6 +175,7 @@ export default { ...@@ -122,6 +175,7 @@ export default {
> >
<gl-button disabled>{{ __('Edit') }}</gl-button> <gl-button disabled>{{ __('Edit') }}</gl-button>
</span> </span>
</div>
</template> </template>
<template #table-busy> <template #table-busy>
...@@ -134,18 +188,6 @@ export default { ...@@ -134,18 +188,6 @@ export default {
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
</template> </template>
<template v-if="hasError && !isErrorDismissed" #bottom-row>
<td :colspan="$options.tableFields.length">
<gl-alert class="gl-my-4" variant="danger" :dismissible="false">
{{
s__(
'DastProfiles|Error fetching the profiles list. Please check your network connection and try again.',
)
}}
</gl-alert>
</td>
</template>
</gl-table> </gl-table>
<p v-if="hasMoreProfilesToLoad" class="gl-display-flex gl-justify-content-center"> <p v-if="hasMoreProfilesToLoad" class="gl-display-flex gl-justify-content-center">
...@@ -153,13 +195,27 @@ export default { ...@@ -153,13 +195,27 @@ export default {
data-testid="loadMore" data-testid="loadMore"
:loading="isLoading && !hasError" :loading="isLoading && !hasError"
@click="$emit('loadMoreProfiles')" @click="$emit('loadMoreProfiles')"
>{{ __('Load more') }}</gl-button
> >
{{ __('Load more') }}
</gl-button>
</p> </p>
</div> </div>
<p v-else class="gl-my-4"> <p v-else class="gl-my-4">
{{ s__('DastProfiles|No profiles created yet') }} {{ s__('DastProfiles|No profiles created yet') }}
</p> </p>
<gl-modal
:ref="modalId"
:modal-id="modalId"
:title="s__('DastProfiles|Are you sure you want to delete this profile?')"
:ok-title="__('Delete')"
:static="true"
:lazy="true"
ok-variant="danger"
body-class="gl-display-none"
@ok="handleDelete"
@cancel="handleCancel"
/>
</section> </section>
</template> </template>
/**
* Appends paginated results to existing ones
* - to be used with $apollo.queries.x.fetchMore
*
* @param previousResult
* @param fetchMoreResult
* @returns {*}
*/
export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => {
const newResult = { ...fetchMoreResult };
const previousEdges = previousResult.project.siteProfiles.edges;
const newEdges = newResult.project.siteProfiles.edges;
newResult.project.siteProfiles.edges = [...previousEdges, ...newEdges];
return newResult;
};
/**
* Removes profile with given id from the cache and writes the result to it
*
* @param store
* @param queryBody
* @param profileToBeDeletedId
*/
export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => {
const data = store.readQuery(queryBody);
data.project.siteProfiles.edges = data.project.siteProfiles.edges.filter(({ node }) => {
return node.id !== profileToBeDeletedId;
});
store.writeQuery({ ...queryBody, data });
};
/**
* Returns an object representing a optimistic response for site-profile deletion
*
* @returns {{__typename: string, dastSiteProfileDelete: {__typename: string, errors: []}}}
*/
export const dastSiteProfilesDeleteResponse = () => ({
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
dastSiteProfileDelete: {
__typename: 'DastSiteProfileDeletePayload',
errors: [],
},
});
mutation dastSiteProfileDelete($projectFullPath: ID!, $profileId: DastSiteProfileID!) {
dastSiteProfileDelete(input: { fullPath: $projectFullPath, id: $profileId }) {
errors
}
}
import { merge } from 'lodash'; import { merge } from 'lodash';
import { mount } from '@vue/test-utils'; import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { GlModal } from '@gitlab/ui';
import DastProfilesList from 'ee/dast_profiles/components/dast_profiles_list.vue'; import DastProfilesList from 'ee/dast_profiles/components/dast_profiles_list.vue';
const TEST_ERROR_MESSAGE = 'something went wrong';
describe('EE - DastProfilesList', () => { describe('EE - DastProfilesList', () => {
let wrapper; let wrapper;
const createComponent = (options = {}) => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { const defaultProps = {
profiles: [], profiles: [],
hasMorePages: false, hasMorePages: false,
profilesPerPage: 10, profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
}; };
wrapper = mount( wrapper = mountFn(
DastProfilesList, DastProfilesList,
merge( merge(
{}, {},
...@@ -25,6 +30,9 @@ describe('EE - DastProfilesList', () => { ...@@ -25,6 +30,9 @@ describe('EE - DastProfilesList', () => {
); );
}; };
const createComponent = createComponentFactory();
const createFullComponent = createComponentFactory(mount);
const withinComponent = () => within(wrapper.element); const withinComponent = () => within(wrapper.element);
const getTable = () => withinComponent().getByRole('table', { name: /site profiles/i }); const getTable = () => withinComponent().getByRole('table', { name: /site profiles/i });
const getAllRowGroups = () => within(getTable()).getAllByRole('rowgroup'); const getAllRowGroups = () => within(getTable()).getAllByRole('rowgroup');
...@@ -36,7 +44,11 @@ describe('EE - DastProfilesList', () => { ...@@ -36,7 +44,11 @@ describe('EE - DastProfilesList', () => {
const getAllTableRows = () => within(getTableBody()).getAllByRole('row'); const getAllTableRows = () => within(getTableBody()).getAllByRole('row');
const getLoadMoreButton = () => wrapper.find('[data-testid="loadMore"]'); const getLoadMoreButton = () => wrapper.find('[data-testid="loadMore"]');
const getAllLoadingIndicators = () => withinComponent().queryAllByTestId('loadingIndicator'); const getAllLoadingIndicators = () => withinComponent().queryAllByTestId('loadingIndicator');
const getErrorMessage = () => withinComponent().queryByText(/error fetching the profiles list/i); const getErrorMessage = () => withinComponent().queryByText(TEST_ERROR_MESSAGE);
const getErrorDetails = () => withinComponent().queryByRole('list', { name: /error details/i });
const getDeleteButtonWithin = element =>
createWrapper(within(element).queryByRole('button', { name: /delete/i }));
const getModal = () => wrapper.find(GlModal);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -48,7 +60,7 @@ describe('EE - DastProfilesList', () => { ...@@ -48,7 +60,7 @@ describe('EE - DastProfilesList', () => {
describe('initial load', () => { describe('initial load', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ propsData: { isLoading: true, profilesPerPage } }); createFullComponent({ propsData: { isLoading: true, profilesPerPage } });
}); });
it('shows a loading indicator for each profile item', () => { it('shows a loading indicator for each profile item', () => {
...@@ -106,14 +118,13 @@ describe('EE - DastProfilesList', () => { ...@@ -106,14 +118,13 @@ describe('EE - DastProfilesList', () => {
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)]; const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
it('does not show loading indicators', () => {
createComponent({});
expect(getAllLoadingIndicators()).toHaveLength(0);
});
describe('profiles list', () => { describe('profiles list', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ propsData: { profiles } }); createFullComponent({ propsData: { profiles } });
});
it('does not show loading indicators', () => {
expect(getAllLoadingIndicators()).toHaveLength(0);
}); });
it('renders a list of profiles', () => { it('renders a list of profiles', () => {
...@@ -133,6 +144,7 @@ describe('EE - DastProfilesList', () => { ...@@ -133,6 +144,7 @@ describe('EE - DastProfilesList', () => {
expect(targetUrlCell.innerText).toContain(profile.targetUrl); expect(targetUrlCell.innerText).toContain(profile.targetUrl);
expect(validationStatusCell.innerText).toContain(profile.validationStatus); expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /edit/i })).not.toBe(null); expect(within(actionsCell).getByRole('button', { name: /edit/i })).not.toBe(null);
expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null);
}); });
}); });
...@@ -145,14 +157,14 @@ describe('EE - DastProfilesList', () => { ...@@ -145,14 +157,14 @@ describe('EE - DastProfilesList', () => {
describe('with more profiles', () => { describe('with more profiles', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ propsData: { profiles, hasMoreProfilesToLoad: true } }); createFullComponent({ propsData: { profiles, hasMoreProfilesToLoad: true } });
}); });
it('shows that there are more projects to be loaded', () => { it('shows that there are more projects to be loaded', () => {
expect(getLoadMoreButton().exists()).toBe(true); expect(getLoadMoreButton().exists()).toBe(true);
}); });
it('emits "loadMore" when the load-more button is clicked', async () => { it('emits "loadMoreProfiles" when the load-more button is clicked', async () => {
expect(wrapper.emitted('loadMoreProfiles')).toBe(undefined); expect(wrapper.emitted('loadMoreProfiles')).toBe(undefined);
await getLoadMoreButton().trigger('click'); await getLoadMoreButton().trigger('click');
...@@ -161,19 +173,59 @@ describe('EE - DastProfilesList', () => { ...@@ -161,19 +173,59 @@ describe('EE - DastProfilesList', () => {
}); });
}); });
}); });
describe.each(profiles)('delete profile', profile => {
beforeEach(() => {
createFullComponent({ propsData: { profiles } });
});
const getCurrentProfileDeleteButton = () =>
getDeleteButtonWithin(getTableRowForProfile(profile));
it('opens a modal with the correct title when a delete button is clicked', async () => {
expect(getModal().isEmpty()).toBe(true);
getCurrentProfileDeleteButton().trigger('click');
await wrapper.vm.$nextTick();
expect(
within(getModal().element).getByText(/are you sure you want to delete this profile/i),
).not.toBe(null);
});
it(`emits "@deleteProfile" with the right payload when the modal's primary action is triggered`, async () => {
expect(wrapper.emitted('deleteProfile')).toBe(undefined);
getCurrentProfileDeleteButton().trigger('click');
await wrapper.vm.$nextTick();
getModal().vm.$emit('ok');
expect(wrapper.emitted('deleteProfile')[0]).toEqual([profile.id]);
});
});
}); });
describe('errors', () => { describe('errors', () => {
it('does not show an error message by default', () => { it('does not show an error message by default', () => {
createComponent(); createFullComponent();
expect(getErrorMessage()).toBe(null); expect(getErrorMessage()).toBe(null);
expect(getErrorDetails()).toBe(null);
}); });
it('shows an error message', () => { it('shows an error message and details', () => {
createComponent({ propsData: { hasError: true } }); const errorDetails = ['foo', 'bar'];
createFullComponent({
propsData: { errorMessage: TEST_ERROR_MESSAGE, errorDetails },
});
expect(getErrorMessage()).not.toBe(null); expect(getErrorMessage()).not.toBe(null);
expect(getErrorDetails()).not.toBe(null);
expect(within(getErrorDetails()).getByText(errorDetails[0])).not.toBe(null);
expect(within(getErrorDetails()).getByText(errorDetails[1])).not.toBe(null);
}); });
}); });
}); });
...@@ -19,9 +19,12 @@ describe('EE - DastProfiles', () => { ...@@ -19,9 +19,12 @@ describe('EE - DastProfiles', () => {
const defaultMocks = { const defaultMocks = {
$apollo: { $apollo: {
queries: { queries: {
siteProfiles: {}, siteProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
}, },
}, },
mutate: jest.fn().mockResolvedValue(),
},
}; };
wrapper = mountFn( wrapper = mountFn(
...@@ -99,7 +102,8 @@ describe('EE - DastProfiles', () => { ...@@ -99,7 +102,8 @@ describe('EE - DastProfiles', () => {
it('passes down the correct default props', () => { it('passes down the correct default props', () => {
expect(getSiteProfilesComponent().props()).toEqual({ expect(getSiteProfilesComponent().props()).toEqual({
hasError: false, errorMessage: '',
errorDetails: [],
hasMoreProfilesToLoad: false, hasMoreProfilesToLoad: false,
isLoading: false, isLoading: false,
profilesPerPage: expect.any(Number), profilesPerPage: expect.any(Number),
...@@ -107,35 +111,51 @@ describe('EE - DastProfiles', () => { ...@@ -107,35 +111,51 @@ describe('EE - DastProfiles', () => {
}); });
}); });
it.each([true, false])('passes down the error state', async hasError => { it.each([true, false])('passes down the loading state', loading => {
wrapper.setData({ hasSiteProfilesLoadingError: hasError }); createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading } } } } });
await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props('hasError')).toBe(hasError); expect(getSiteProfilesComponent().props('isLoading')).toBe(loading);
}); });
it.each([true, false])('passes down the pagination information', async hasNextPage => { it.each`
wrapper.setData({ siteProfilesPageInfo: { hasNextPage } }); givenData | propName | expectedPropValue
${{ errorMessage: 'foo' }} | ${'errorMessage'} | ${'foo'}
${{ siteProfilesPageInfo: { hasNextPage: true } }} | ${'hasMoreProfilesToLoad'} | ${true}
${{ siteProfiles: [{ foo: 'bar' }] }} | ${'profiles'} | ${[{ foo: 'bar' }]}
`('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => {
wrapper.setData(givenData);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props('hasMoreProfilesToLoad')).toBe(hasNextPage); expect(getSiteProfilesComponent().props(propName)).toEqual(expectedPropValue);
}); });
it.each([true, false])('passes down the loading state', loading => { it('fetches more results when "@loadMoreProfiles" is emitted', () => {
createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading } } } } }); const {
$apollo: {
queries: {
siteProfiles: { fetchMore },
},
},
} = wrapper.vm;
expect(getSiteProfilesComponent().props('isLoading')).toBe(loading); expect(fetchMore).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('loadMoreProfiles');
expect(fetchMore).toHaveBeenCalledTimes(1);
}); });
it('passes down the profiles data', async () => { it('deletes profile when "@deleteProfile" is emitted', () => {
const siteProfiles = [{}]; const {
wrapper.setData({ siteProfiles }); $apollo: { mutate },
} = wrapper.vm;
await wrapper.vm.$nextTick(); expect(mutate).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('deleteProfile');
expect(getSiteProfilesComponent().props('profiles')).toBe(siteProfiles); expect(mutate).toHaveBeenCalledTimes(1);
}); });
}); });
}); });
import {
appendToPreviousResult,
removeProfile,
dastSiteProfilesDeleteResponse,
} from 'ee/dast_profiles/graphql/cache_utils';
describe('EE - DastProfiles GraphQL CacheUtils', () => {
describe('appendToPreviousResult', () => {
it('appends new results to previous', () => {
const previousResult = { project: { siteProfiles: { edges: ['foo'] } } };
const fetchMoreResult = { project: { siteProfiles: { edges: ['bar'] } } };
const expected = { project: { siteProfiles: { edges: ['foo', 'bar'] } } };
const result = appendToPreviousResult(previousResult, { fetchMoreResult });
expect(result).toEqual(expected);
});
});
describe('removeProfile', () => {
it('removes the profile with the given id from the cache', () => {
const mockQueryBody = { query: 'foo', variables: { foo: 'bar' } };
const mockProfiles = [{ id: 0 }, { id: 1 }];
const mockData = {
project: {
siteProfiles: {
edges: [{ node: mockProfiles[0] }, { node: mockProfiles[1] }],
},
},
};
const mockStore = {
readQuery: () => mockData,
writeQuery: jest.fn(),
};
removeProfile({
store: mockStore,
queryBody: mockQueryBody,
profileToBeDeletedId: mockProfiles[0].id,
});
expect(mockStore.writeQuery).toHaveBeenCalledWith({
...mockQueryBody,
data: {
project: {
siteProfiles: {
edges: [{ node: mockProfiles[1] }],
},
},
},
});
});
});
describe('dastSiteProfilesDeleteResponse', () => {
it('returns a mutation response with the correct shape', () => {
expect(dastSiteProfilesDeleteResponse()).toEqual({
__typename: 'Mutation',
dastSiteProfileDelete: {
__typename: 'DastSiteProfileDeletePayload',
errors: [],
},
});
});
});
});
...@@ -7554,9 +7554,21 @@ msgstr "" ...@@ -7554,9 +7554,21 @@ msgstr ""
msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan." msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan."
msgstr "" msgstr ""
msgid "DastProfiles|Are you sure you want to delete this profile?"
msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again." msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr "" msgstr ""
msgid "DastProfiles|Could not delete site profile. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not update the site profile. Please try again." msgid "DastProfiles|Could not update the site profile. Please try again."
msgstr "" msgstr ""
...@@ -7572,7 +7584,7 @@ msgstr "" ...@@ -7572,7 +7584,7 @@ msgstr ""
msgid "DastProfiles|Edit site profile" msgid "DastProfiles|Edit site profile"
msgstr "" msgstr ""
msgid "DastProfiles|Error fetching the profiles list. Please check your network connection and try again." msgid "DastProfiles|Error Details"
msgstr "" msgstr ""
msgid "DastProfiles|Manage Profiles" msgid "DastProfiles|Manage Profiles"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment