Projects and groups badges settings UI
This commit is contained in:
parent
dd552d06f6
commit
31dd86b636
|
|
@ -0,0 +1,121 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import Tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'Badge',
|
||||
components: {
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
Tooltip,
|
||||
},
|
||||
directives: {
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
imageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
linkUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasError: false,
|
||||
isLoading: true,
|
||||
numRetries: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageUrlWithRetries() {
|
||||
if (this.numRetries === 0) {
|
||||
return this.imageUrl;
|
||||
}
|
||||
|
||||
return `${this.imageUrl}#retries=${this.numRetries}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
imageUrl() {
|
||||
this.hasError = false;
|
||||
this.isLoading = true;
|
||||
this.numRetries = 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onError() {
|
||||
this.isLoading = false;
|
||||
this.hasError = true;
|
||||
},
|
||||
onLoad() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
reloadImage() {
|
||||
this.hasError = false;
|
||||
this.isLoading = true;
|
||||
this.numRetries += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
v-show="!isLoading && !hasError"
|
||||
:href="linkUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
class="project-badge"
|
||||
:src="imageUrlWithRetries"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<loading-icon
|
||||
v-show="isLoading"
|
||||
:inline="true"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-show="hasError"
|
||||
class="btn-group"
|
||||
>
|
||||
<div class="btn btn-default btn-xs disabled">
|
||||
<icon
|
||||
class="prepend-left-8 append-right-8"
|
||||
name="doc_image"
|
||||
:size="16"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-default btn-xs disabled"
|
||||
>
|
||||
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-show="hasError"
|
||||
class="btn btn-transparent btn-xs text-primary"
|
||||
type="button"
|
||||
v-tooltip
|
||||
:title="s__('Badges|Reload badge image')"
|
||||
@click="reloadImage"
|
||||
>
|
||||
<icon
|
||||
name="retry"
|
||||
:size="16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import createFlash from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import createEmptyBadge from '../empty_badge';
|
||||
import Badge from './badge.vue';
|
||||
|
||||
const badgePreviewDelayInMilliseconds = 1500;
|
||||
|
||||
export default {
|
||||
name: 'BadgeForm',
|
||||
components: {
|
||||
Badge,
|
||||
LoadingButton,
|
||||
LoadingIcon,
|
||||
},
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'badgeInAddForm',
|
||||
'badgeInEditForm',
|
||||
'docsUrl',
|
||||
'isRendering',
|
||||
'isSaving',
|
||||
'renderedBadge',
|
||||
]),
|
||||
badge() {
|
||||
if (this.isEditing) {
|
||||
return this.badgeInEditForm;
|
||||
}
|
||||
|
||||
return this.badgeInAddForm;
|
||||
},
|
||||
canSubmit() {
|
||||
return (
|
||||
this.badge !== null &&
|
||||
this.badge.imageUrl &&
|
||||
this.badge.imageUrl.trim() !== '' &&
|
||||
this.badge.linkUrl &&
|
||||
this.badge.linkUrl.trim() !== '' &&
|
||||
!this.isSaving
|
||||
);
|
||||
},
|
||||
helpText() {
|
||||
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
|
||||
.map(placeholder => `<code>%{${placeholder}}</code>`)
|
||||
.join(', ');
|
||||
return sprintf(
|
||||
s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
|
||||
{
|
||||
docsLinkEnd: '</a>',
|
||||
docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
|
||||
placeholders,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
renderedImageUrl() {
|
||||
return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
|
||||
},
|
||||
renderedLinkUrl() {
|
||||
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
|
||||
},
|
||||
imageUrl: {
|
||||
get() {
|
||||
return this.badge ? this.badge.imageUrl : '';
|
||||
},
|
||||
set(imageUrl) {
|
||||
const badge = this.badge || createEmptyBadge();
|
||||
this.updateBadgeInForm({
|
||||
...badge,
|
||||
imageUrl,
|
||||
});
|
||||
},
|
||||
},
|
||||
linkUrl: {
|
||||
get() {
|
||||
return this.badge ? this.badge.linkUrl : '';
|
||||
},
|
||||
set(linkUrl) {
|
||||
const badge = this.badge || createEmptyBadge();
|
||||
this.updateBadgeInForm({
|
||||
...badge,
|
||||
linkUrl,
|
||||
});
|
||||
},
|
||||
},
|
||||
submitButtonLabel() {
|
||||
if (this.isEditing) {
|
||||
return s__('Badges|Save changes');
|
||||
}
|
||||
return s__('Badges|Add badge');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
|
||||
debouncedPreview: _.debounce(function preview() {
|
||||
this.renderBadge();
|
||||
}, badgePreviewDelayInMilliseconds),
|
||||
onCancel() {
|
||||
this.stopEditing();
|
||||
},
|
||||
onSubmit() {
|
||||
if (!this.canSubmit) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.isEditing) {
|
||||
return this.saveBadge()
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|The badge was saved.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(
|
||||
s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return this.addBadge()
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|A new badge was added.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(
|
||||
s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
},
|
||||
badgeImageUrlPlaceholder:
|
||||
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
|
||||
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="prepend-top-default append-bottom-default"
|
||||
@submit.prevent.stop="onSubmit"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
|
||||
<input
|
||||
id="badge-link-url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="linkUrl"
|
||||
:placeholder="$options.badgeLinkUrlPlaceholder"
|
||||
@input="debouncedPreview"
|
||||
/>
|
||||
<span
|
||||
class="help-block"
|
||||
v-html="helpText"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
|
||||
<input
|
||||
id="badge-image-url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="imageUrl"
|
||||
:placeholder="$options.badgeImageUrlPlaceholder"
|
||||
@input="debouncedPreview"
|
||||
/>
|
||||
<span
|
||||
class="help-block"
|
||||
v-html="helpText"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
|
||||
<badge
|
||||
id="badge-preview"
|
||||
v-show="renderedBadge && !isRendering"
|
||||
:image-url="renderedImageUrl"
|
||||
:link-url="renderedLinkUrl"
|
||||
/>
|
||||
<p v-show="isRendering">
|
||||
<loading-icon
|
||||
:inline="true"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
v-show="!renderedBadge && !isRendering"
|
||||
class="disabled-content"
|
||||
>{{ s__('Badges|No image to preview') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="row-content-block">
|
||||
<loading-button
|
||||
type="submit"
|
||||
container-class="btn btn-success"
|
||||
:disabled="!canSubmit"
|
||||
:loading="isSaving"
|
||||
:label="submitButtonLabel"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-cancel"
|
||||
type="button"
|
||||
v-if="isEditing"
|
||||
@click="onCancel"
|
||||
>{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import BadgeListRow from './badge_list_row.vue';
|
||||
import { GROUP_BADGE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'BadgeList',
|
||||
components: {
|
||||
BadgeListRow,
|
||||
LoadingIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['badges', 'isLoading', 'kind']),
|
||||
hasNoBadges() {
|
||||
return !this.isLoading && (!this.badges || !this.badges.length);
|
||||
},
|
||||
isGroupBadge() {
|
||||
return this.kind === GROUP_BADGE;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ s__('Badges|Your badges') }}
|
||||
<span
|
||||
v-show="!isLoading"
|
||||
class="badge"
|
||||
>{{ badges.length }}</span>
|
||||
</div>
|
||||
<loading-icon
|
||||
v-show="isLoading"
|
||||
class="panel-body"
|
||||
size="2"
|
||||
/>
|
||||
<div
|
||||
v-if="hasNoBadges"
|
||||
class="panel-body"
|
||||
>
|
||||
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
|
||||
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="panel-body"
|
||||
>
|
||||
<badge-list-row
|
||||
v-for="badge in badges"
|
||||
:key="badge.id"
|
||||
:badge="badge"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import { PROJECT_BADGE } from '../constants';
|
||||
import Badge from './badge.vue';
|
||||
|
||||
export default {
|
||||
name: 'BadgeListRow',
|
||||
components: {
|
||||
Badge,
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
},
|
||||
props: {
|
||||
badge: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['kind']),
|
||||
badgeKindText() {
|
||||
if (this.badge.kind === PROJECT_BADGE) {
|
||||
return s__('Badges|Project Badge');
|
||||
}
|
||||
|
||||
return s__('Badges|Group Badge');
|
||||
},
|
||||
canEditBadge() {
|
||||
return this.badge.kind === this.kind;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['editBadge', 'updateBadgeInModal']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
|
||||
<badge
|
||||
class="table-section section-30"
|
||||
:image-url="badge.renderedImageUrl"
|
||||
:link-url="badge.renderedLinkUrl"
|
||||
/>
|
||||
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
|
||||
<div class="table-section section-10">
|
||||
<span class="badge">{{ badgeKindText }}</span>
|
||||
</div>
|
||||
<div class="table-section section-10 table-button-footer">
|
||||
<div
|
||||
v-if="canEditBadge"
|
||||
class="table-action-buttons">
|
||||
<button
|
||||
class="btn btn-default append-right-8"
|
||||
type="button"
|
||||
:disabled="badge.isDeleting"
|
||||
@click="editBadge(badge)"
|
||||
>
|
||||
<icon
|
||||
name="pencil"
|
||||
:size="16"
|
||||
:aria-label="__('Edit')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#delete-badge-modal"
|
||||
:disabled="badge.isDeleting"
|
||||
@click="updateBadgeInModal(badge)"
|
||||
>
|
||||
<icon
|
||||
name="remove"
|
||||
:size="16"
|
||||
:aria-label="__('Delete')"
|
||||
/>
|
||||
</button>
|
||||
<loading-icon
|
||||
v-show="badge.isDeleting"
|
||||
:inline="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import Badge from './badge.vue';
|
||||
import BadgeForm from './badge_form.vue';
|
||||
import BadgeList from './badge_list.vue';
|
||||
|
||||
export default {
|
||||
name: 'BadgeSettings',
|
||||
components: {
|
||||
Badge,
|
||||
BadgeForm,
|
||||
BadgeList,
|
||||
GlModal,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['badgeInModal', 'isEditing']),
|
||||
deleteModalText() {
|
||||
return s__(
|
||||
'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['deleteBadge']),
|
||||
onSubmitModal() {
|
||||
this.deleteBadge(this.badgeInModal)
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|The badge was deleted.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(s__('Badges|Deleting the badge failed, please try again.'));
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="badge-settings">
|
||||
<gl-modal
|
||||
id="delete-badge-modal"
|
||||
:header-title-text="s__('Badges|Delete badge?')"
|
||||
footer-primary-button-variant="danger"
|
||||
:footer-primary-button-text="s__('Badges|Delete badge')"
|
||||
@submit="onSubmitModal">
|
||||
<div class="well">
|
||||
<badge
|
||||
:image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
|
||||
:link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
|
||||
/>
|
||||
</div>
|
||||
<p v-html="deleteModalText"></p>
|
||||
</gl-modal>
|
||||
|
||||
<badge-form
|
||||
v-show="isEditing"
|
||||
:is-editing="true"
|
||||
/>
|
||||
|
||||
<badge-form
|
||||
v-show="!isEditing"
|
||||
:is-editing="false"
|
||||
/>
|
||||
<badge-list v-show="!isEditing" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export const GROUP_BADGE = 'group';
|
||||
export const PROJECT_BADGE = 'project';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default () => ({
|
||||
imageUrl: '',
|
||||
isDeleting: false,
|
||||
linkUrl: '',
|
||||
renderedImageUrl: '',
|
||||
renderedLinkUrl: '',
|
||||
});
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import types from './mutation_types';
|
||||
|
||||
export const transformBackendBadge = badge => ({
|
||||
id: badge.id,
|
||||
imageUrl: badge.image_url,
|
||||
kind: badge.kind,
|
||||
linkUrl: badge.link_url,
|
||||
renderedImageUrl: badge.rendered_image_url,
|
||||
renderedLinkUrl: badge.rendered_link_url,
|
||||
isDeleting: false,
|
||||
});
|
||||
|
||||
export default {
|
||||
requestNewBadge({ commit }) {
|
||||
commit(types.REQUEST_NEW_BADGE);
|
||||
},
|
||||
receiveNewBadge({ commit }, newBadge) {
|
||||
commit(types.RECEIVE_NEW_BADGE, newBadge);
|
||||
},
|
||||
receiveNewBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_NEW_BADGE_ERROR);
|
||||
},
|
||||
addBadge({ dispatch, state }) {
|
||||
const newBadge = state.badgeInAddForm;
|
||||
const endpoint = state.apiEndpointUrl;
|
||||
dispatch('requestNewBadge');
|
||||
return axios
|
||||
.post(endpoint, {
|
||||
image_url: newBadge.imageUrl,
|
||||
link_url: newBadge.linkUrl,
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveNewBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveNewBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
requestDeleteBadge({ commit }, badgeId) {
|
||||
commit(types.REQUEST_DELETE_BADGE, badgeId);
|
||||
},
|
||||
receiveDeleteBadge({ commit }, badgeId) {
|
||||
commit(types.RECEIVE_DELETE_BADGE, badgeId);
|
||||
},
|
||||
receiveDeleteBadgeError({ commit }, badgeId) {
|
||||
commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
|
||||
},
|
||||
deleteBadge({ dispatch, state }, badge) {
|
||||
const badgeId = badge.id;
|
||||
dispatch('requestDeleteBadge', badgeId);
|
||||
const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
|
||||
return axios
|
||||
.delete(endpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveDeleteBadgeError', badgeId);
|
||||
throw error;
|
||||
})
|
||||
.then(() => {
|
||||
dispatch('receiveDeleteBadge', badgeId);
|
||||
});
|
||||
},
|
||||
|
||||
editBadge({ commit }, badge) {
|
||||
commit(types.START_EDITING, badge);
|
||||
},
|
||||
|
||||
requestLoadBadges({ commit }, data) {
|
||||
commit(types.REQUEST_LOAD_BADGES, data);
|
||||
},
|
||||
receiveLoadBadges({ commit }, badges) {
|
||||
commit(types.RECEIVE_LOAD_BADGES, badges);
|
||||
},
|
||||
receiveLoadBadgesError({ commit }) {
|
||||
commit(types.RECEIVE_LOAD_BADGES_ERROR);
|
||||
},
|
||||
|
||||
loadBadges({ dispatch, state }, data) {
|
||||
dispatch('requestLoadBadges', data);
|
||||
const endpoint = state.apiEndpointUrl;
|
||||
return axios
|
||||
.get(endpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveLoadBadgesError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
|
||||
});
|
||||
},
|
||||
|
||||
requestRenderedBadge({ commit }) {
|
||||
commit(types.REQUEST_RENDERED_BADGE);
|
||||
},
|
||||
receiveRenderedBadge({ commit }, renderedBadge) {
|
||||
commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
|
||||
},
|
||||
receiveRenderedBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_RENDERED_BADGE_ERROR);
|
||||
},
|
||||
|
||||
renderBadge({ dispatch, state }) {
|
||||
const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
|
||||
const { linkUrl, imageUrl } = badge;
|
||||
if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
|
||||
return Promise.resolve(badge);
|
||||
}
|
||||
|
||||
dispatch('requestRenderedBadge');
|
||||
|
||||
const parameters = [
|
||||
`link_url=${encodeURIComponent(linkUrl)}`,
|
||||
`image_url=${encodeURIComponent(imageUrl)}`,
|
||||
].join('&');
|
||||
const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
|
||||
return axios
|
||||
.get(renderEndpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveRenderedBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
|
||||
requestUpdatedBadge({ commit }) {
|
||||
commit(types.REQUEST_UPDATED_BADGE);
|
||||
},
|
||||
receiveUpdatedBadge({ commit }, updatedBadge) {
|
||||
commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
|
||||
},
|
||||
receiveUpdatedBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_UPDATED_BADGE_ERROR);
|
||||
},
|
||||
|
||||
saveBadge({ dispatch, state }) {
|
||||
const badge = state.badgeInEditForm;
|
||||
const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
|
||||
dispatch('requestUpdatedBadge');
|
||||
return axios
|
||||
.put(endpoint, {
|
||||
image_url: badge.imageUrl,
|
||||
link_url: badge.linkUrl,
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveUpdatedBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
|
||||
stopEditing({ commit }) {
|
||||
commit(types.STOP_EDITING);
|
||||
},
|
||||
|
||||
updateBadgeInForm({ commit }, badge) {
|
||||
commit(types.UPDATE_BADGE_IN_FORM, badge);
|
||||
},
|
||||
|
||||
updateBadgeInModal({ commit }, badge) {
|
||||
commit(types.UPDATE_BADGE_IN_MODAL, badge);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import createState from './state';
|
||||
import actions from './actions';
|
||||
import mutations from './mutations';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: createState(),
|
||||
actions,
|
||||
mutations,
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
export default {
|
||||
RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
|
||||
RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
|
||||
RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
|
||||
RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
|
||||
RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
|
||||
RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
|
||||
RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
|
||||
RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
|
||||
RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
|
||||
RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
|
||||
REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
|
||||
REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
|
||||
REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
|
||||
REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
|
||||
REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
|
||||
START_EDITING: 'START_EDITING',
|
||||
STOP_EDITING: 'STOP_EDITING',
|
||||
UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
|
||||
UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
|
||||
};
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import types from './mutation_types';
|
||||
import { PROJECT_BADGE } from '../constants';
|
||||
|
||||
const reorderBadges = badges =>
|
||||
badges.sort((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
return a.kind === PROJECT_BADGE ? 1 : -1;
|
||||
}
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
export default {
|
||||
[types.RECEIVE_NEW_BADGE](state, newBadge) {
|
||||
Object.assign(state, {
|
||||
badgeInAddForm: null,
|
||||
badges: reorderBadges(state.badges.concat(newBadge)),
|
||||
isSaving: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_NEW_BADGE_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_NEW_BADGE](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === updatedBadge.id) {
|
||||
return updatedBadge;
|
||||
}
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: null,
|
||||
badges,
|
||||
isEditing: false,
|
||||
isSaving: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_UPDATED_BADGE_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_UPDATED_BADGE](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_LOAD_BADGES](state, badges) {
|
||||
Object.assign(state, {
|
||||
badges: reorderBadges(badges),
|
||||
isLoading: false,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_LOAD_BADGES_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isLoading: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_LOAD_BADGES](state, data) {
|
||||
Object.assign(state, {
|
||||
kind: data.kind, // project or group
|
||||
apiEndpointUrl: data.apiEndpointUrl,
|
||||
docsUrl: data.docsUrl,
|
||||
isLoading: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_DELETE_BADGE](state, badgeId) {
|
||||
const badges = state.badges.filter(badge => badge.id !== badgeId);
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === badgeId) {
|
||||
return {
|
||||
...badge,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_DELETE_BADGE](state, badgeId) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === badgeId) {
|
||||
return {
|
||||
...badge,
|
||||
isDeleting: true,
|
||||
};
|
||||
}
|
||||
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
|
||||
Object.assign(state, { isRendering: false, renderedBadge });
|
||||
},
|
||||
[types.RECEIVE_RENDERED_BADGE_ERROR](state) {
|
||||
Object.assign(state, { isRendering: false });
|
||||
},
|
||||
[types.REQUEST_RENDERED_BADGE](state) {
|
||||
Object.assign(state, { isRendering: true });
|
||||
},
|
||||
|
||||
[types.START_EDITING](state, badge) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: { ...badge },
|
||||
isEditing: true,
|
||||
renderedBadge: { ...badge },
|
||||
});
|
||||
},
|
||||
[types.STOP_EDITING](state) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: null,
|
||||
isEditing: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
|
||||
[types.UPDATE_BADGE_IN_FORM](state, badge) {
|
||||
if (state.isEditing) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: badge,
|
||||
});
|
||||
} else {
|
||||
Object.assign(state, {
|
||||
badgeInAddForm: badge,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[types.UPDATE_BADGE_IN_MODAL](state, badge) {
|
||||
Object.assign(state, {
|
||||
badgeInModal: badge,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export default () => ({
|
||||
apiEndpointUrl: null,
|
||||
badgeInAddForm: null,
|
||||
badgeInEditForm: null,
|
||||
badgeInModal: null,
|
||||
badges: [],
|
||||
docsUrl: null,
|
||||
renderedBadge: null,
|
||||
isEditing: false,
|
||||
isLoading: false,
|
||||
isRendering: false,
|
||||
isSaving: false,
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { GROUP_BADGE } from '~/badges/constants';
|
||||
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
mountBadgeSettings(GROUP_BADGE);
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { PROJECT_BADGE } from '~/badges/constants';
|
||||
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
mountBadgeSettings(PROJECT_BADGE);
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import Vue from 'vue';
|
||||
import BadgeSettings from '~/badges/components/badge_settings.vue';
|
||||
import store from '~/badges/store';
|
||||
|
||||
export default kind => {
|
||||
const badgeSettingsElement = document.getElementById('badge-settings');
|
||||
|
||||
store.dispatch('loadBadges', {
|
||||
kind,
|
||||
apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
|
||||
docsUrl: badgeSettingsElement.dataset.docsUrl,
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: badgeSettingsElement,
|
||||
store,
|
||||
components: {
|
||||
BadgeSettings,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(BadgeSettings);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
.table-section {
|
||||
white-space: nowrap;
|
||||
|
||||
$section-widths: 10 15 20 25 30 40 100;
|
||||
$section-widths: 10 15 20 25 30 40 50 100;
|
||||
@each $width in $section-widths {
|
||||
&.section-#{$width} {
|
||||
flex: 0 0 #{$width + '%'};
|
||||
|
|
|
|||
|
|
@ -1143,3 +1143,11 @@ pre.light-well {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.project-badge {
|
||||
opacity: 0.9;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
module Groups
|
||||
module Settings
|
||||
class BadgesController < Groups::ApplicationController
|
||||
include GrapeRouteHelpers::NamedRouteMatcher
|
||||
|
||||
before_action :authorize_admin_group!
|
||||
|
||||
def index
|
||||
@badge_api_endpoint = api_v4_groups_badges_path(id: @group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
module Projects
|
||||
module Settings
|
||||
class BadgesController < Projects::ApplicationController
|
||||
include GrapeRouteHelpers::NamedRouteMatcher
|
||||
|
||||
before_action :authorize_admin_project!
|
||||
|
||||
def index
|
||||
@badge_api_endpoint = api_v4_projects_badges_path(id: @project.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
module GroupsHelper
|
||||
def group_nav_link_paths
|
||||
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
|
||||
%w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
|
||||
end
|
||||
|
||||
def group_sidebar_links
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
- breadcrumb_title _('Project Badges')
|
||||
- page_title _('Project Badges')
|
||||
|
||||
= render 'shared/badges/badge_settings'
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
%span.nav-item-name
|
||||
Settings
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
|
||||
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to edit_group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Settings') }
|
||||
|
|
@ -122,6 +122,12 @@
|
|||
%span
|
||||
General
|
||||
|
||||
= nav_link(controller: :badges) do
|
||||
= link_to group_settings_badges_path(@group), title: _('Project Badges') do
|
||||
%span
|
||||
= _('Project Badges')
|
||||
|
||||
|
||||
= nav_link(path: 'groups#projects') do
|
||||
= link_to projects_group_path(@group), title: 'Projects' do
|
||||
%span
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@
|
|||
#{ _('Snippets') }
|
||||
|
||||
- if project_nav_tab? :settings
|
||||
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
|
||||
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
|
||||
= link_to edit_project_path(@project), class: 'shortcuts-tree' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('settings')
|
||||
|
|
@ -268,7 +268,7 @@
|
|||
%ul.sidebar-sub-level-items
|
||||
- can_edit = can?(current_user, :admin_project, @project)
|
||||
- if can_edit
|
||||
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do
|
||||
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to edit_project_path(@project) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Settings') }
|
||||
|
|
@ -281,6 +281,11 @@
|
|||
= link_to project_project_members_path(@project), title: 'Members' do
|
||||
%span
|
||||
Members
|
||||
- if can_edit
|
||||
= nav_link(controller: :badges) do
|
||||
= link_to project_settings_badges_path(@project), title: _('Badges') do
|
||||
%span
|
||||
= _('Badges')
|
||||
- if can_edit
|
||||
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
|
||||
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
|
||||
|
|
|
|||
|
|
@ -23,11 +23,14 @@
|
|||
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
|
||||
= deleted_message % { project_name: fork_source_name(@project) }
|
||||
|
||||
.project-badges
|
||||
.project-badges.prepend-top-default.append-bottom-default
|
||||
- @project.badges.each do |badge|
|
||||
- badge_link_url = badge.rendered_link_url(@project)
|
||||
%a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
|
||||
%img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
|
||||
%a.append-right-8{ href: badge.rendered_link_url(@project),
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer' }>
|
||||
%img.project-badge{ src: badge.rendered_image_url(@project),
|
||||
'aria-hidden': true,
|
||||
alt: '' }>
|
||||
|
||||
.project-repo-buttons
|
||||
.count-buttons
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
- breadcrumb_title _('Badges')
|
||||
- page_title _('Badges')
|
||||
|
||||
= render 'shared/badges/badge_settings'
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#badge-settings{ data: { api_endpoint_url: @badge_api_endpoint,
|
||||
docs_url: help_page_path('user/project/badges')} }
|
||||
.text-center.prepend-top-default
|
||||
= icon('spinner spin 2x')
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Projects and groups badges settings UI
|
||||
merge_request: 17114
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -39,7 +39,7 @@ module.exports = function(config) {
|
|||
frameworks: ['jasmine'],
|
||||
files: [
|
||||
{ pattern: 'spec/javascripts/test_bundle.js', watched: false },
|
||||
{ pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
|
||||
{ pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw|.png)', included: false },
|
||||
],
|
||||
preprocessors: {
|
||||
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
|
||||
namespace :settings do
|
||||
resource :ci_cd, only: [:show], controller: 'ci_cd'
|
||||
resources :badges, only: [:index]
|
||||
end
|
||||
|
||||
resource :variables, only: [:show, :update]
|
||||
|
|
|
|||
|
|
@ -435,6 +435,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
resource :repository, only: [:show], controller: :repository do
|
||||
post :create_deploy_token, path: 'deploy_token/create'
|
||||
end
|
||||
resources :badges, only: [:index]
|
||||
end
|
||||
|
||||
# Since both wiki and repository routing contains wildcard characters
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ module API
|
|||
end
|
||||
|
||||
destroy_conditionally!(badge)
|
||||
body false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Group Badges' do
|
||||
include WaitForRequests
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'}
|
||||
let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'}
|
||||
let!(:badge_1) { create(:group_badge, group: group) }
|
||||
let!(:badge_2) { create(:group_badge, group: group) }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
|
||||
visit(group_settings_badges_path(group))
|
||||
end
|
||||
|
||||
it 'shows a list of badges', :js do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
expect(rows[0]).to have_content badge_1.link_url
|
||||
expect(rows[1]).to have_content badge_2.link_url
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding a badge', :js do
|
||||
it 'user can preview a badge' do
|
||||
page.within '.badge-settings form' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
within '#badge-preview' do
|
||||
expect(find('a')[:href]).to eq badge_link_url
|
||||
expect(find('a img')[:src]).to eq badge_image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
page.within '.badge-settings' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
|
||||
click_button 'Add badge'
|
||||
wait_for_requests
|
||||
|
||||
within '.panel-body' do
|
||||
expect(find('a')[:href]).to eq badge_link_url
|
||||
expect(find('a img')[:src]).to eq badge_image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'editing a badge', :js do
|
||||
it 'form is shown when clicking edit button in list' do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
rows[1].find('[aria-label="Edit"]').click
|
||||
|
||||
within 'form' do
|
||||
expect(find('#badge-link-url').value).to eq badge_2.link_url
|
||||
expect(find('#badge-image-url').value).to eq badge_2.image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates a badge when submitting the edit form' do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
rows[1].find('[aria-label="Edit"]').click
|
||||
within 'form' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
expect(rows[1]).to have_content badge_link_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'deleting a badge', :js do
|
||||
def click_delete_button(badge_row)
|
||||
badge_row.find('[aria-label="Delete"]').click
|
||||
end
|
||||
|
||||
it 'shows a modal when deleting a badge' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
|
||||
click_delete_button(rows[1])
|
||||
|
||||
expect(find('.modal .modal-title')).to have_content 'Delete badge?'
|
||||
end
|
||||
|
||||
it 'deletes a badge when confirming the modal' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
click_delete_button(rows[1])
|
||||
|
||||
find('.modal .btn-danger').click
|
||||
wait_for_requests
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 1
|
||||
expect(rows[0]).to have_content badge_1.link_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Project Badges' do
|
||||
include WaitForRequests
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, namespace: group) }
|
||||
let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'}
|
||||
let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'}
|
||||
let!(:project_badge) { create(:project_badge, project: project) }
|
||||
let!(:group_badge) { create(:group_badge, group: group) }
|
||||
|
||||
before do
|
||||
group.add_master(user)
|
||||
sign_in(user)
|
||||
|
||||
visit(project_settings_badges_path(project))
|
||||
end
|
||||
|
||||
it 'shows a list of badges', :js do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
expect(rows[0]).to have_content group_badge.link_url
|
||||
expect(rows[1]).to have_content project_badge.link_url
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding a badge', :js do
|
||||
it 'user can preview a badge' do
|
||||
page.within '.badge-settings form' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
within '#badge-preview' do
|
||||
expect(find('a')[:href]).to eq badge_link_url
|
||||
expect(find('a img')[:src]).to eq badge_image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
page.within '.badge-settings' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
|
||||
click_button 'Add badge'
|
||||
wait_for_requests
|
||||
|
||||
within '.panel-body' do
|
||||
expect(find('a')[:href]).to eq badge_link_url
|
||||
expect(find('a img')[:src]).to eq badge_image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'editing a badge', :js do
|
||||
it 'form is shown when clicking edit button in list' do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
rows[1].find('[aria-label="Edit"]').click
|
||||
|
||||
within 'form' do
|
||||
expect(find('#badge-link-url').value).to eq project_badge.link_url
|
||||
expect(find('#badge-image-url').value).to eq project_badge.image_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates a badge when submitting the edit form' do
|
||||
page.within '.badge-settings' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
rows[1].find('[aria-label="Edit"]').click
|
||||
within 'form' do
|
||||
fill_in 'badge-link-url', with: badge_link_url
|
||||
fill_in 'badge-image-url', with: badge_image_url
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
expect(rows[1]).to have_content badge_link_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'deleting a badge', :js do
|
||||
def click_delete_button(badge_row)
|
||||
badge_row.find('[aria-label="Delete"]').click
|
||||
end
|
||||
|
||||
it 'shows a modal when deleting a badge' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
|
||||
click_delete_button(rows[1])
|
||||
|
||||
expect(find('.modal .modal-title')).to have_content 'Delete badge?'
|
||||
end
|
||||
|
||||
it 'deletes a badge when confirming the modal' do
|
||||
wait_for_requests
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 2
|
||||
click_delete_button(rows[1])
|
||||
|
||||
find('.modal .btn-danger').click
|
||||
wait_for_requests
|
||||
|
||||
rows = all('.panel-body > div')
|
||||
expect(rows.length).to eq 1
|
||||
expect(rows[0]).to have_content group_badge.link_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/badges/store';
|
||||
import BadgeForm from '~/badges/components/badge_form.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
describe('BadgeForm component', () => {
|
||||
const Component = Vue.extend(BadgeForm);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div id="dummy-element"></div>
|
||||
`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
props: {
|
||||
isEditing: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('onCancel', () => {
|
||||
it('calls stopEditing', () => {
|
||||
spyOn(vm, 'stopEditing');
|
||||
|
||||
vm.onCancel();
|
||||
|
||||
expect(vm.stopEditing).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit', () => {
|
||||
describe('if isEditing is true', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(vm, 'saveBadge').and.returnValue(Promise.resolve());
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: false,
|
||||
badgeInEditForm: createDummyBadge(),
|
||||
});
|
||||
vm.isEditing = true;
|
||||
});
|
||||
|
||||
it('returns immediately if imageUrl is empty', () => {
|
||||
store.state.badgeInEditForm.imageUrl = '';
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.saveBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns immediately if linkUrl is empty', () => {
|
||||
store.state.badgeInEditForm.linkUrl = '';
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.saveBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns immediately if isSaving is true', () => {
|
||||
store.state.isSaving = true;
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.saveBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls saveBadge', () => {
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.saveBadge).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if isEditing is false', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(vm, 'addBadge').and.returnValue(Promise.resolve());
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: false,
|
||||
badgeInAddForm: createDummyBadge(),
|
||||
});
|
||||
vm.isEditing = false;
|
||||
});
|
||||
|
||||
it('returns immediately if imageUrl is empty', () => {
|
||||
store.state.badgeInAddForm.imageUrl = '';
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.addBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns immediately if linkUrl is empty', () => {
|
||||
store.state.badgeInAddForm.linkUrl = '';
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.addBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns immediately if isSaving is true', () => {
|
||||
store.state.isSaving = true;
|
||||
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.addBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls addBadge', () => {
|
||||
vm.onSubmit();
|
||||
|
||||
expect(vm.addBadge).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if isEditing is false', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
props: {
|
||||
isEditing: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders one button', () => {
|
||||
const buttons = vm.$el.querySelectorAll('.row-content-block button');
|
||||
expect(buttons.length).toBe(1);
|
||||
const buttonAddElement = buttons[0];
|
||||
expect(buttonAddElement).toBeVisible();
|
||||
expect(buttonAddElement).toHaveText('Add badge');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if isEditing is true', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
props: {
|
||||
isEditing: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders two buttons', () => {
|
||||
const buttons = vm.$el.querySelectorAll('.row-content-block button');
|
||||
expect(buttons.length).toBe(2);
|
||||
const buttonSaveElement = buttons[0];
|
||||
expect(buttonSaveElement).toBeVisible();
|
||||
expect(buttonSaveElement).toHaveText('Save changes');
|
||||
const buttonCancelElement = buttons[1];
|
||||
expect(buttonCancelElement).toBeVisible();
|
||||
expect(buttonCancelElement).toHaveText('Cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
|
||||
import store from '~/badges/store';
|
||||
import BadgeListRow from '~/badges/components/badge_list_row.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
describe('BadgeListRow component', () => {
|
||||
const Component = Vue.extend(BadgeListRow);
|
||||
let badge;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div id="delete-badge-modal" class="modal"></div>
|
||||
<div id="dummy-element"></div>
|
||||
`);
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
kind: PROJECT_BADGE,
|
||||
});
|
||||
badge = createDummyBadge();
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
props: { badge },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders the badge', () => {
|
||||
const badgeElement = vm.$el.querySelector('.project-badge');
|
||||
expect(badgeElement).not.toBeNull();
|
||||
expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
|
||||
});
|
||||
|
||||
it('renders the badge link', () => {
|
||||
expect(vm.$el).toContainText(badge.linkUrl);
|
||||
});
|
||||
|
||||
it('renders the badge kind', () => {
|
||||
expect(vm.$el).toContainText('Project Badge');
|
||||
});
|
||||
|
||||
it('shows edit and delete buttons', () => {
|
||||
const buttons = vm.$el.querySelectorAll('.table-button-footer button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
const buttonEditElement = buttons[0];
|
||||
expect(buttonEditElement).toBeVisible();
|
||||
expect(buttonEditElement).toHaveSpriteIcon('pencil');
|
||||
const buttonDeleteElement = buttons[1];
|
||||
expect(buttonDeleteElement).toBeVisible();
|
||||
expect(buttonDeleteElement).toHaveSpriteIcon('remove');
|
||||
});
|
||||
|
||||
it('calls editBadge when clicking then edit button', () => {
|
||||
spyOn(vm, 'editBadge');
|
||||
|
||||
const editButton = vm.$el.querySelector('.table-button-footer button:first-of-type');
|
||||
editButton.click();
|
||||
|
||||
expect(vm.editBadge).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls updateBadgeInModal and shows modal when clicking then delete button', done => {
|
||||
spyOn(vm, 'updateBadgeInModal');
|
||||
$('#delete-badge-modal').on('shown.bs.modal', () => done());
|
||||
|
||||
const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type');
|
||||
deleteButton.click();
|
||||
|
||||
expect(vm.updateBadgeInModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('for a group badge', () => {
|
||||
beforeEach(done => {
|
||||
badge.kind = GROUP_BADGE;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('renders the badge kind', () => {
|
||||
expect(vm.$el).toContainText('Group Badge');
|
||||
});
|
||||
|
||||
it('hides edit and delete buttons', () => {
|
||||
const buttons = vm.$el.querySelectorAll('.table-button-footer button');
|
||||
expect(buttons).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import Vue from 'vue';
|
||||
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
|
||||
import store from '~/badges/store';
|
||||
import BadgeList from '~/badges/components/badge_list.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
describe('BadgeList component', () => {
|
||||
const Component = Vue.extend(BadgeList);
|
||||
const numberOfDummyBadges = 3;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures('<div id="dummy-element"></div>');
|
||||
const badges = [];
|
||||
for (let id = 0; id < numberOfDummyBadges; id += 1) {
|
||||
badges.push({ id, ...createDummyBadge() });
|
||||
}
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badges,
|
||||
kind: PROJECT_BADGE,
|
||||
isLoading: false,
|
||||
});
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders a header with the badge count', () => {
|
||||
const header = vm.$el.querySelector('.panel-heading');
|
||||
expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`));
|
||||
});
|
||||
|
||||
it('renders a row for each badge', () => {
|
||||
const rows = vm.$el.querySelectorAll('.gl-responsive-table-row');
|
||||
expect(rows).toHaveLength(numberOfDummyBadges);
|
||||
});
|
||||
|
||||
it('renders a message if no badges exist', done => {
|
||||
store.state.badges = [];
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.$el).toContainText('This project has no badges');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('shows a loading icon when loading', done => {
|
||||
store.state.isLoading = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const loadingIcon = vm.$el.querySelector('.fa-spinner');
|
||||
expect(loadingIcon).toBeVisible();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('for group badges', () => {
|
||||
beforeEach(done => {
|
||||
store.state.kind = GROUP_BADGE;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('renders a message if no badges exist', done => {
|
||||
store.state.badges = [];
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.$el).toContainText('This group has no badges');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import store from '~/badges/store';
|
||||
import BadgeSettings from '~/badges/components/badge_settings.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
describe('BadgeSettings component', () => {
|
||||
const Component = Vue.extend(BadgeSettings);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div id="dummy-element"></div>
|
||||
<button
|
||||
id="dummy-modal-button"
|
||||
type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#delete-badge-modal"
|
||||
>Show modal</button>
|
||||
`);
|
||||
vm = mountComponentWithStore(Component, {
|
||||
el: '#dummy-element',
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('displays modal if button is clicked', done => {
|
||||
const badge = createDummyBadge();
|
||||
store.state.badgeInModal = badge;
|
||||
const modal = vm.$el.querySelector('#delete-badge-modal');
|
||||
const button = document.getElementById('dummy-modal-button');
|
||||
|
||||
$(modal).on('shown.bs.modal', () => {
|
||||
expect(modal).toContainText('Delete badge?');
|
||||
const badgeElement = modal.querySelector('img.project-badge');
|
||||
expect(badgeElement).not.toBe(null);
|
||||
expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
button.click();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('displays a form to add a badge', () => {
|
||||
const form = vm.$el.querySelector('form:nth-of-type(2)');
|
||||
expect(form).not.toBe(null);
|
||||
const button = form.querySelector('.btn-success');
|
||||
expect(button).not.toBe(null);
|
||||
expect(button).toHaveText(/Add badge/);
|
||||
});
|
||||
|
||||
it('displays badge list', () => {
|
||||
const badgeListElement = vm.$el.querySelector('.panel');
|
||||
expect(badgeListElement).not.toBe(null);
|
||||
expect(badgeListElement).toBeVisible();
|
||||
expect(badgeListElement).toContainText('Your badges');
|
||||
});
|
||||
|
||||
describe('when editing', () => {
|
||||
beforeEach(done => {
|
||||
store.state.isEditing = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('displays a form to edit a badge', () => {
|
||||
const form = vm.$el.querySelector('form:nth-of-type(1)');
|
||||
expect(form).not.toBe(null);
|
||||
const submitButton = form.querySelector('.btn-success');
|
||||
expect(submitButton).not.toBe(null);
|
||||
expect(submitButton).toHaveText(/Save changes/);
|
||||
const cancelButton = form.querySelector('.btn-cancel');
|
||||
expect(cancelButton).not.toBe(null);
|
||||
expect(cancelButton).toHaveText(/Cancel/);
|
||||
});
|
||||
|
||||
it('displays no badge list', () => {
|
||||
const badgeListElement = vm.$el.querySelector('.panel');
|
||||
expect(badgeListElement).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('onSubmitModal', () => {
|
||||
it('triggers ', () => {
|
||||
spyOn(vm, 'deleteBadge').and.callFake(() => Promise.resolve());
|
||||
const modal = vm.$el.querySelector('#delete-badge-modal');
|
||||
const deleteButton = modal.querySelector('.btn-danger');
|
||||
|
||||
deleteButton.click();
|
||||
|
||||
const badge = store.state.badgeInModal;
|
||||
expect(vm.deleteBadge).toHaveBeenCalledWith(badge);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import Vue from 'vue';
|
||||
import Badge from '~/badges/components/badge.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
|
||||
|
||||
describe('Badge component', () => {
|
||||
const Component = Vue.extend(Badge);
|
||||
const dummyProps = {
|
||||
imageUrl: DUMMY_IMAGE_URL,
|
||||
linkUrl: `${TEST_HOST}/badge/link/url`,
|
||||
};
|
||||
let vm;
|
||||
|
||||
const findElements = () => {
|
||||
const buttons = vm.$el.querySelectorAll('button');
|
||||
return {
|
||||
badgeImage: vm.$el.querySelector('img.project-badge'),
|
||||
loadingIcon: vm.$el.querySelector('.fa-spinner'),
|
||||
reloadButton: buttons[buttons.length - 1],
|
||||
};
|
||||
};
|
||||
|
||||
const createComponent = (props, el = null) => {
|
||||
vm = mountComponent(Component, props, el);
|
||||
const { badgeImage } = findElements();
|
||||
return new Promise(resolve => badgeImage.addEventListener('load', resolve)).then(() =>
|
||||
Vue.nextTick(),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('watchers', () => {
|
||||
describe('imageUrl', () => {
|
||||
it('sets isLoading and resets numRetries and hasError', done => {
|
||||
const props = { ...dummyProps };
|
||||
createComponent(props)
|
||||
.then(() => {
|
||||
expect(vm.isLoading).toBe(false);
|
||||
vm.hasError = true;
|
||||
vm.numRetries = 42;
|
||||
|
||||
vm.imageUrl = `${props.imageUrl}#something/else`;
|
||||
|
||||
return Vue.nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(vm.isLoading).toBe(true);
|
||||
expect(vm.numRetries).toBe(0);
|
||||
expect(vm.hasError).toBe(false);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
beforeEach(done => {
|
||||
createComponent({ ...dummyProps })
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('onError resets isLoading and sets hasError', () => {
|
||||
vm.hasError = false;
|
||||
vm.isLoading = true;
|
||||
|
||||
vm.onError();
|
||||
|
||||
expect(vm.hasError).toBe(true);
|
||||
expect(vm.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('onLoad sets isLoading', () => {
|
||||
vm.isLoading = true;
|
||||
|
||||
vm.onLoad();
|
||||
|
||||
expect(vm.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('reloadImage resets isLoading and hasError and increases numRetries', () => {
|
||||
vm.hasError = true;
|
||||
vm.isLoading = false;
|
||||
vm.numRetries = 0;
|
||||
|
||||
vm.reloadImage();
|
||||
|
||||
expect(vm.hasError).toBe(false);
|
||||
expect(vm.isLoading).toBe(true);
|
||||
expect(vm.numRetries).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
beforeEach(done => {
|
||||
setFixtures('<div id="dummy-element"></div>');
|
||||
createComponent({ ...dummyProps }, '#dummy-element')
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('shows a badge image after loading', () => {
|
||||
expect(vm.isLoading).toBe(false);
|
||||
expect(vm.hasError).toBe(false);
|
||||
const { badgeImage, loadingIcon, reloadButton } = findElements();
|
||||
expect(badgeImage).toBeVisible();
|
||||
expect(loadingIcon).toBeHidden();
|
||||
expect(reloadButton).toBeHidden();
|
||||
expect(vm.$el.innerText).toBe('');
|
||||
});
|
||||
|
||||
it('shows a loading icon when loading', done => {
|
||||
vm.isLoading = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const { badgeImage, loadingIcon, reloadButton } = findElements();
|
||||
expect(badgeImage).toBeHidden();
|
||||
expect(loadingIcon).toBeVisible();
|
||||
expect(reloadButton).toBeHidden();
|
||||
expect(vm.$el.innerText).toBe('');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('shows an error and reload button if loading failed', done => {
|
||||
vm.hasError = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const { badgeImage, loadingIcon, reloadButton } = findElements();
|
||||
expect(badgeImage).toBeHidden();
|
||||
expect(loadingIcon).toBeHidden();
|
||||
expect(reloadButton).toBeVisible();
|
||||
expect(reloadButton).toHaveSpriteIcon('retry');
|
||||
expect(vm.$el.innerText.trim()).toBe('No badge image');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { PROJECT_BADGE } from '~/badges/constants';
|
||||
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
|
||||
|
||||
export const createDummyBadge = () => {
|
||||
const id = Math.floor(1000 * Math.random());
|
||||
return {
|
||||
id,
|
||||
imageUrl: `${TEST_HOST}/badges/${id}/image/url`,
|
||||
isDeleting: false,
|
||||
linkUrl: `${TEST_HOST}/badges/${id}/link/url`,
|
||||
kind: PROJECT_BADGE,
|
||||
renderedImageUrl: `${DUMMY_IMAGE_URL}?id=${id}`,
|
||||
renderedLinkUrl: `${TEST_HOST}/badges/${id}/rendered/link/url`,
|
||||
};
|
||||
};
|
||||
|
||||
export const createDummyBadgeResponse = () => ({
|
||||
image_url: `${TEST_HOST}/badge/image/url`,
|
||||
link_url: `${TEST_HOST}/badge/link/url`,
|
||||
kind: PROJECT_BADGE,
|
||||
rendered_image_url: DUMMY_IMAGE_URL,
|
||||
rendered_link_url: `${TEST_HOST}/rendered/badge/link/url`,
|
||||
});
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import actions, { transformBackendBadge } from '~/badges/store/actions';
|
||||
import mutationTypes from '~/badges/store/mutation_types';
|
||||
import createState from '~/badges/store/state';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import testAction from 'spec/helpers/vuex_action_helper';
|
||||
import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge';
|
||||
|
||||
describe('Badges store actions', () => {
|
||||
const dummyEndpointUrl = `${TEST_HOST}/badges/endpoint`;
|
||||
const dummyBadges = [{ ...createDummyBadge(), id: 5 }, { ...createDummyBadge(), id: 6 }];
|
||||
|
||||
let axiosMock;
|
||||
let badgeId;
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
state = {
|
||||
...createState(),
|
||||
apiEndpointUrl: dummyEndpointUrl,
|
||||
badges: dummyBadges,
|
||||
};
|
||||
badgeId = state.badges[0].id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
describe('requestNewBadge', () => {
|
||||
it('commits REQUEST_NEW_BADGE', done => {
|
||||
testAction(
|
||||
actions.requestNewBadge,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.REQUEST_NEW_BADGE }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveNewBadge', () => {
|
||||
it('commits RECEIVE_NEW_BADGE', done => {
|
||||
const newBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.receiveNewBadge,
|
||||
newBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_NEW_BADGE, payload: newBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveNewBadgeError', () => {
|
||||
it('commits RECEIVE_NEW_BADGE_ERROR', done => {
|
||||
testAction(
|
||||
actions.receiveNewBadgeError,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_NEW_BADGE_ERROR }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addBadge', () => {
|
||||
let badgeInAddForm;
|
||||
let dispatch;
|
||||
let endpointMock;
|
||||
|
||||
beforeEach(() => {
|
||||
endpointMock = axiosMock.onPost(dummyEndpointUrl);
|
||||
dispatch = jasmine.createSpy('dispatch');
|
||||
badgeInAddForm = createDummyBadge();
|
||||
state = {
|
||||
...state,
|
||||
badgeInAddForm,
|
||||
};
|
||||
});
|
||||
|
||||
it('dispatches requestNewBadge and receiveNewBadge for successful response', done => {
|
||||
const dummyResponse = createDummyBadgeResponse();
|
||||
|
||||
endpointMock.replyOnce(req => {
|
||||
expect(req.data).toBe(
|
||||
JSON.stringify({
|
||||
image_url: badgeInAddForm.imageUrl,
|
||||
link_url: badgeInAddForm.linkUrl,
|
||||
}),
|
||||
);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [200, dummyResponse];
|
||||
});
|
||||
|
||||
const dummyBadge = transformBackendBadge(dummyResponse);
|
||||
actions
|
||||
.addBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadge', dummyBadge]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestNewBadge and receiveNewBadgeError for error response', done => {
|
||||
endpointMock.replyOnce(req => {
|
||||
expect(req.data).toBe(
|
||||
JSON.stringify({
|
||||
image_url: badgeInAddForm.imageUrl,
|
||||
link_url: badgeInAddForm.linkUrl,
|
||||
}),
|
||||
);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [500, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.addBadge({ state, dispatch })
|
||||
.then(() => done.fail('Expected Ajax call to fail!'))
|
||||
.catch(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadgeError']]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDeleteBadge', () => {
|
||||
it('commits REQUEST_DELETE_BADGE', done => {
|
||||
testAction(
|
||||
actions.requestDeleteBadge,
|
||||
badgeId,
|
||||
state,
|
||||
[{ type: mutationTypes.REQUEST_DELETE_BADGE, payload: badgeId }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDeleteBadge', () => {
|
||||
it('commits RECEIVE_DELETE_BADGE', done => {
|
||||
testAction(
|
||||
actions.receiveDeleteBadge,
|
||||
badgeId,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_DELETE_BADGE, payload: badgeId }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDeleteBadgeError', () => {
|
||||
it('commits RECEIVE_DELETE_BADGE_ERROR', done => {
|
||||
testAction(
|
||||
actions.receiveDeleteBadgeError,
|
||||
badgeId,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_DELETE_BADGE_ERROR, payload: badgeId }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBadge', () => {
|
||||
let dispatch;
|
||||
let endpointMock;
|
||||
|
||||
beforeEach(() => {
|
||||
endpointMock = axiosMock.onDelete(`${dummyEndpointUrl}/${badgeId}`);
|
||||
dispatch = jasmine.createSpy('dispatch');
|
||||
});
|
||||
|
||||
it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', done => {
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]);
|
||||
dispatch.calls.reset();
|
||||
return [200, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.deleteBadge({ state, dispatch }, { id: badgeId })
|
||||
.then(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadge', badgeId]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', done => {
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]);
|
||||
dispatch.calls.reset();
|
||||
return [500, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.deleteBadge({ state, dispatch }, { id: badgeId })
|
||||
.then(() => done.fail('Expected Ajax call to fail!'))
|
||||
.catch(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadgeError', badgeId]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editBadge', () => {
|
||||
it('commits START_EDITING', done => {
|
||||
const dummyBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.editBadge,
|
||||
dummyBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.START_EDITING, payload: dummyBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestLoadBadges', () => {
|
||||
it('commits REQUEST_LOAD_BADGES', done => {
|
||||
const dummyData = 'this is not real data';
|
||||
testAction(
|
||||
actions.requestLoadBadges,
|
||||
dummyData,
|
||||
state,
|
||||
[{ type: mutationTypes.REQUEST_LOAD_BADGES, payload: dummyData }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveLoadBadges', () => {
|
||||
it('commits RECEIVE_LOAD_BADGES', done => {
|
||||
const badges = dummyBadges;
|
||||
testAction(
|
||||
actions.receiveLoadBadges,
|
||||
badges,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_LOAD_BADGES, payload: badges }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveLoadBadgesError', () => {
|
||||
it('commits RECEIVE_LOAD_BADGES_ERROR', done => {
|
||||
testAction(
|
||||
actions.receiveLoadBadgesError,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_LOAD_BADGES_ERROR }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadBadges', () => {
|
||||
let dispatch;
|
||||
let endpointMock;
|
||||
|
||||
beforeEach(() => {
|
||||
endpointMock = axiosMock.onGet(dummyEndpointUrl);
|
||||
dispatch = jasmine.createSpy('dispatch');
|
||||
});
|
||||
|
||||
it('dispatches requestLoadBadges and receiveLoadBadges for successful response', done => {
|
||||
const dummyData = 'this is just some data';
|
||||
const dummyReponse = [
|
||||
createDummyBadgeResponse(),
|
||||
createDummyBadgeResponse(),
|
||||
createDummyBadgeResponse(),
|
||||
];
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]);
|
||||
dispatch.calls.reset();
|
||||
return [200, dummyReponse];
|
||||
});
|
||||
|
||||
actions
|
||||
.loadBadges({ state, dispatch }, dummyData)
|
||||
.then(() => {
|
||||
const badges = dummyReponse.map(transformBackendBadge);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadges', badges]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', done => {
|
||||
const dummyData = 'this is just some data';
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]);
|
||||
dispatch.calls.reset();
|
||||
return [500, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.loadBadges({ state, dispatch }, dummyData)
|
||||
.then(() => done.fail('Expected Ajax call to fail!'))
|
||||
.catch(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadgesError']]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestRenderedBadge', () => {
|
||||
it('commits REQUEST_RENDERED_BADGE', done => {
|
||||
testAction(
|
||||
actions.requestRenderedBadge,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.REQUEST_RENDERED_BADGE }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveRenderedBadge', () => {
|
||||
it('commits RECEIVE_RENDERED_BADGE', done => {
|
||||
const dummyBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.receiveRenderedBadge,
|
||||
dummyBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_RENDERED_BADGE, payload: dummyBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveRenderedBadgeError', () => {
|
||||
it('commits RECEIVE_RENDERED_BADGE_ERROR', done => {
|
||||
testAction(
|
||||
actions.receiveRenderedBadgeError,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_RENDERED_BADGE_ERROR }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderBadge', () => {
|
||||
let dispatch;
|
||||
let endpointMock;
|
||||
let badgeInForm;
|
||||
|
||||
beforeEach(() => {
|
||||
badgeInForm = createDummyBadge();
|
||||
state = {
|
||||
...state,
|
||||
badgeInAddForm: badgeInForm,
|
||||
};
|
||||
const urlParameters = [
|
||||
`link_url=${encodeURIComponent(badgeInForm.linkUrl)}`,
|
||||
`image_url=${encodeURIComponent(badgeInForm.imageUrl)}`,
|
||||
].join('&');
|
||||
endpointMock = axiosMock.onGet(`${dummyEndpointUrl}/render?${urlParameters}`);
|
||||
dispatch = jasmine.createSpy('dispatch');
|
||||
});
|
||||
|
||||
it('returns immediately if imageUrl is empty', done => {
|
||||
spyOn(axios, 'get');
|
||||
badgeInForm.imageUrl = '';
|
||||
|
||||
actions
|
||||
.renderBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('returns immediately if linkUrl is empty', done => {
|
||||
spyOn(axios, 'get');
|
||||
badgeInForm.linkUrl = '';
|
||||
|
||||
actions
|
||||
.renderBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('escapes user input', done => {
|
||||
spyOn(axios, 'get').and.callFake(() => Promise.resolve({ data: createDummyBadgeResponse() }));
|
||||
badgeInForm.imageUrl = '&make-sandwhich=true';
|
||||
badgeInForm.linkUrl = '<script>I am dangerous!</script>';
|
||||
|
||||
actions
|
||||
.renderBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
expect(axios.get.calls.count()).toBe(1);
|
||||
const url = axios.get.calls.argsFor(0)[0];
|
||||
expect(url).toMatch(`^${dummyEndpointUrl}/render?`);
|
||||
expect(url).toMatch('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&');
|
||||
expect(url).toMatch('&image_url=%26make-sandwhich%3Dtrue$');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', done => {
|
||||
const dummyReponse = createDummyBadgeResponse();
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [200, dummyReponse];
|
||||
});
|
||||
|
||||
actions
|
||||
.renderBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
const renderedBadge = transformBackendBadge(dummyReponse);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadge', renderedBadge]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', done => {
|
||||
endpointMock.replyOnce(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [500, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.renderBadge({ state, dispatch })
|
||||
.then(() => done.fail('Expected Ajax call to fail!'))
|
||||
.catch(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadgeError']]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestUpdatedBadge', () => {
|
||||
it('commits REQUEST_UPDATED_BADGE', done => {
|
||||
testAction(
|
||||
actions.requestUpdatedBadge,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.REQUEST_UPDATED_BADGE }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveUpdatedBadge', () => {
|
||||
it('commits RECEIVE_UPDATED_BADGE', done => {
|
||||
const updatedBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.receiveUpdatedBadge,
|
||||
updatedBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_UPDATED_BADGE, payload: updatedBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveUpdatedBadgeError', () => {
|
||||
it('commits RECEIVE_UPDATED_BADGE_ERROR', done => {
|
||||
testAction(
|
||||
actions.receiveUpdatedBadgeError,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.RECEIVE_UPDATED_BADGE_ERROR }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveBadge', () => {
|
||||
let badgeInEditForm;
|
||||
let dispatch;
|
||||
let endpointMock;
|
||||
|
||||
beforeEach(() => {
|
||||
badgeInEditForm = createDummyBadge();
|
||||
state = {
|
||||
...state,
|
||||
badgeInEditForm,
|
||||
};
|
||||
endpointMock = axiosMock.onPut(`${dummyEndpointUrl}/${badgeInEditForm.id}`);
|
||||
dispatch = jasmine.createSpy('dispatch');
|
||||
});
|
||||
|
||||
it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', done => {
|
||||
const dummyResponse = createDummyBadgeResponse();
|
||||
|
||||
endpointMock.replyOnce(req => {
|
||||
expect(req.data).toBe(
|
||||
JSON.stringify({
|
||||
image_url: badgeInEditForm.imageUrl,
|
||||
link_url: badgeInEditForm.linkUrl,
|
||||
}),
|
||||
);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [200, dummyResponse];
|
||||
});
|
||||
|
||||
const updatedBadge = transformBackendBadge(dummyResponse);
|
||||
actions
|
||||
.saveBadge({ state, dispatch })
|
||||
.then(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadge', updatedBadge]]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('dispatches requestUpdatedBadge and receiveUpdatedBadgeError for error response', done => {
|
||||
endpointMock.replyOnce(req => {
|
||||
expect(req.data).toBe(
|
||||
JSON.stringify({
|
||||
image_url: badgeInEditForm.imageUrl,
|
||||
link_url: badgeInEditForm.linkUrl,
|
||||
}),
|
||||
);
|
||||
expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]);
|
||||
dispatch.calls.reset();
|
||||
return [500, ''];
|
||||
});
|
||||
|
||||
actions
|
||||
.saveBadge({ state, dispatch })
|
||||
.then(() => done.fail('Expected Ajax call to fail!'))
|
||||
.catch(() => {
|
||||
expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadgeError']]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopEditing', () => {
|
||||
it('commits STOP_EDITING', done => {
|
||||
testAction(
|
||||
actions.stopEditing,
|
||||
null,
|
||||
state,
|
||||
[{ type: mutationTypes.STOP_EDITING }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBadgeInForm', () => {
|
||||
it('commits UPDATE_BADGE_IN_FORM', done => {
|
||||
const dummyBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.updateBadgeInForm,
|
||||
dummyBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.UPDATE_BADGE_IN_FORM, payload: dummyBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
describe('updateBadgeInModal', () => {
|
||||
it('commits UPDATE_BADGE_IN_MODAL', done => {
|
||||
const dummyBadge = createDummyBadge();
|
||||
testAction(
|
||||
actions.updateBadgeInModal,
|
||||
dummyBadge,
|
||||
state,
|
||||
[{ type: mutationTypes.UPDATE_BADGE_IN_MODAL, payload: dummyBadge }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
|
||||
import store from '~/badges/store';
|
||||
import types from '~/badges/store/mutation_types';
|
||||
import createState from '~/badges/store/state';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
describe('Badges store mutations', () => {
|
||||
let dummyBadge;
|
||||
|
||||
beforeEach(() => {
|
||||
dummyBadge = createDummyBadge();
|
||||
store.replaceState(createState());
|
||||
});
|
||||
|
||||
describe('RECEIVE_DELETE_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
const badges = [
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1 },
|
||||
dummyBadge,
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1 },
|
||||
];
|
||||
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badges,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes deleted badge', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
|
||||
store.commit(types.RECEIVE_DELETE_BADGE, dummyBadge.id);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount - 1);
|
||||
expect(store.state.badges.indexOf(dummyBadge)).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_DELETE_BADGE_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
const badges = [
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false },
|
||||
{ ...dummyBadge, isDeleting: true },
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true },
|
||||
];
|
||||
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badges,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isDeleting to false', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
|
||||
store.commit(types.RECEIVE_DELETE_BADGE_ERROR, dummyBadge.id);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount);
|
||||
expect(store.state.badges[0].isDeleting).toBe(false);
|
||||
expect(store.state.badges[1].isDeleting).toBe(false);
|
||||
expect(store.state.badges[2].isDeleting).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_LOAD_BADGES', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isLoading: 'not false',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets badges and isLoading to false', () => {
|
||||
const badges = [createDummyBadge()];
|
||||
store.commit(types.RECEIVE_LOAD_BADGES, badges);
|
||||
|
||||
expect(store.state.isLoading).toBe(false);
|
||||
expect(store.state.badges).toBe(badges);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_LOAD_BADGES_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isLoading: 'not false',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isLoading to false', () => {
|
||||
store.commit(types.RECEIVE_LOAD_BADGES_ERROR);
|
||||
|
||||
expect(store.state.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_NEW_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
const badges = [
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1, kind: GROUP_BADGE },
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1, kind: GROUP_BADGE },
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1, kind: PROJECT_BADGE },
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1, kind: PROJECT_BADGE },
|
||||
];
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInAddForm: createDummyBadge(),
|
||||
badges,
|
||||
isSaving: 'dummy value',
|
||||
renderedBadge: createDummyBadge(),
|
||||
});
|
||||
});
|
||||
|
||||
it('resets the add form', () => {
|
||||
store.commit(types.RECEIVE_NEW_BADGE, dummyBadge);
|
||||
|
||||
expect(store.state.badgeInAddForm).toBe(null);
|
||||
expect(store.state.isSaving).toBe(false);
|
||||
expect(store.state.renderedBadge).toBe(null);
|
||||
});
|
||||
|
||||
it('inserts group badge at correct position', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
dummyBadge = { ...dummyBadge, kind: GROUP_BADGE };
|
||||
|
||||
store.commit(types.RECEIVE_NEW_BADGE, dummyBadge);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount + 1);
|
||||
expect(store.state.badges.indexOf(dummyBadge)).toBe(1);
|
||||
});
|
||||
|
||||
it('inserts project badge at correct position', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
dummyBadge = { ...dummyBadge, kind: PROJECT_BADGE };
|
||||
|
||||
store.commit(types.RECEIVE_NEW_BADGE, dummyBadge);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount + 1);
|
||||
expect(store.state.badges.indexOf(dummyBadge)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_NEW_BADGE_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isSaving to false', () => {
|
||||
store.commit(types.RECEIVE_NEW_BADGE_ERROR);
|
||||
|
||||
expect(store.state.isSaving).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_RENDERED_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isRendering: 'dummy value',
|
||||
renderedBadge: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets renderedBadge', () => {
|
||||
store.commit(types.RECEIVE_RENDERED_BADGE, dummyBadge);
|
||||
|
||||
expect(store.state.isRendering).toBe(false);
|
||||
expect(store.state.renderedBadge).toBe(dummyBadge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_RENDERED_BADGE_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isRendering: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isRendering to false', () => {
|
||||
store.commit(types.RECEIVE_RENDERED_BADGE_ERROR);
|
||||
|
||||
expect(store.state.isRendering).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_UPDATED_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
const badges = [
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1 },
|
||||
dummyBadge,
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1 },
|
||||
];
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInEditForm: createDummyBadge(),
|
||||
badges,
|
||||
isEditing: 'dummy value',
|
||||
isSaving: 'dummy value',
|
||||
renderedBadge: createDummyBadge(),
|
||||
});
|
||||
});
|
||||
|
||||
it('resets the edit form', () => {
|
||||
store.commit(types.RECEIVE_UPDATED_BADGE, dummyBadge);
|
||||
|
||||
expect(store.state.badgeInAddForm).toBe(null);
|
||||
expect(store.state.isSaving).toBe(false);
|
||||
expect(store.state.renderedBadge).toBe(null);
|
||||
});
|
||||
|
||||
it('replaces the updated badge', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
const badgeIndex = store.state.badges.indexOf(dummyBadge);
|
||||
const newBadge = { id: dummyBadge.id, dummy: 'value' };
|
||||
|
||||
store.commit(types.RECEIVE_UPDATED_BADGE, newBadge);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount);
|
||||
expect(store.state.badges[badgeIndex]).toBe(newBadge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_UPDATED_BADGE_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isSaving to false', () => {
|
||||
store.commit(types.RECEIVE_NEW_BADGE_ERROR);
|
||||
|
||||
expect(store.state.isSaving).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_DELETE_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
const badges = [
|
||||
{ ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false },
|
||||
{ ...dummyBadge, isDeleting: false },
|
||||
{ ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true },
|
||||
];
|
||||
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badges,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isDeleting to true', () => {
|
||||
const badgeCount = store.state.badges.length;
|
||||
|
||||
store.commit(types.REQUEST_DELETE_BADGE, dummyBadge.id);
|
||||
|
||||
expect(store.state.badges.length).toBe(badgeCount);
|
||||
expect(store.state.badges[0].isDeleting).toBe(false);
|
||||
expect(store.state.badges[1].isDeleting).toBe(true);
|
||||
expect(store.state.badges[2].isDeleting).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_LOAD_BADGES', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
apiEndpointUrl: 'some endpoint',
|
||||
docsUrl: 'some url',
|
||||
isLoading: 'dummy value',
|
||||
kind: 'some kind',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isLoading to true and initializes the store', () => {
|
||||
const dummyData = {
|
||||
apiEndpointUrl: 'dummy endpoint',
|
||||
docsUrl: 'dummy url',
|
||||
kind: 'dummy kind',
|
||||
};
|
||||
|
||||
store.commit(types.REQUEST_LOAD_BADGES, dummyData);
|
||||
|
||||
expect(store.state.isLoading).toBe(true);
|
||||
expect(store.state.apiEndpointUrl).toBe(dummyData.apiEndpointUrl);
|
||||
expect(store.state.docsUrl).toBe(dummyData.docsUrl);
|
||||
expect(store.state.kind).toBe(dummyData.kind);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_NEW_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isSaving to true', () => {
|
||||
store.commit(types.REQUEST_NEW_BADGE);
|
||||
|
||||
expect(store.state.isSaving).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_RENDERED_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isRendering: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isRendering to true', () => {
|
||||
store.commit(types.REQUEST_RENDERED_BADGE);
|
||||
|
||||
expect(store.state.isRendering).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_UPDATED_BADGE', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
isSaving: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isSaving to true', () => {
|
||||
store.commit(types.REQUEST_NEW_BADGE);
|
||||
|
||||
expect(store.state.isSaving).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('START_EDITING', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInEditForm: 'dummy value',
|
||||
isEditing: 'dummy value',
|
||||
renderedBadge: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes the edit form', () => {
|
||||
store.commit(types.START_EDITING, dummyBadge);
|
||||
|
||||
expect(store.state.isEditing).toBe(true);
|
||||
expect(store.state.badgeInEditForm).toEqual(dummyBadge);
|
||||
expect(store.state.renderedBadge).toEqual(dummyBadge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('STOP_EDITING', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInEditForm: 'dummy value',
|
||||
isEditing: 'dummy value',
|
||||
renderedBadge: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('resets the edit form', () => {
|
||||
store.commit(types.STOP_EDITING);
|
||||
|
||||
expect(store.state.isEditing).toBe(false);
|
||||
expect(store.state.badgeInEditForm).toBe(null);
|
||||
expect(store.state.renderedBadge).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_BADGE_IN_FORM', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInAddForm: 'dummy value',
|
||||
badgeInEditForm: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets badgeInEditForm if isEditing is true', () => {
|
||||
store.state.isEditing = true;
|
||||
|
||||
store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge);
|
||||
|
||||
expect(store.state.badgeInEditForm).toBe(dummyBadge);
|
||||
});
|
||||
|
||||
it('sets badgeInAddForm if isEditing is false', () => {
|
||||
store.state.isEditing = false;
|
||||
|
||||
store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge);
|
||||
|
||||
expect(store.state.badgeInAddForm).toBe(dummyBadge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_BADGE_IN_MODAL', () => {
|
||||
beforeEach(() => {
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
badgeInModal: 'dummy value',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets badgeInModal', () => {
|
||||
store.commit(types.UPDATE_BADGE_IN_MODAL, dummyBadge);
|
||||
|
||||
expect(store.state.badgeInModal).toBe(dummyBadge);
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
|
|
@ -3,6 +3,12 @@ export const createComponentWithStore = (Component, store, propsData = {}) => ne
|
|||
propsData,
|
||||
});
|
||||
|
||||
export const mountComponentWithStore = (Component, { el, props, store }) =>
|
||||
new Component({
|
||||
store,
|
||||
propsData: props || { },
|
||||
}).$mount(el);
|
||||
|
||||
export default (Component, props = {}, el = null) => new Component({
|
||||
propsData: props,
|
||||
}).$mount(el);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
export default {
|
||||
toHaveSpriteIcon: () => ({
|
||||
compare(element, iconName) {
|
||||
if (!iconName) {
|
||||
throw new Error('toHaveSpriteIcon is missing iconName argument!');
|
||||
}
|
||||
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
throw new Error(`${element} is not a DOM element!`);
|
||||
}
|
||||
|
||||
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
|
||||
const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`));
|
||||
const result = {
|
||||
pass: !!matchingIcon,
|
||||
};
|
||||
|
||||
if (result.pass) {
|
||||
result.message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
|
||||
} else {
|
||||
result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
|
||||
|
||||
const existingIcons = iconReferences.map((reference) => {
|
||||
const iconUrl = reference.getAttribute('xlink:href');
|
||||
return `"${iconUrl.replace(/^.+#/, '')}"`;
|
||||
});
|
||||
if (existingIcons.length > 0) {
|
||||
result.message += ` (only found ${existingIcons.join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
@ -7,6 +7,9 @@ import Vue from 'vue';
|
|||
import VueResource from 'vue-resource';
|
||||
|
||||
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
|
||||
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
|
||||
|
||||
import customMatchers from './matchers';
|
||||
|
||||
const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent);
|
||||
Vue.config.devtools = !isHeadlessChrome;
|
||||
|
|
@ -27,15 +30,17 @@ Vue.config.errorHandler = function (err) {
|
|||
Vue.use(VueResource);
|
||||
|
||||
// enable test fixtures
|
||||
jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures';
|
||||
jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures';
|
||||
jasmine.getFixtures().fixturesPath = FIXTURES_PATH;
|
||||
jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH;
|
||||
|
||||
beforeAll(() => jasmine.addMatchers(customMatchers));
|
||||
|
||||
// globalize common libraries
|
||||
window.$ = window.jQuery = $;
|
||||
|
||||
// stub expected globals
|
||||
window.gl = window.gl || {};
|
||||
window.gl.TEST_HOST = 'http://test.host';
|
||||
window.gl.TEST_HOST = TEST_HOST;
|
||||
window.gon = window.gon || {};
|
||||
window.gon.test_env = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
|
||||
export const TEST_HOST = 'http://test.host';
|
||||
|
||||
export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
|
||||
Loading…
Reference in New Issue