提交 2f47b6d8 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 d15cc268
......@@ -112,9 +112,10 @@
- "Gemfile{,.lock}"
- "Rakefile"
- "config.ru"
# List explicitly all the app/ dirs that aren't backend (i.e. all except app/assets).
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
- "{,ee/}{app/channels,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
- "{,ee/}{bin,cable,config,db,lib}/**/*"
- "{,ee/}spec/**/*.rb"
.db-patterns: &db-patterns
- "{,ee/}{db}/**/*"
......
<script>
import { GlButton } from '@gitlab/ui';
import TagsListRow from './tags_list_row.vue';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
export default {
components: {
GlButton,
TagsListRow,
},
props: {
tags: {
type: Array,
required: false,
default: () => [],
},
isDesktop: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
},
data() {
return {
selectedItems: {},
};
},
computed: {
hasSelectedItems() {
return this.tags.some(tag => this.selectedItems[tag.name]);
},
},
methods: {
updateSelectedItems(name) {
this.$set(this.selectedItems, name, !this.selectedItems[name]);
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
<h5 data-testid="list-title">
{{ $options.i18n.TAGS_LIST_TITLE }}
</h5>
<gl-button
v-if="isDesktop"
:disabled="!hasSelectedItems"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
>
{{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
</gl-button>
</div>
<tags-list-row
v-for="(tag, index) in tags"
:key="tag.path"
:tag="tag"
:index="index"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
</div>
</template>
<script>
import { GlFormCheckbox, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
SHORT_REVISION_LABEL,
CREATED_AT_LABEL,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
} from '../../constants/index';
export default {
components: {
GlSprintf,
GlFormCheckbox,
DeleteButton,
ListItem,
ClipboardButton,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
tag: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
selected: {
type: Boolean,
default: false,
required: false,
},
isDesktop: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
REMOVE_TAG_BUTTON_TITLE,
SHORT_REVISION_LABEL,
CREATED_AT_LABEL,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
},
computed: {
formattedSize() {
return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : '';
},
layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
},
mobileClasses() {
return this.isDesktop ? '' : 'mw-s';
},
},
};
</script>
<template>
<list-item :index="index" :selected="selected">
<template #left-action>
<gl-form-checkbox class="gl-m-0" :checked="selected" @change="$emit('select')" />
</template>
<template #left-primary>
<div class="gl-display-flex gl-align-items-center">
<div
v-gl-tooltip="{ title: tag.name }"
data-testid="name"
class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
:class="mobileClasses"
>
{{ tag.name }}
</div>
<clipboard-button
v-if="tag.location"
:title="tag.location"
:text="tag.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
</template>
<template #left-secondary>
<span data-testid="size">
{{ formattedSize }}
<template v-if="formattedSize && layers"
>&middot;</template
>
{{ layers }}
</span>
</template>
<template #right-primary>
<span data-testid="time">
<gl-sprintf :message="$options.i18n.CREATED_AT_LABEL">
<template #timeInfo>
<time-ago-tooltip :time="tag.created_at" />
</template>
</gl-sprintf>
</span>
</template>
<template #right-secondary>
<span data-testid="short-revision">
<gl-sprintf :message="$options.i18n.SHORT_REVISION_LABEL">
<template #imageId>{{ tag.short_revision }}</template>
</gl-sprintf>
</span>
</template>
<template #right-action>
<delete-button
:disabled="!tag.destroy_path"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="Boolean(tag.destroy_path)"
data-testid="single-delete-button"
@delete="$emit('delete')"
/>
</template>
</list-item>
</template>
<script>
import { GlTable, GlFormCheckbox, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
LIST_KEY_TAG,
LIST_KEY_IMAGE_ID,
LIST_KEY_SIZE,
LIST_KEY_LAST_UPDATED,
LIST_KEY_ACTIONS,
LIST_KEY_CHECKBOX,
LIST_LABEL_TAG,
LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE,
LIST_LABEL_LAST_UPDATED,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
} from '../../constants/index';
export default {
components: {
GlTable,
GlFormCheckbox,
GlButton,
ClipboardButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
tags: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
isDesktop: {
type: Boolean,
required: false,
default: false,
},
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
},
data() {
return {
selectedItems: [],
};
},
computed: {
fields() {
const tagClass = this.isDesktop ? 'w-25' : '';
const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
return [
{ key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
{
key: LIST_KEY_TAG,
label: LIST_LABEL_TAG,
class: `${tagClass} js-tag-column`,
innerClass: tagInnerClass,
},
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
{ key: LIST_KEY_ACTIONS, label: '' },
].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
},
tagsNames() {
return this.tags.map(t => t.name);
},
selectAllChecked() {
return this.selectedItems.length === this.tags.length && this.tags.length > 0;
},
},
watch: {
tagsNames: {
immediate: false,
handler(tagsNames) {
this.selectedItems = this.selectedItems.filter(t => tagsNames.includes(t));
},
},
},
methods: {
formatSize(size) {
return numberToHumanSize(size);
},
layers(layers) {
return layers ? n__('%d layer', '%d layers', layers) : '';
},
onSelectAllChange() {
if (this.selectAllChecked) {
this.selectedItems = [];
} else {
this.selectedItems = this.tags.map(x => x.name);
}
},
updateSelectedItems(name) {
const delIndex = this.selectedItems.findIndex(x => x === name);
if (delIndex > -1) {
this.selectedItems.splice(delIndex, 1);
} else {
this.selectedItems.push(name);
}
},
},
};
</script>
<template>
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty :busy="isLoading">
<template v-if="isDesktop" #head(checkbox)>
<gl-form-checkbox
data-testid="mainCheckbox"
:checked="selectAllChecked"
@change="onSelectAllChange"
/>
</template>
<template #head(actions)>
<span class="gl-display-flex gl-justify-content-end">
<gl-button
v-gl-tooltip
data-testid="bulkDeleteButton"
:disabled="!selectedItems || selectedItems.length === 0"
icon="remove"
variant="danger"
:title="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
:aria-label="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
@click="$emit('delete', selectedItems)"
/>
</span>
</template>
<template #cell(checkbox)="{item}">
<gl-form-checkbox
data-testid="rowCheckbox"
:checked="selectedItems.includes(item.name)"
@change="updateSelectedItems(item.name)"
/>
</template>
<template #cell(name)="{item, field}">
<div data-testid="rowName" :class="[field.innerClass, 'gl-display-flex']">
<span
v-gl-tooltip
data-testid="rowNameText"
:title="item.name"
class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
>
{{ item.name }}
</span>
<clipboard-button
v-if="item.location"
data-testid="rowClipboardButton"
:title="item.location"
:text="item.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
</template>
<template #cell(short_revision)="{value}">
<span data-testid="rowShortRevision">
{{ value }}
</span>
</template>
<template #cell(total_size)="{item}">
<span data-testid="rowSize">
{{ formatSize(item.total_size) }}
<template v-if="item.total_size && item.layers">
&middot;
</template>
{{ layers(item.layers) }}
</span>
</template>
<template #cell(created_at)="{value}">
<span v-gl-tooltip data-testid="rowTime" :title="tooltipTitle(value)">
{{ timeFormatted(value) }}
</span>
</template>
<template #cell(actions)="{item}">
<span class="gl-display-flex gl-justify-content-end">
<gl-button
data-testid="singleDeleteButton"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:aria-label="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:disabled="!item.destroy_path"
variant="danger"
icon="remove"
category="secondary"
@click="$emit('delete', [item.name])"
/>
</span>
</template>
<template #empty>
<slot name="empty"></slot>
</template>
<template #table-busy>
<slot name="loader"></slot>
</template>
</gl-table>
</template>
......@@ -12,12 +12,19 @@ export default {
default: false,
required: false,
},
selected: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
optionalClasses() {
return {
'gl-border-t-solid gl-border-t-1': this.index === 0,
'disabled-content': this.disabled,
'gl-border-gray-200': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
},
......@@ -26,22 +33,36 @@ export default {
<template>
<div
:class="[
'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4',
optionalClasses,
]"
class="gl-display-flex gl-align-items-center gl-border-b-solid gl-border-b-1 gl-py-4 gl-px-2"
:class="optionalClasses"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
<div v-if="$slots['left-action']" class="gl-mr-5 gl-display-none gl-display-sm-block">
<slot name="left-action"></slot>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-black-normal gl-font-weight-bold"
>
<div>
<slot name="left-primary"></slot>
</div>
<div>
<slot name="right-primary"></slot>
</div>
</div>
<div class="gl-font-sm gl-text-gray-500">
<slot name="left-secondary"></slot>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-500"
>
<div>
<slot name="left-secondary"></slot>
</div>
<div>
<slot name="right-secondary"></slot>
</div>
</div>
</div>
<div>
<slot name="right"></slot>
<div v-if="$slots['right-action']" class="gl-ml-5 gl-display-none gl-display-sm-block">
<slot name="right-action"></slot>
</div>
</div>
</template>
......@@ -106,9 +106,8 @@ export default {
</gl-sprintf>
</span>
</template>
<template #right>
<template #right-action>
<delete-button
class="gl-display-none d-sm-block"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
......
......@@ -14,12 +14,13 @@ export const DELETE_TAGS_ERROR_MESSAGE = s__(
export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const SHORT_REVISION_LABEL = s__('ContainerRegistry|Image ID: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
......@@ -36,17 +37,15 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
);
// Parameters
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10;
export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_TAG = 'name';
export const LIST_KEY_IMAGE_ID = 'short_revision';
export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';
export const ALERT_SUCCESS_TAG = 'success_tag';
export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
......
......@@ -6,7 +6,7 @@ import Tracking from '~/tracking';
import DeleteAlert from '../components/details_page/delete_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
import TagsTable from '../components/details_page/tags_table.vue';
import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
......@@ -24,7 +24,7 @@ export default {
DetailsHeader,
GlPagination,
DeleteModal,
TagsTable,
TagsList,
TagsLoader,
EmptyTagsState,
},
......@@ -65,10 +65,8 @@ export default {
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
deleteTags(toBeDeletedList) {
this.itemsToBeDeleted = toBeDeletedList.map(name => ({
...this.tags.find(t => t.name === name),
}));
deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]);
this.track('click_button');
this.$refs.deleteModal.show();
},
......@@ -114,24 +112,21 @@ export default {
</script>
<template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
<div v-gl-resize-observer="handleResize" class="gl-my-3 gl-w-full slide-enter-to-element">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
:is-admin="config.isAdmin"
class="my-2"
class="gl-my-2"
/>
<details-header :image-name="imageName" />
<tags-table :tags="tags" :is-loading="isLoading" :is-desktop="isDesktop" @delete="deleteTags">
<template #empty>
<empty-tags-state :no-containers-image="config.noContainersImage" />
</template>
<template #loader>
<tags-loader v-once />
</template>
</tags-table>
<tags-loader v-if="isLoading" />
<template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
<tags-list v-else :tags="tags" :is-desktop="isDesktop" @delete="deleteTags" />
</template>
<gl-pagination
v-if="!isLoading"
......@@ -140,7 +135,7 @@ export default {
:per-page="tagsPagination.perPage"
:total-items="tagsPagination.total"
align="center"
class="w-100"
class="gl-w-full gl-mt-3"
/>
<delete-modal
......
---
title: Convert the Image tag UI from a table to a list view
merge_request: 35138
author:
type: changed
---
title: Fix approval rule type when project rule has users/groups
merge_request: 34026
author:
type: fixed
......@@ -66,7 +66,7 @@ one major version. For example, it is safe to:
- `9.5.5` -> `9.5.9`
- `8.9.2` -> `8.9.6`
NOTE **Note** Version specific changes in Omnibus GitLab Linux packages can be found in [the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/update/README.html#version-specific-changes).
NOTE: **Note** Version specific changes in Omnibus GitLab Linux packages can be found in [the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/update/README.html#version-specific-changes).
NOTE: **Note:**
Instructions are available for downloading an Omnibus GitLab Linux package locally and [manually installing](https://docs.gitlab.com/omnibus/manual_install.html) it.
......@@ -107,7 +107,7 @@ Please see the table below for some examples:
| Target version | Your version | Recommended upgrade path | Note |
| --------------------- | ------------ | ------------------------ | ---- |
| `13.2.0` | `11.5.0` | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.0` -> `13.2.0` | Four intermediate versions are required: the final 11.11, 12.0, and 12.10 releases, plus 13.0. |
| `13.2.0` | `11.5.0` | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.0` -> `13.2.0` | Four intermediate versions are required: the final `11.11`, `12.0`, and `12.10` releases, plus `13.0`. |
| `13.0.1` | `11.10.8` | `11.10.5` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.1` | Three intermediate versions are required: `11.11`, `12.0`, and `12.10`. |
| `12.10.6` | `11.3.4` | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.10.6` | Two intermediate versions are required: `11.11` and `12.0` |
| `12.9.5.` | `10.4.5` | `10.4.5` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.9.5` | Three intermediate versions are required: `10.8`, `11.11`, and `12.0`, then `12.9.5` |
......
......@@ -6082,9 +6082,6 @@ msgstr ""
msgid "ContainerRegistry|CLI Commands"
msgstr ""
msgid "ContainerRegistry|Compressed Size"
msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
......@@ -6100,6 +6097,9 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
msgid "ContainerRegistry|Delete selected"
msgstr ""
msgid "ContainerRegistry|Docker connection error"
msgstr ""
......@@ -6133,16 +6133,16 @@ msgstr ""
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
msgid "ContainerRegistry|Image ID"
msgid "ContainerRegistry|Image ID: %{imageId}"
msgstr ""
msgid "ContainerRegistry|Image Repositories"
msgstr ""
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgid "ContainerRegistry|Image tags"
msgstr ""
msgid "ContainerRegistry|Last Updated"
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
msgid "ContainerRegistry|Login"
......@@ -6157,6 +6157,9 @@ msgstr ""
msgid "ContainerRegistry|Please contact your administrator."
msgstr ""
msgid "ContainerRegistry|Published %{timeInfo}"
msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
......@@ -6172,9 +6175,6 @@ msgstr ""
msgid "ContainerRegistry|Remove repository"
msgstr ""
msgid "ContainerRegistry|Remove selected tags"
msgstr ""
msgid "ContainerRegistry|Remove tag"
msgid_plural "ContainerRegistry|Remove tags"
msgstr[0] ""
......@@ -6204,9 +6204,6 @@ msgstr ""
msgid "ContainerRegistry|Sorry, your filter produced no results."
msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Groups::ImportsController do
RSpec.describe Groups::ImportsController do
describe 'GET #show' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::Pipelines::TestsController do
RSpec.describe Projects::Pipelines::TestsController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Registrations::ExperienceLevelsController do
RSpec.describe Registrations::ExperienceLevelsController do
let_it_be(:namespace) { create(:group, path: 'group-path' ) }
let_it_be(:user) { create(:user) }
......
......@@ -82,7 +82,7 @@ RSpec.describe 'Container Registry', :js do
expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
first('[data-testid="singleDeleteButton"]').click
first('[data-testid="single-delete-button"]').click
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Import/Export - Group Import', :js do
RSpec.describe 'Import/Export - Group Import', :js do
let_it_be(:user) { create(:user) }
let_it_be(:import_path) { "#{Dir.tmpdir}/group_import_spec" }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Projects > Activity > User sees design Activity', :js do
RSpec.describe 'Projects > Activity > User sees design Activity', :js do
include DesignManagementTestHelpers
let_it_be(:uploader) { create(:user) }
......
......@@ -84,7 +84,7 @@ RSpec.describe 'Container Registry', :js do
expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['1']) { service }
first('[data-testid="singleDeleteButton"]').click
first('[data-testid="single-delete-button"]').click
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
......
import { shallowMount } from '@vue/test-utils';
import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import ListItem from '~/registry/explorer/components/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
} from '~/registry/explorer/constants/index';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { tagsListResponse } from '../../mock_data';
describe('tags list row', () => {
let wrapper;
const [tag] = [...tagsListResponse.data];
const defaultProps = { tag, isDesktop: true, index: 0 };
const findCheckbox = () => wrapper.find(GlFormCheckbox);
const findName = () => wrapper.find('[data-testid="name"]');
const findSize = () => wrapper.find('[data-testid="size"]');
const findTime = () => wrapper.find('[data-testid="time"]');
const findShortRevision = () => wrapper.find('[data-testid="short-revision"]');
const findClipboardButton = () => wrapper.find(ClipboardButton);
const findDeleteButton = () => wrapper.find(DeleteButton);
const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
stubs: {
GlSprintf,
ListItem,
},
propsData,
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('checkbox', () => {
it('exists', () => {
mountComponent();
expect(findCheckbox().exists()).toBe(true);
});
it('is wired to the selected prop', () => {
mountComponent({ ...defaultProps, selected: true });
expect(findCheckbox().attributes('checked')).toBe('true');
});
it('when changed emit a select event', () => {
mountComponent();
findCheckbox().vm.$emit('change');
expect(wrapper.emitted('select')).toEqual([[]]);
});
});
describe('tag name', () => {
it('exists', () => {
mountComponent();
expect(findName().exists()).toBe(true);
});
it('has the correct text', () => {
mountComponent();
expect(findName().text()).toBe(tag.name);
});
it('has a tooltip', () => {
mountComponent();
const tooltip = getBinding(findName().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(tag.name);
});
it('on mobile has mw-s class', () => {
mountComponent({ ...defaultProps, isDesktop: false });
expect(findName().classes('mw-s')).toBe(true);
});
});
describe('clipboard button', () => {
it('exist if tag.location exist', () => {
mountComponent();
expect(findClipboardButton().exists()).toBe(true);
});
it('is hidden if tag does not have a location', () => {
mountComponent({ ...defaultProps, tag: { ...tag, location: null } });
expect(findClipboardButton().exists()).toBe(false);
});
it('has the correct props/attributes', () => {
mountComponent();
expect(findClipboardButton().attributes()).toMatchObject({
text: 'location',
title: 'location',
});
});
});
describe('size', () => {
it('exists', () => {
mountComponent();
expect(findSize().exists()).toBe(true);
});
it('contains the total_size and layers', () => {
mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024 } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB · 10 layers');
});
it('when total_size is missing', () => {
mountComponent();
expect(findSize().text()).toMatchInterpolatedText('10 layers');
});
it('when layers are missing', () => {
mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024, layers: null } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB');
});
it('when there is 1 layer', () => {
mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } });
expect(findSize().text()).toMatchInterpolatedText('1 layer');
});
});
describe('time', () => {
it('exists', () => {
mountComponent();
expect(findTime().exists()).toBe(true);
});
it('has the correct text', () => {
mountComponent();
expect(findTime().text()).toBe('Published');
});
it('contains time_ago_tooltip component', () => {
mountComponent();
expect(findTimeAgoTooltip().exists()).toBe(true);
});
it('pass the correct props to time ago tooltip', () => {
mountComponent();
expect(findTimeAgoTooltip().attributes()).toMatchObject({ time: tag.created_at });
});
});
describe('shortRevision', () => {
it('exists', () => {
mountComponent();
expect(findShortRevision().exists()).toBe(true);
});
it('has the correct text', () => {
mountComponent();
expect(findShortRevision().text()).toMatchInterpolatedText('Image ID: b118ab5b0');
});
});
describe('delete button', () => {
it('exists', () => {
mountComponent();
expect(findDeleteButton().exists()).toBe(true);
});
it('has the correct props/attributes', () => {
mountComponent();
expect(findDeleteButton().attributes()).toMatchObject({
title: REMOVE_TAG_BUTTON_TITLE,
tooltiptitle: REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
tooltipdisabled: 'true',
});
});
it('is disabled when tag has no destroy path', () => {
mountComponent({ ...defaultProps, tag: { ...tag, destroy_path: null } });
expect(findDeleteButton().attributes('disabled')).toBe('true');
});
it('delete event emits delete', () => {
mountComponent();
findDeleteButton().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue';
import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index';
import { tagsListResponse } from '../../mock_data';
describe('Tags List', () => {
let wrapper;
const tags = [...tagsListResponse.data];
const findTagsListRow = () => wrapper.findAll(TagsListRow);
const findDeleteButton = () => wrapper.find(GlButton);
const findListTitle = () => wrapper.find('[data-testid="list-title"]');
const mountComponent = (propsData = { tags, isDesktop: true }) => {
wrapper = shallowMount(component, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('List title', () => {
it('exists', () => {
mountComponent();
expect(findListTitle().exists()).toBe(true);
});
it('has the correct text', () => {
mountComponent();
expect(findListTitle().text()).toBe(TAGS_LIST_TITLE);
});
});
describe('delete button', () => {
it('is not shown on mobile view', () => {
mountComponent({ tags, isDesktop: false });
expect(findDeleteButton().exists()).toBe(false);
});
it('is shown on desktop view', () => {
mountComponent();
expect(findDeleteButton().exists()).toBe(true);
});
it('has the correct text', () => {
mountComponent();
expect(findDeleteButton().text()).toBe(REMOVE_TAGS_BUTTON_TITLE);
});
it('has the correct props', () => {
mountComponent();
expect(findDeleteButton().attributes()).toMatchObject({
category: 'secondary',
variant: 'danger',
});
});
it('is disabled when no item is selected', () => {
mountComponent();
expect(findDeleteButton().attributes('disabled')).toBe('true');
});
it('is enabled when at least one item is selected', async () => {
mountComponent();
findTagsListRow()
.at(0)
.vm.$emit('select');
await wrapper.vm.$nextTick();
expect(findDeleteButton().attributes('disabled')).toBe(undefined);
});
it('click event emits a deleted event with selected items', () => {
mountComponent();
findTagsListRow()
.at(0)
.vm.$emit('select');
findDeleteButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]);
});
});
describe('list rows', () => {
it('one row exist for each tag', () => {
mountComponent();
expect(findTagsListRow()).toHaveLength(tags.length);
});
it('the correct props are bound to it', () => {
mountComponent();
expect(
findTagsListRow()
.at(0)
.attributes(),
).toMatchObject({
index: '0',
isdesktop: 'true',
});
});
describe('events', () => {
it('select event update the selected items', async () => {
mountComponent();
findTagsListRow()
.at(0)
.vm.$emit('select');
await wrapper.vm.$nextTick();
expect(
findTagsListRow()
.at(0)
.attributes('selected'),
).toBe('true');
});
it('delete event emit a delete event', () => {
mountComponent();
findTagsListRow()
.at(0)
.vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]);
});
});
});
});
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/components/details_page/tags_table.vue';
import { tagsListResponse } from '../../mock_data';
describe('tags_table', () => {
let wrapper;
const tags = [...tagsListResponse.data];
const findMainCheckbox = () => wrapper.find('[data-testid="mainCheckbox"]');
const findFirstRowItem = testid => wrapper.find(`[data-testid="${testid}"]`);
const findBulkDeleteButton = () => wrapper.find('[data-testid="bulkDeleteButton"]');
const findAllDeleteButtons = () => wrapper.findAll('[data-testid="singleDeleteButton"]');
const findAllCheckboxes = () => wrapper.findAll('[data-testid="rowCheckbox"]');
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
const findLoaderSlot = () => wrapper.find('[data-testid="loaderSlot"]');
const findEmptySlot = () => wrapper.find('[data-testid="emptySlot"]');
const mountComponent = (propsData = { tags, isDesktop: true }) => {
wrapper = mount(component, {
stubs: {
...stubChildren(component),
GlTable: false,
},
propsData,
slots: {
loader: '<div data-testid="loaderSlot"></div>',
empty: '<div data-testid="emptySlot"></div>',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it.each([
'rowCheckbox',
'rowName',
'rowShortRevision',
'rowSize',
'rowTime',
'singleDeleteButton',
])('%s exist in the table', element => {
mountComponent();
expect(findFirstRowItem(element).exists()).toBe(true);
});
describe('header checkbox', () => {
it('exists', () => {
mountComponent();
expect(findMainCheckbox().exists()).toBe(true);
});
it('if selected selects all the rows', () => {
mountComponent();
findMainCheckbox().vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(findMainCheckbox().attributes('checked')).toBeTruthy();
expect(findCheckedCheckboxes()).toHaveLength(tags.length);
});
});
it('if deselect deselects all the row', () => {
mountComponent();
findMainCheckbox().vm.$emit('change');
return wrapper.vm
.$nextTick()
.then(() => {
expect(findMainCheckbox().attributes('checked')).toBeTruthy();
findMainCheckbox().vm.$emit('change');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(findMainCheckbox().attributes('checked')).toBe(undefined);
expect(findCheckedCheckboxes()).toHaveLength(0);
});
});
});
describe('row checkbox', () => {
beforeEach(() => {
mountComponent();
});
it('selecting and deselecting the checkbox works as intended', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.selectedItems).toEqual([tags[0].name]);
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy();
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.vm.selectedItems.length).toBe(0);
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBe(undefined);
});
});
});
describe('header delete button', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => {
expect(findBulkDeleteButton().exists()).toBe(true);
});
it('is disabled if no item is selected', () => {
expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
});
it('is enabled if at least one item is selected', () => {
expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
});
});
describe('on click', () => {
it('when one item is selected', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
findBulkDeleteButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[['centos6']]]);
});
it('when multiple items are selected', () => {
findMainCheckbox().vm.$emit('change');
findBulkDeleteButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[tags.map(t => t.name)]]);
});
});
});
describe('row delete button', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => {
expect(
findAllDeleteButtons()
.at(0)
.exists(),
).toBe(true);
});
it('is disabled if the item has no destroy_path', () => {
expect(
findAllDeleteButtons()
.at(1)
.attributes('disabled'),
).toBe('true');
});
it('on click', () => {
findAllDeleteButtons()
.at(0)
.vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[['centos6']]]);
});
});
describe('name cell', () => {
it('tag column has a tooltip with the tag name', () => {
mountComponent();
expect(findFirstTagNameText().attributes('title')).toBe(tagsListResponse.data[0].name);
});
describe('on desktop viewport', () => {
beforeEach(() => {
mountComponent();
});
it('table header has class w-25', () => {
expect(findFirsTagColumn().classes()).toContain('w-25');
});
it('tag column has the mw-m class', () => {
expect(findFirstRowItem('rowName').classes()).toContain('mw-m');
});
});
describe('on mobile viewport', () => {
beforeEach(() => {
mountComponent({ tags, isDesktop: false });
});
it('table header does not have class w-25', () => {
expect(findFirsTagColumn().classes()).not.toContain('w-25');
});
it('tag column has the gl-justify-content-end class', () => {
expect(findFirstRowItem('rowName').classes()).toContain('gl-justify-content-end');
});
});
});
describe('last updated cell', () => {
let timeCell;
beforeEach(() => {
mountComponent();
timeCell = findFirstRowItem('rowTime');
});
it('displays the time in string format', () => {
expect(timeCell.text()).toBe('2 years ago');
});
it('has a tooltip timestamp', () => {
expect(timeCell.attributes('title')).toBe('Sep 19, 2017 1:45pm GMT+0000');
});
});
describe('empty state slot', () => {
describe('when the table is empty', () => {
beforeEach(() => {
mountComponent({ tags: [], isDesktop: true });
});
it('does not show table rows', () => {
expect(findFirstTagNameText().exists()).toBe(false);
});
it('has the empty state slot', () => {
expect(findEmptySlot().exists()).toBe(true);
});
});
describe('when the table is not empty', () => {
beforeEach(() => {
mountComponent({ tags, isDesktop: true });
});
it('does show table rows', () => {
expect(findFirstTagNameText().exists()).toBe(true);
});
it('does not show the empty state', () => {
expect(findEmptySlot().exists()).toBe(false);
});
});
});
describe('loader slot', () => {
describe('when the data is loading', () => {
beforeEach(() => {
mountComponent({ isLoading: true, tags });
});
it('show the loader', () => {
expect(findLoaderSlot().exists()).toBe(true);
});
it('does not show the table rows', () => {
expect(findFirstTagNameText().exists()).toBe(false);
});
});
describe('when the data is not loading', () => {
beforeEach(() => {
mountComponent({ isLoading: false, tags });
});
it('does not show the loader', () => {
expect(findLoaderSlot().exists()).toBe(false);
});
it('shows the table rows', () => {
expect(findFirstTagNameText().exists()).toBe(true);
});
});
});
});
......@@ -4,17 +4,23 @@ import component from '~/registry/explorer/components/list_item.vue';
describe('list item', () => {
let wrapper;
const findLeftActionSlot = () => wrapper.find('[data-testid="left-action"]');
const findLeftPrimarySlot = () => wrapper.find('[data-testid="left-primary"]');
const findLeftSecondarySlot = () => wrapper.find('[data-testid="left-secondary"]');
const findRightSlot = () => wrapper.find('[data-testid="right"]');
const findRightPrimarySlot = () => wrapper.find('[data-testid="right-primary"]');
const findRightSecondarySlot = () => wrapper.find('[data-testid="right-secondary"]');
const findRightActionSlot = () => wrapper.find('[data-testid="right-action"]');
const mountComponent = propsData => {
wrapper = shallowMount(component, {
propsData,
slots: {
'left-action': '<div data-testid="left-action" />',
'left-primary': '<div data-testid="left-primary" />',
'left-secondary': '<div data-testid="left-secondary" />',
right: '<div data-testid="right" />',
'right-primary': '<div data-testid="right-primary" />',
'right-secondary': '<div data-testid="right-secondary" />',
'right-action': '<div data-testid="right-action" />',
},
});
};
......@@ -24,29 +30,30 @@ describe('list item', () => {
wrapper = null;
});
it('has a left primary slot', () => {
it.each`
slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot}
${'left-secondary'} | ${findLeftSecondarySlot}
${'right-primary'} | ${findRightPrimarySlot}
${'right-secondary'} | ${findRightSecondarySlot}
${'left-action'} | ${findLeftActionSlot}
${'right-action'} | ${findRightActionSlot}
`('has a $slotName slot', ({ finderFunction }) => {
mountComponent();
expect(findLeftPrimarySlot().exists()).toBe(true);
});
it('has a left secondary slot', () => {
mountComponent();
expect(findLeftSecondarySlot().exists()).toBe(true);
});
it('has a right slot', () => {
mountComponent();
expect(findRightSlot().exists()).toBe(true);
expect(finderFunction().exists()).toBe(true);
});
describe('disabled prop', () => {
it('when true applies disabled-content class', () => {
mountComponent({ disabled: true });
expect(wrapper.classes('disabled-content')).toBe(true);
});
it('when false does not apply disabled-content class', () => {
mountComponent({ disabled: false });
expect(wrapper.classes('disabled-content')).toBe(false);
});
});
......@@ -54,15 +61,38 @@ describe('list item', () => {
describe('index prop', () => {
it('when index is 0 displays a top border', () => {
mountComponent({ index: 0 });
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-border-t-solid', 'gl-border-t-1']),
);
});
it('when index is not 0 hides top border', () => {
it('when index is not 0 hides top border', () => {
mountComponent({ index: 1 });
expect(wrapper.classes('gl-border-t-solid')).toBe(false);
expect(wrapper.classes('gl-border-t-1')).toBe(false);
expect(wrapper.classes()).toEqual(
expect.not.arrayContaining(['gl-border-t-solid', 'gl-border-t-1']),
);
});
});
describe('selected prop', () => {
it('when true applies the selected border and background', () => {
mountComponent({ selected: true });
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
);
expect(wrapper.classes()).toEqual(expect.not.arrayContaining(['gl-border-gray-200']));
});
it('when false applies the default border', () => {
mountComponent({ selected: false });
expect(wrapper.classes()).toEqual(
expect.not.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
);
expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-200']));
});
});
});
......@@ -71,7 +71,7 @@ export const tagsListResponse = {
layers: 10,
location: 'location',
path: 'bar',
created_at: 1505828744434,
created_at: '1505828744434',
destroy_path: 'path',
},
{
......@@ -82,7 +82,7 @@ export const tagsListResponse = {
layers: 10,
path: 'foo',
location: 'location-2',
created_at: 1505828744434,
created_at: '1505828744434',
},
],
headers,
......
......@@ -5,6 +5,7 @@ import component from '~/registry/explorer/pages/details.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
import { createStore } from '~/registry/explorer/stores/';
import {
......@@ -15,7 +16,7 @@ import {
} from '~/registry/explorer/stores/mutation_types/';
import { tagsListResponse } from '../mock_data';
import { TagsTable, DeleteModal } from '../stubs';
import { DeleteModal } from '../stubs';
describe('Details Page', () => {
let wrapper;
......@@ -25,18 +26,23 @@ describe('Details Page', () => {
const findDeleteModal = () => wrapper.find(DeleteModal);
const findPagination = () => wrapper.find(GlPagination);
const findTagsLoader = () => wrapper.find(TagsLoader);
const findTagsTable = () => wrapper.find(TagsTable);
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
const findEmptyTagsState = () => wrapper.find(EmptyTagsState);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
const tagsArrayToSelectedTags = tags =>
tags.reduce((acc, c) => {
acc[c.name] = true;
return acc;
}, {});
const mountComponent = options => {
wrapper = shallowMount(component, {
store,
stubs: {
TagsTable,
DeleteModal,
},
mocks: {
......@@ -66,15 +72,18 @@ describe('Details Page', () => {
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent();
store.commit(SET_MAIN_LOADING, true);
return wrapper.vm.$nextTick();
mountComponent();
});
afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('binds isLoading to tags-table', () => {
expect(findTagsTable().props('isLoading')).toBe(true);
it('shows the loader', () => {
expect(findTagsLoader().exists()).toBe(true);
});
it('does not show the list', () => {
expect(findTagsList().exists()).toBe(false);
});
it('does not show pagination', () => {
......@@ -82,8 +91,9 @@ describe('Details Page', () => {
});
});
describe('table slots', () => {
describe('when the list of tags is empty', () => {
beforeEach(() => {
store.commit(SET_TAGS_LIST_SUCCESS, []);
mountComponent();
});
......@@ -91,32 +101,37 @@ describe('Details Page', () => {
expect(findEmptyTagsState().exists()).toBe(true);
});
it('has a skeleton loader', () => {
expect(findTagsLoader().exists()).toBe(true);
it('does not show the loader', () => {
expect(findTagsLoader().exists()).toBe(false);
});
it('does not show the list', () => {
expect(findTagsList().exists()).toBe(false);
});
});
describe('table', () => {
describe('list', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => {
expect(findTagsTable().exists()).toBe(true);
expect(findTagsList().exists()).toBe(true);
});
it('has the correct props bound', () => {
expect(findTagsTable().props()).toMatchObject({
expect(findTagsList().props()).toMatchObject({
isDesktop: true,
isLoading: false,
tags: store.state.tags,
});
});
describe('deleteEvent', () => {
describe('single item', () => {
let tagToBeDeleted;
beforeEach(() => {
findTagsTable().vm.$emit('delete', [store.state.tags[0].name]);
[tagToBeDeleted] = store.state.tags;
findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true });
});
it('open the modal', () => {
......@@ -124,7 +139,7 @@ describe('Details Page', () => {
});
it('maps the selection to itemToBeDeleted', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([store.state.tags[0]]);
expect(wrapper.vm.itemsToBeDeleted).toEqual([tagToBeDeleted]);
});
it('tracks a single delete event', () => {
......@@ -136,7 +151,7 @@ describe('Details Page', () => {
describe('multiple items', () => {
beforeEach(() => {
findTagsTable().vm.$emit('delete', store.state.tags.map(t => t.name));
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags));
});
it('open the modal', () => {
......@@ -202,7 +217,7 @@ describe('Details Page', () => {
describe('when one item is selected to be deleted', () => {
beforeEach(() => {
mountComponent();
findTagsTable().vm.$emit('delete', [store.state.tags[0].name]);
findTagsList().vm.$emit('delete', { [store.state.tags[0].name]: true });
});
it('dispatch requestDeleteTag with the right parameters', () => {
......@@ -217,7 +232,7 @@ describe('Details Page', () => {
describe('when more than one item is selected to be deleted', () => {
beforeEach(() => {
mountComponent();
findTagsTable().vm.$emit('delete', store.state.tags.map(t => t.name));
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags));
});
it('dispatch requestDeleteTags with the right parameters', () => {
......
import RealTagsTable from '~/registry/explorer/components/details_page/tags_table.vue';
import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
export const GlModal = {
......@@ -18,11 +17,6 @@ export const RouterLink = {
props: ['to'],
};
export const TagsTable = {
props: RealTagsTable.props,
template: `<div><slot name="empty"></slot><slot name="loader"></slot></div>`,
};
export const DeleteModal = {
template: '<div></div>',
methods: {
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Mutations::AlertManagement::Alerts::SetAssignees do
RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
let_it_be(:starting_assignee) { create(:user) }
let_it_be(:unassigned_user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, assignees: [starting_assignee]) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Mutations::ContainerExpirationPolicies::Update do
RSpec.describe Mutations::ContainerExpirationPolicies::Update do
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe LooksAhead do
RSpec.describe LooksAhead do
include GraphqlHelpers
let_it_be(:the_user) { create(:user) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Resolvers::ProjectMembersResolver do
RSpec.describe Resolvers::ProjectMembersResolver do
include GraphqlHelpers
context "with a group" do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Resolvers::ProjectPipelineResolver do
RSpec.describe Resolvers::ProjectPipelineResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Resolvers::UsersResolver do
RSpec.describe Resolvers::UsersResolver do
include GraphqlHelpers
let_it_be(:user1) { create(:user) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['AccessLevelEnum'] do
RSpec.describe GitlabSchema.types['AccessLevelEnum'] do
specify { expect(described_class.graphql_name).to eq('AccessLevelEnum') }
it 'exposes all the existing access levels' do
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AccessLevel'] do
RSpec.describe GitlabSchema.types['AccessLevel'] do
specify { expect(described_class.graphql_name).to eq('AccessLevel') }
specify { expect(described_class).to require_graphql_authorizations(nil) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['ReleaseEvidence'] do
RSpec.describe GitlabSchema.types['ReleaseEvidence'] do
it { expect(described_class).to require_graphql_authorizations(:download_code) }
it 'has the expected fields' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Types::GroupMemberType do
RSpec.describe Types::GroupMemberType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
specify { expect(described_class.graphql_name).to eq('GroupMember') }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['MilestoneStats'] do
RSpec.describe GitlabSchema.types['MilestoneStats'] do
it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
it 'has the expected fields' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Types::ProjectMemberType do
RSpec.describe Types::ProjectMemberType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
specify { expect(described_class.graphql_name).to eq('ProjectMember') }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['ReleaseAssetLink'] do
RSpec.describe GitlabSchema.types['ReleaseAssetLink'] do
it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Types::Snippets::FileInputActionEnum do
RSpec.describe Types::Snippets::FileInputActionEnum do
specify { expect(described_class.graphql_name).to eq('SnippetFileInputActionEnum') }
it 'exposes all file input action types' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Types::Snippets::FileInputType do
RSpec.describe Types::Snippets::FileInputType do
specify { expect(described_class.graphql_name).to eq('SnippetFileInputType') }
it 'has the correct arguments' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['UntrustedRegexp'] do
RSpec.describe GitlabSchema.types['UntrustedRegexp'] do
using RSpec::Parameterized::TableSyntax
specify { expect(described_class.graphql_name).to eq('UntrustedRegexp') }
......
......@@ -2,7 +2,7 @@
require "spec_helper"
describe Analytics::UniqueVisitsHelper do
RSpec.describe Analytics::UniqueVisitsHelper do
include Devise::Test::ControllerHelpers
describe '#track_visit' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe SubscribableBannerHelper do
RSpec.describe SubscribableBannerHelper do
describe '#display_subscription_banner!' do
it 'is over-written in EE' do
expect { helper.display_subscription_banner! }.not_to raise_error
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe API::Entities::DeployKey do
RSpec.describe API::Entities::DeployKey do
describe '#as_json' do
subject { entity.as_json }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe API::Entities::DeployKeysProject do
RSpec.describe API::Entities::DeployKeysProject do
describe '#as_json' do
subject { entity.as_json }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe API::Entities::SSHKey do
RSpec.describe API::Entities::SSHKey do
describe '#as_json' do
subject { entity.as_json }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe API::Validations::Validators::UntrustedRegexp do
RSpec.describe API::Validations::Validators::UntrustedRegexp do
include ApiValidatorsHelpers
subject do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Banzai::Filter::JiraImport::AdfToCommonmarkFilter do
RSpec.describe Banzai::Filter::JiraImport::AdfToCommonmarkFilter do
include FilterSpecHelper
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline do
RSpec.describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline do
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
it 'converts text in Atlassian Document Format ' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe ExtractsRef do
RSpec.describe ExtractsRef do
include described_class
include RepoHelpers
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state do
RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state do
let(:unique_visits) { Gitlab::Analytics::UniqueVisits.new }
let(:target1_id) { 'g_analytics_contribution' }
let(:target2_id) { 'g_analytics_insights' }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Ci::Build::Releaser do
RSpec.describe Gitlab::Ci::Build::Releaser do
subject { described_class.new(config: config[:release]).script }
describe '#script' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Ci::Reports::TestReportSummary do
RSpec.describe Gitlab::Ci::Reports::TestReportSummary do
let(:build_report_result_1) { build(:ci_build_report_result) }
let(:build_report_result_2) { build(:ci_build_report_result, :with_junit_success) }
let(:test_report_summary) { described_class.new([build_report_result_1, build_report_result_2]) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Ci::Reports::TestSuiteSummary do
RSpec.describe Gitlab::Ci::Reports::TestSuiteSummary do
let(:build_report_result_1) { build(:ci_build_report_result) }
let(:build_report_result_2) { build(:ci_build_report_result, :with_junit_success) }
let(:test_suite_summary) { described_class.new([build_report_result_1, build_report_result_2]) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::DataBuilder::Alert do
RSpec.describe Gitlab::DataBuilder::Alert do
let_it_be(:project) { create(:project) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Database::CustomStructure do
RSpec.describe Gitlab::Database::CustomStructure do
let_it_be(:structure) { described_class.new }
let_it_be(:filepath) { Rails.root.join(described_class::CUSTOM_DUMP_FILE) }
let_it_be(:file_header) do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Database::DynamicModelHelpers do
RSpec.describe Gitlab::Database::DynamicModelHelpers do
describe '#define_batchable_model' do
subject { including_class.new.define_batchable_model(table_name) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
let(:model) do
ActiveRecord::Migration.new.extend(described_class)
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable, '#perform' do
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable, '#perform' do
subject { described_class.new }
let(:source_table) { '_test_partitioning_backfills' }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::DependencyLinker::GoModLinker do
RSpec.describe Gitlab::DependencyLinker::GoModLinker do
let(:file_name) { 'go.mod' }
let(:file_content) do
<<-CONTENT.strip_heredoc
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::DependencyLinker::GoSumLinker do
RSpec.describe Gitlab::DependencyLinker::GoSumLinker do
let(:file_name) { 'go.sum' }
let(:file_content) do
<<-CONTENT.strip_heredoc
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Diff::StatsCache, :use_clean_rails_memory_store_caching do
RSpec.describe Gitlab::Diff::StatsCache, :use_clean_rails_memory_store_caching do
subject(:stats_cache) { described_class.new(cachable_key: cachable_key) }
let(:key) { ['diff_stats', cachable_key, described_class::VERSION].join(":") }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Doctor::Secrets do
RSpec.describe Gitlab::Doctor::Secrets do
let!(:user) { create(:user, otp_secret: "test") }
let!(:group) { create(:group, runners_token: "test") }
let(:logger) { double(:logger).as_null_object }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Emoji do
RSpec.describe Gitlab::Emoji do
let_it_be(:emojis) { Gemojione.index.instance_variable_get(:@emoji_by_name) }
let_it_be(:emojis_by_moji) { Gemojione.index.instance_variable_get(:@emoji_by_moji) }
let_it_be(:emoji_unicode_versions_by_name) { Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::GitAccessProject do
RSpec.describe Gitlab::GitAccessProject do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:actor) { user }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Graphql::MountMutation do
RSpec.describe Gitlab::Graphql::MountMutation do
let_it_be(:mutation) do
Class.new(Mutations::BaseMutation) do
graphql_name 'TestMutation'
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Instrumentation::RedisBase, :request_store do
RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
let(:instrumentation_class_a) do
stub_const('InstanceA', Class.new(described_class))
end
......
......@@ -4,7 +4,7 @@ require 'fast_spec_helper'
require 'support/helpers/rails_helpers'
require 'rspec-parameterized'
describe Gitlab::Instrumentation::RedisClusterValidator do
RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
include RailsHelpers
describe '.validate!' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Stages::UrlValidator do
RSpec.describe Gitlab::Metrics::Dashboard::Stages::UrlValidator do
let(:project) { build_stubbed(:project) }
describe '#transform!' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Stages::VariableEndpointInserter do
RSpec.describe Gitlab::Metrics::Dashboard::Stages::VariableEndpointInserter do
include MetricsDashboardHelpers
let(:project) { build_stubbed(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Suggestions::CommitMessage do
RSpec.describe Gitlab::Suggestions::CommitMessage do
def create_suggestion(file_path, new_line, to_content)
position = Gitlab::Diff::Position.new(old_path: file_path,
new_path: file_path,
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Suggestions::FileSuggestion do
RSpec.describe Gitlab::Suggestions::FileSuggestion do
def create_suggestion(new_line, to_content, lines_above = 0, lines_below = 0)
position = Gitlab::Diff::Position.new(old_path: file_path,
new_path: file_path,
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Suggestions::SuggestionSet do
RSpec.describe Gitlab::Suggestions::SuggestionSet do
def create_suggestion(file_path, new_line, to_content)
position = Gitlab::Diff::Position.new(old_path: file_path,
new_path: file_path,
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::UsageDataConcerns::Topology do
RSpec.describe Gitlab::UsageDataConcerns::Topology do
include UsageDataHelpers
describe '#topology_usage_data' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
context Kramdown::Parser::AtlassianDocumentFormat do
RSpec.context Kramdown::Parser::AtlassianDocumentFormat do
let_it_be(:options) { { input: 'AtlassianDocumentFormat', html_tables: true } }
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe LearnGitlab do
RSpec.describe LearnGitlab do
let_it_be(:current_user) { create(:user) }
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::PROJECT_NAME) }
let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::BOARD_NAME) }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb')
describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
RSpec.describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
let(:migration) { described_class.new }
let(:approval_rules) { table(:approval_merge_request_rules) }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200609212701_add_incident_settings_to_all_existing_projects.rb')
describe AddIncidentSettingsToAllExistingProjects, :migration do
RSpec.describe AddIncidentSettingsToAllExistingProjects, :migration do
let(:project_incident_management_settings) { table(:project_incident_management_settings) }
let(:labels) { table(:labels) }
let(:label_links) { table(:label_links) }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200608072931_backfill_imported_snippet_repositories.rb')
describe BackfillImportedSnippetRepositories do
RSpec.describe BackfillImportedSnippetRepositories do
let(:users) { table(:users) }
let(:snippets) { table(:snippets) }
let(:user) { users.create(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test', state: 'active') }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200602013901_cap_designs_filename_length_to_new_limit')
describe CapDesignsFilenameLengthToNewLimit, :migration, schema: 20200528125905 do
RSpec.describe CapDesignsFilenameLengthToNewLimit, :migration, schema: 20200528125905 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
......
......@@ -4,7 +4,7 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb')
describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
RSpec.describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:members) { table(:members) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe AlertManagement::AlertUserMention do
RSpec.describe AlertManagement::AlertUserMention do
describe 'associations' do
it { is_expected.to belong_to(:alert_management_alert) }
it { is_expected.to belong_to(:note) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe BlobViewer::GoMod do
RSpec.describe BlobViewer::GoMod do
include FakeBlobHelpers
let(:project) { build_stubbed(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GroupDeployKey do
RSpec.describe GroupDeployKey do
it { is_expected.to validate_presence_of(:user) }
it 'is of type DeployKey' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe GroupImportState do
RSpec.describe GroupImportState do
describe 'validations' do
let_it_be(:group) { create(:group) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe ChatMessage::AlertMessage do
RSpec.describe ChatMessage::AlertMessage do
subject { described_class.new(args) }
let_it_be(:start_time) { Time.current }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe SnippetStatistics do
RSpec.describe SnippetStatistics do
let_it_be(:snippet_without_repo) { create(:snippet) }
let_it_be(:snippet_with_repo) { create(:snippet, :repository) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::BlamePresenter do
RSpec.describe Gitlab::BlamePresenter do
let(:project) { create(:project, :repository) }
let(:path) { 'files/ruby/popen.rb' }
let(:commit) { project.commit('master') }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Setting assignees of an alert' do
RSpec.describe 'Setting assignees of an alert' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Updating the container expiration policy' do
RSpec.describe 'Updating the container expiration policy' do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Importing Jira Users' do
RSpec.describe 'Importing Jira Users' do
include JiraServiceHelper
include GraphqlHelpers
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Mutations::Metrics::Dashboard::Annotations::Delete do
RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'getting Alert Management Alert Assignees' do
RSpec.describe 'getting Alert Management Alert Assignees' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'getting Alert Management Alert Notes' do
RSpec.describe 'getting Alert Management Alert Notes' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'getting pipeline information nested in a project' do
RSpec.describe 'getting pipeline information nested in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository, :public) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Query.project(fullPath).releases()' do
RSpec.describe 'Query.project(fullPath).releases()' do
include GraphqlHelpers
let_it_be(:stranger) { create(:user) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'GroupMember' do
RSpec.describe 'GroupMember' do
include GraphqlHelpers
let_it_be(:member) { create(:group_member, :developer) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'ProjectMember' do
RSpec.describe 'ProjectMember' do
include GraphqlHelpers
let_it_be(:member) { create(:project_member, :developer) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Users' do
RSpec.describe 'Users' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user, created_at: 1.day.ago) }
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册