Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/registry')
-rw-r--r--spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap4
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js48
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js55
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_spec.js10
-rw-r--r--spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap5
-rw-r--r--spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap11
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js41
-rw-r--r--spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js17
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js61
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_spec.js60
-rw-r--r--spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js31
-rw-r--r--spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js9
-rw-r--r--spec/frontend/registry/explorer/mock_data.js281
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js336
-rw-r--r--spec/frontend/registry/explorer/pages/index_spec.js4
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js341
-rw-r--r--spec/frontend/registry/explorer/stores/actions_spec.js362
-rw-r--r--spec/frontend/registry/explorer/stores/getters_spec.js41
-rw-r--r--spec/frontend/registry/explorer/stores/mutations_spec.js133
-rw-r--r--spec/frontend/registry/explorer/stubs.js32
-rw-r--r--spec/frontend/registry/explorer/utils_spec.js56
-rw-r--r--spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap (renamed from spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap)8
-rw-r--r--spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap64
-rw-r--r--spec/frontend/registry/settings/components/expiration_dropdown_spec.js83
-rw-r--r--spec/frontend/registry/settings/components/expiration_input_spec.js169
-rw-r--r--spec/frontend/registry/settings/components/expiration_run_text_spec.js62
-rw-r--r--spec/frontend/registry/settings/components/expiration_toggle_spec.js77
-rw-r--r--spec/frontend/registry/settings/components/registry_settings_app_spec.js33
-rw-r--r--spec/frontend/registry/settings/components/settings_form_spec.js195
-rw-r--r--spec/frontend/registry/settings/graphql/cache_updated_spec.js2
-rw-r--r--spec/frontend/registry/settings/mock_data.js24
-rw-r--r--spec/frontend/registry/settings/utils_spec.js (renamed from spec/frontend/registry/shared/utils_spec.js)7
-rw-r--r--spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap148
-rw-r--r--spec/frontend/registry/shared/components/expiration_policy_fields_spec.js202
-rw-r--r--spec/frontend/registry/shared/mock_data.js12
-rw-r--r--spec/frontend/registry/shared/stubs.js20
36 files changed, 1522 insertions, 1522 deletions
diff --git a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap b/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
index aeb49f88770..5f191ef5561 100644
--- a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
+++ b/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
@@ -2,9 +2,7 @@
exports[`TagsLoader component has the correct markup 1`] = `
<div>
- <div
- preserve-aspect-ratio="xMinYMax meet"
- >
+ <div>
<rect
height="15"
rx="4"
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
index fc93e9094c9..ec883886026 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -7,9 +7,27 @@ import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants';
describe('Details Header', () => {
let wrapper;
- const mountComponent = propsData => {
+ const defaultImage = {
+ name: 'foo',
+ updatedAt: '2020-11-03T13:29:21Z',
+ project: {
+ visibility: 'public',
+ },
+ };
+
+ const findLastUpdatedAndVisibility = () => wrapper.find('[data-testid="updated-and-visibility"]');
+
+ const waitForMetadataItems = async () => {
+ // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
+ await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
+ };
+
+ const mountComponent = (image = defaultImage) => {
wrapper = shallowMount(component, {
- propsData,
+ propsData: {
+ image,
+ },
stubs: {
GlSprintf,
TitleArea,
@@ -23,12 +41,34 @@ describe('Details Header', () => {
});
it('has the correct title ', () => {
- mountComponent();
+ mountComponent({ ...defaultImage, name: '' });
expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
it('shows imageName in the title', () => {
- mountComponent({ imageName: 'foo' });
+ mountComponent();
expect(wrapper.text()).toContain('foo');
});
+
+ it('has a metadata item with last updated text', async () => {
+ mountComponent();
+ await waitForMetadataItems();
+
+ expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago');
+ });
+
+ describe('visibility icon', () => {
+ it('shows an eye when the project is public', async () => {
+ mountComponent();
+ await waitForMetadataItems();
+
+ expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
+ });
+ it('shows an eye slashed when the project is not public', async () => {
+ mountComponent({ ...defaultImage, project: { visibility: 'private' } });
+ await waitForMetadataItems();
+
+ expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
+ });
+ });
});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index 3276ef911e3..e1b75636735 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -15,12 +15,12 @@ import {
NOT_AVAILABLE_SIZE,
} from '~/registry/explorer/constants/index';
-import { tagsListResponse } from '../../mock_data';
+import { tagsMock } from '../../mock_data';
import { ListItem } from '../../stubs';
describe('tags list row', () => {
let wrapper;
- const [tag] = [...tagsListResponse.data];
+ const [tag] = [...tagsMock];
const defaultProps = { tag, isMobile: false, index: 0 };
@@ -65,7 +65,7 @@ describe('tags list row', () => {
});
it("does not exist when the row can't be deleted", () => {
- const customTag = { ...tag, destroy_path: '' };
+ const customTag = { ...tag, canDelete: false };
mountComponent({ ...defaultProps, tag: customTag });
@@ -137,8 +137,8 @@ describe('tags list row', () => {
mountComponent();
expect(findClipboardButton().attributes()).toMatchObject({
- text: 'location',
- title: 'location',
+ text: tag.location,
+ title: tag.location,
});
});
});
@@ -171,26 +171,26 @@ describe('tags list row', () => {
expect(findSize().exists()).toBe(true);
});
- it('contains the total_size and layers', () => {
- mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024 } });
+ it('contains the totalSize and layers', () => {
+ mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 1024, layers: 10 } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB · 10 layers');
});
- it('when total_size is missing', () => {
- mountComponent();
+ it('when totalSize is missing', () => {
+ mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 0, layers: 10 } });
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`);
});
it('when layers are missing', () => {
- mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024, layers: null } });
+ mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 1024 } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB');
});
it('when there is 1 layer', () => {
- mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } });
+ mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 0, layers: 1 } });
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`);
});
@@ -218,7 +218,7 @@ describe('tags list row', () => {
it('pass the correct props to time ago tooltip', () => {
mountComponent();
- expect(findTimeAgoTooltip().attributes()).toMatchObject({ time: tag.created_at });
+ expect(findTimeAgoTooltip().attributes()).toMatchObject({ time: tag.createdAt });
});
});
@@ -232,7 +232,7 @@ describe('tags list row', () => {
it('has the correct text', () => {
mountComponent();
- expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5');
+ expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 2cf3d2f');
});
it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => {
@@ -260,18 +260,15 @@ describe('tags list row', () => {
});
it.each`
- destroy_path | digest
- ${'foo'} | ${null}
- ${null} | ${'foo'}
- ${null} | ${null}
- `(
- 'is disabled when destroy_path is $destroy_path and digest is $digest',
- ({ destroy_path, digest }) => {
- mountComponent({ ...defaultProps, tag: { ...tag, destroy_path, digest } });
-
- expect(findDeleteButton().attributes('disabled')).toBe('true');
- },
- );
+ canDelete | digest
+ ${true} | ${null}
+ ${false} | ${'foo'}
+ ${false} | ${null}
+ `('is disabled when canDelete is $canDelete and digest is $digest', ({ canDelete, digest }) => {
+ mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest } });
+
+ expect(findDeleteButton().attributes('disabled')).toBe('true');
+ });
it('delete event emits delete', () => {
mountComponent();
@@ -295,10 +292,10 @@ describe('tags list row', () => {
});
describe.each`
- name | finderFunction | text | icon | clipboard
- ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false}
- ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true}
- ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true}
+ name | finderFunction | text | icon | clipboard
+ ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 01:29 GMT+0000 on 2020-11-03'} | ${'clock'} | ${false}
+ ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
+ ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, () => {
expect(finderFunction().text()).toMatchInterpolatedText(text);
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
index ebeaa8ff870..035b59731c9 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
@@ -3,12 +3,12 @@ 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';
+import { tagsMock } from '../../mock_data';
describe('Tags List', () => {
let wrapper;
- const tags = [...tagsListResponse.data];
- const readOnlyTags = tags.map(t => ({ ...t, destroy_path: undefined }));
+ const tags = [...tagsMock];
+ const readOnlyTags = tags.map(t => ({ ...t, canDelete: false }));
const findTagsListRow = () => wrapper.findAll(TagsListRow);
const findDeleteButton = () => wrapper.find(GlButton);
@@ -92,7 +92,7 @@ describe('Tags List', () => {
.vm.$emit('select');
findDeleteButton().vm.$emit('click');
- expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]);
+ expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]);
});
});
@@ -132,7 +132,7 @@ describe('Tags List', () => {
findTagsListRow()
.at(0)
.vm.$emit('delete');
- expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]);
+ expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]);
});
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
index a8412e2bde9..56579847468 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
@@ -1,10 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Group Empty state to match the default snapshot 1`] = `
-<div
- svg-path="foo"
- title="There are no container images available in this group"
->
+<div>
<p>
With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
<gl-link-stub
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
index 8413e17c7b2..bab6b25cc15 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
@@ -1,10 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Project Empty state to match the default snapshot 1`] = `
-<div
- svg-path="bazFoo"
- title="There are no container images stored for this project"
->
+<div>
<p>
With the Container Registry, every project can have its own space to store its Docker images.
<gl-link-stub
@@ -46,7 +43,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
class="gl-font-monospace!"
readonly=""
type="text"
- value="docker login bar"
+ value="bazbaz"
/>
</gl-form-input-group-stub>
@@ -67,7 +64,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
class="gl-font-monospace!"
readonly=""
type="text"
- value="docker build -t foo ."
+ value="foofoo"
/>
</gl-form-input-group-stub>
@@ -79,7 +76,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
class="gl-font-monospace!"
readonly=""
type="text"
- value="docker push foo"
+ value="barbar"
/>
</gl-form-input-group-stub>
</div>
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
index 551d1eee68d..74b9ea5fd96 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
@@ -2,10 +2,8 @@ import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdown } from '@gitlab/ui';
import Tracking from '~/tracking';
-import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
-
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -14,31 +12,33 @@ import {
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
-} from '~/registry/explorer//constants';
+} from '~/registry/explorer/constants';
+
+import { dockerCommands } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('cli_commands', () => {
let wrapper;
- let store;
+
+ const config = {
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'bar',
+ };
const findDropdownButton = () => wrapper.find(GlDropdown);
const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
const mountComponent = () => {
- store = new Vuex.Store({
- state: {
- config: {
- repositoryUrl: 'foo',
- registryHostUrlWithPort: 'bar',
- },
- },
- getters,
- });
wrapper = mount(QuickstartDropdown, {
localVue,
- store,
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
});
};
@@ -50,7 +50,6 @@ describe('cli_commands', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
- store = null;
});
it('shows the correct text on the button', () => {
@@ -67,11 +66,11 @@ describe('cli_commands', () => {
});
describe.each`
- index | labelText | titleText | getter | trackedEvent
- ${0} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${'dockerLoginCommand'} | ${'click_copy_login'}
- ${1} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${'dockerBuildCommand'} | ${'click_copy_build'}
- ${2} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${'dockerPushCommand'} | ${'click_copy_push'}
- `('code instructions at $index', ({ index, labelText, titleText, getter, trackedEvent }) => {
+ index | labelText | titleText | command | trackedEvent
+ ${0} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${dockerCommands.dockerLoginCommand} | ${'click_copy_login'}
+ ${1} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${dockerCommands.dockerBuildCommand} | ${'click_copy_build'}
+ ${2} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${dockerCommands.dockerPushCommand} | ${'click_copy_push'}
+ `('code instructions at $index', ({ index, labelText, titleText, command, trackedEvent }) => {
let codeInstruction;
beforeEach(() => {
@@ -85,7 +84,7 @@ describe('cli_commands', () => {
it(`has the correct props`, () => {
expect(codeInstruction.props()).toMatchObject({
label: labelText,
- instruction: store.getters[getter],
+ instruction: command,
copyText: titleText,
trackingAction: trackedEvent,
trackingLabel: 'quickstart_dropdown',
diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
index 2f51e875672..1ba2036dc34 100644
--- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
@@ -9,24 +9,21 @@ localVue.use(Vuex);
describe('Registry Group Empty state', () => {
let wrapper;
- let store;
+ const config = {
+ noContainersImage: 'foo',
+ helpPagePath: 'baz',
+ };
beforeEach(() => {
- store = new Vuex.Store({
- state: {
- config: {
- noContainersImage: 'foo',
- helpPagePath: 'baz',
- },
- },
- });
wrapper = shallowMount(groupEmptyState, {
localVue,
- store,
stubs: {
GlEmptyState,
GlSprintf,
},
+ provide() {
+ return { config };
+ },
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
index 9f7a2758ae1..b9839d92f1d 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
@@ -11,13 +12,15 @@ import {
REMOVE_REPOSITORY_LABEL,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
+ IMAGE_DELETE_SCHEDULED_STATUS,
+ IMAGE_FAILED_DELETED_STATUS,
} from '~/registry/explorer/constants';
import { RouterLink } from '../../stubs';
import { imagesListResponse } from '../../mock_data';
describe('Image List Row', () => {
let wrapper;
- const item = imagesListResponse.data[0];
+ const [item] = imagesListResponse;
const findDetailsLink = () => wrapper.find('[data-testid="details-link"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
@@ -50,13 +53,15 @@ describe('Image List Row', () => {
describe('main tooltip', () => {
it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
mountComponent();
+
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
});
it('is disabled when item is being deleted', () => {
- mountComponent({ item: { ...item, deleting: true } });
+ mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
+
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false);
});
@@ -65,12 +70,13 @@ describe('Image List Row', () => {
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
+
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to')).toMatchObject({
name: 'details',
params: {
- id: item.id,
+ id: getIdFromGraphQLId(item.id),
},
});
});
@@ -85,16 +91,18 @@ describe('Image List Row', () => {
describe('warning icon', () => {
it.each`
- failedDelete | cleanup_policy_started_at | shown | title
- ${true} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
- ${false} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
- ${false} | ${false} | ${false} | ${''}
+ status | expirationPolicyStartedAt | shown | title
+ ${IMAGE_FAILED_DELETED_STATUS} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
+ ${''} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
+ ${''} | ${false} | ${false} | ${''}
`(
- 'when failedDelete is $failedDelete and cleanup_policy_started_at is $cleanup_policy_started_at',
- ({ cleanup_policy_started_at, failedDelete, shown, title }) => {
- mountComponent({ item: { ...item, failedDelete, cleanup_policy_started_at } });
+ 'when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt',
+ ({ expirationPolicyStartedAt, status, shown, title }) => {
+ mountComponent({ item: { ...item, status, expirationPolicyStartedAt } });
+
const icon = findWarningIcon();
expect(icon.exists()).toBe(shown);
+
if (shown) {
const tooltip = getBinding(icon.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(title);
@@ -112,30 +120,33 @@ describe('Image List Row', () => {
it('has the correct props', () => {
mountComponent();
- expect(findDeleteBtn().attributes()).toMatchObject({
+
+ expect(findDeleteBtn().props()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL,
- tooltipdisabled: `${Boolean(item.destroy_path)}`,
- tooltiptitle: LIST_DELETE_BUTTON_DISABLED,
+ tooltipDisabled: item.canDelete,
+ tooltipTitle: LIST_DELETE_BUTTON_DISABLED,
});
});
it('emits a delete event', () => {
mountComponent();
+
findDeleteBtn().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});
it.each`
- destroy_path | deleting | state
- ${null} | ${null} | ${'true'}
- ${null} | ${true} | ${'true'}
- ${'foo'} | ${true} | ${'true'}
- ${'foo'} | ${false} | ${undefined}
+ canDelete | status | state
+ ${false} | ${''} | ${true}
+ ${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
+ ${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
+ ${true} | ${''} | ${false}
`(
- 'disabled is $state when destroy_path is $destroy_path and deleting is $deleting',
- ({ destroy_path, deleting, state }) => {
- mountComponent({ item: { ...item, destroy_path, deleting } });
- expect(findDeleteBtn().attributes('disabled')).toBe(state);
+ 'disabled is $state when canDelete is $canDelete and status is $status',
+ ({ canDelete, status, state }) => {
+ mountComponent({ item: { ...item, canDelete, status } });
+
+ expect(findDeleteBtn().props('disabled')).toBe(state);
},
);
});
@@ -155,11 +166,13 @@ describe('Image List Row', () => {
describe('tags count text', () => {
it('with one tag in the image', () => {
- mountComponent({ item: { ...item, tags_count: 1 } });
+ mountComponent({ item: { ...item, tagsCount: 1 } });
+
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
});
it('with more than one tag in the image', () => {
- mountComponent({ item: { ...item, tags_count: 3 } });
+ mountComponent({ item: { ...item, tagsCount: 3 } });
+
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
index 03ba6ad7f80..54befc9973a 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
@@ -1,29 +1,25 @@
import { shallowMount } from '@vue/test-utils';
-import { GlPagination } from '@gitlab/ui';
+import { GlKeysetPagination } from '@gitlab/ui';
import Component from '~/registry/explorer/components/list_page/image_list.vue';
import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue';
-import { imagesListResponse, imagePagination } from '../../mock_data';
+import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data';
describe('Image List', () => {
let wrapper;
const findRow = () => wrapper.findAll(ImageListRow);
- const findPagination = () => wrapper.find(GlPagination);
+ const findPagination = () => wrapper.find(GlKeysetPagination);
- const mountComponent = () => {
+ const mountComponent = (pageInfo = defaultPageInfo) => {
wrapper = shallowMount(Component, {
propsData: {
- images: imagesListResponse.data,
- pagination: imagePagination,
+ images: imagesListResponse,
+ pageInfo,
},
});
};
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -31,10 +27,14 @@ describe('Image List', () => {
describe('list', () => {
it('contains one list element for each image', () => {
- expect(findRow().length).toBe(imagesListResponse.data.length);
+ mountComponent();
+
+ expect(findRow().length).toBe(imagesListResponse.length);
});
it('when delete event is emitted on the row it emits up a delete event', () => {
+ mountComponent();
+
findRow()
.at(0)
.vm.$emit('delete', 'foo');
@@ -44,19 +44,41 @@ describe('Image List', () => {
describe('pagination', () => {
it('exists', () => {
+ mountComponent();
+
expect(findPagination().exists()).toBe(true);
});
- it('is wired to the correct pagination props', () => {
- const pagination = findPagination();
- expect(pagination.props('perPage')).toBe(imagePagination.perPage);
- expect(pagination.props('totalItems')).toBe(imagePagination.total);
- expect(pagination.props('value')).toBe(imagePagination.page);
+ it.each`
+ hasNextPage | hasPreviousPage | isVisible
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ `(
+ 'when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible',
+ ({ hasNextPage, hasPreviousPage, isVisible }) => {
+ mountComponent({ hasNextPage, hasPreviousPage });
+
+ expect(findPagination().exists()).toBe(isVisible);
+ expect(findPagination().props('hasPreviousPage')).toBe(hasPreviousPage);
+ expect(findPagination().props('hasNextPage')).toBe(hasNextPage);
+ },
+ );
+
+ it('emits "prev-page" when the user clicks the back page button', () => {
+ mountComponent({ hasPreviousPage: true });
+
+ findPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
});
- it('emits a pageChange event when the page change', () => {
- findPagination().vm.$emit(GlPagination.model.event, 2);
- expect(wrapper.emitted('pageChange')).toEqual([[2]]);
+ it('emits "next-page" when the user clicks the forward page button', () => {
+ mountComponent({ hasNextPage: true });
+
+ findPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
index 73746c545cb..3a27cf1923c 100644
--- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
@@ -3,36 +3,35 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../../stubs';
import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
-import * as getters from '~/registry/explorer/stores/getters';
+import { dockerCommands } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Project Empty state', () => {
let wrapper;
- let store;
+ const config = {
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'bar',
+ helpPagePath: 'baz',
+ twoFactorAuthHelpLink: 'barBaz',
+ personalAccessTokensHelpLink: 'fooBaz',
+ noContainersImage: 'bazFoo',
+ };
beforeEach(() => {
- store = new Vuex.Store({
- state: {
- config: {
- repositoryUrl: 'foo',
- registryHostUrlWithPort: 'bar',
- helpPagePath: 'baz',
- twoFactorAuthHelpLink: 'barBaz',
- personalAccessTokensHelpLink: 'fooBaz',
- noContainersImage: 'bazFoo',
- },
- },
- getters,
- });
wrapper = shallowMount(projectEmptyState, {
localVue,
- store,
stubs: {
GlEmptyState,
GlSprintf,
},
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
});
});
diff --git a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
index d730bfcde24..fb0b98ba004 100644
--- a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
@@ -32,10 +32,6 @@ describe('Registry Breadcrumb', () => {
{ name: 'baz', meta: { nameGenerator } },
];
- const state = {
- imageDetails: { foo: 'bar' },
- };
-
const findDivider = () => wrapper.find('.js-divider');
const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' });
const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' });
@@ -56,9 +52,6 @@ describe('Registry Breadcrumb', () => {
routes,
},
},
- $store: {
- state,
- },
},
});
};
@@ -87,7 +80,6 @@ describe('Registry Breadcrumb', () => {
});
it('the link text is calculated by nameGenerator', () => {
- expect(nameGenerator).toHaveBeenCalledWith(state);
expect(nameGenerator).toHaveBeenCalledTimes(1);
});
});
@@ -111,7 +103,6 @@ describe('Registry Breadcrumb', () => {
});
it('the link text is calculated by nameGenerator', () => {
- expect(nameGenerator).toHaveBeenCalledWith(state);
expect(nameGenerator).toHaveBeenCalledTimes(2);
});
});
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index da5f1840b5c..992d880581a 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -1,110 +1,211 @@
-export const headers = {
- 'X-PER-PAGE': 5,
- 'X-PAGE': 1,
- 'X-TOTAL': 13,
- 'X-TOTAL_PAGES': 1,
- 'X-NEXT-PAGE': null,
- 'X-PREVIOUS-PAGE': null,
-};
-export const reposServerResponse = [
+export const imagesListResponse = [
{
- destroy_path: 'path',
- id: '123',
- location: 'location',
- path: 'foo',
- tags_path: 'tags_path',
+ __typename: 'ContainerRepository',
+ id: 'gid://gitlab/ContainerRepository/26',
+ name: 'rails-12009',
+ path: 'gitlab-org/gitlab-test/rails-12009',
+ status: null,
+ location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009',
+ canDelete: true,
+ createdAt: '2020-11-03T13:29:21Z',
+ tagsCount: 18,
+ expirationPolicyStartedAt: null,
},
{
- destroy_path: 'path_',
- id: '456',
- location: 'location_',
- path: 'bar',
- tags_path: 'tags_path_',
+ __typename: 'ContainerRepository',
+ id: 'gid://gitlab/ContainerRepository/11',
+ name: 'rails-20572',
+ path: 'gitlab-org/gitlab-test/rails-20572',
+ status: null,
+ location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572',
+ canDelete: true,
+ createdAt: '2020-09-21T06:57:43Z',
+ tagsCount: 1,
+ expirationPolicyStartedAt: null,
},
];
-export const registryServerResponse = [
- {
- name: 'centos7',
- short_revision: 'b118ab5b0',
- revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
- total_size: 679,
- layers: 19,
- location: 'location',
- created_at: 1505828744434,
- destroy_path: 'path_',
+export const pageInfo = {
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjI2In0',
+ endCursor: 'eyJpZCI6IjgifQ',
+ __typename: 'ContainerRepositoryConnection',
+};
+
+export const graphQLImageListMock = {
+ data: {
+ project: {
+ __typename: 'Project',
+ containerRepositoriesCount: 2,
+ containerRepositories: {
+ __typename: 'ContainerRepositoryConnection',
+ nodes: imagesListResponse,
+ pageInfo,
+ },
+ },
},
- {
- name: 'centos6',
- short_revision: 'b118ab5b0',
- revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
- total_size: 679,
- layers: 19,
- location: 'location',
- created_at: 1505828744434,
+};
+
+export const graphQLEmptyImageListMock = {
+ data: {
+ project: {
+ __typename: 'Project',
+ containerRepositoriesCount: 2,
+ containerRepositories: {
+ __typename: 'ContainerRepositoryConnection',
+ nodes: [],
+ pageInfo,
+ },
+ },
},
-];
+};
-export const imagesListResponse = {
- data: [
- {
- path: 'foo',
- location: 'location',
- destroy_path: 'path',
+export const graphQLEmptyGroupImageListMock = {
+ data: {
+ group: {
+ __typename: 'Group',
+ containerRepositoriesCount: 2,
+ containerRepositories: {
+ __typename: 'ContainerRepositoryConnection',
+ nodes: [],
+ pageInfo,
+ },
},
- {
- path: 'bar',
- location: 'location-2',
- destroy_path: 'path-2',
+ },
+};
+
+export const deletedContainerRepository = {
+ id: 'gid://gitlab/ContainerRepository/11',
+ status: 'DELETE_SCHEDULED',
+ path: 'gitlab-org/gitlab-test/rails-12009',
+ __typename: 'ContainerRepository',
+};
+
+export const graphQLImageDeleteMock = {
+ data: {
+ destroyContainerRepository: {
+ containerRepository: {
+ ...deletedContainerRepository,
+ },
+ errors: [],
+ __typename: 'DestroyContainerRepositoryPayload',
+ },
+ },
+};
+
+export const graphQLImageDeleteMockError = {
+ data: {
+ destroyContainerRepository: {
+ containerRepository: {
+ ...deletedContainerRepository,
+ },
+ errors: ['foo'],
+ __typename: 'DestroyContainerRepositoryPayload',
},
- ],
- headers,
+ },
+};
+
+export const containerRepositoryMock = {
+ id: 'gid://gitlab/ContainerRepository/26',
+ name: 'rails-12009',
+ path: 'gitlab-org/gitlab-test/rails-12009',
+ status: null,
+ location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
+ canDelete: true,
+ createdAt: '2020-11-03T13:29:21Z',
+ updatedAt: '2020-11-03T13:29:21Z',
+ tagsCount: 13,
+ expirationPolicyStartedAt: null,
+ project: {
+ visibility: 'public',
+ __typename: 'Project',
+ },
};
-export const tagsListResponse = {
- data: [
- {
- name: 'centos6',
- revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
- short_revision: 'b118ab5b0',
- size: 19,
- layers: 10,
- location: 'location',
- path: 'bar:centos6',
- created_at: '2020-06-29T10:23:51.766+00:00',
- destroy_path: 'path',
- digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c',
+export const tagsPageInfo = {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'MQ',
+ endCursor: 'MTA',
+};
+
+export const tagsMock = [
+ {
+ digest: 'sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062',
+ location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:beta-24753',
+ path: 'gitlab-org/gitlab-test/rails-12009:beta-24753',
+ name: 'beta-24753',
+ revision: 'c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b',
+ shortRevision: 'c2613843a',
+ createdAt: '2020-11-03T13:29:38+00:00',
+ totalSize: 105,
+ canDelete: true,
+ __typename: 'ContainerRepositoryTag',
+ },
+ {
+ digest: 'sha256:7f94f97dff89ffd122cafe50cd32329adf682356a7a96f69cbfe313ee589791c',
+ location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:beta-31075',
+ path: 'gitlab-org/gitlab-test/rails-12009:beta-31075',
+ name: 'beta-31075',
+ revision: 'df44e7228f0f255c73e35b6f0699624a615f42746e3e8e2e4b3804a6d6fc3292',
+ shortRevision: 'df44e7228',
+ createdAt: '2020-11-03T13:29:32+00:00',
+ totalSize: 104,
+ canDelete: true,
+ __typename: 'ContainerRepositoryTag',
+ },
+];
+
+export const graphQLImageDetailsMock = override => ({
+ data: {
+ containerRepository: {
+ ...containerRepositoryMock,
+
+ tags: {
+ nodes: tagsMock,
+ pageInfo: { ...tagsPageInfo },
+ __typename: 'ContainerRepositoryTagConnection',
+ },
+ __typename: 'ContainerRepositoryDetails',
+ ...override,
},
- {
- name: 'test-tag',
- revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
- short_revision: 'b969de599',
- size: 19,
- layers: 10,
- path: 'foo:test-tag',
- location: 'location-2',
- created_at: '2020-06-29T10:23:51.766+00:00',
- digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7736dfd5c',
+ },
+});
+
+export const graphQLImageDetailsEmptyTagsMock = {
+ data: {
+ containerRepository: {
+ ...containerRepositoryMock,
+ tags: {
+ nodes: [],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ __typename: 'ContainerRepositoryTagConnection',
+ },
+ __typename: 'ContainerRepositoryDetails',
},
- ],
- headers,
+ },
};
-export const imagePagination = {
- perPage: 10,
- page: 1,
- total: 14,
- totalPages: 2,
- nextPage: 2,
+export const graphQLDeleteImageRepositoryTagsMock = {
+ data: {
+ destroyContainerRepositoryTags: {
+ deletedTagNames: [],
+ errors: [],
+ __typename: 'DestroyContainerRepositoryTagsPayload',
+ },
+ },
};
-export const imageDetailsMock = {
- id: 1,
- name: 'rails-32309',
- path: 'gitlab-org/gitlab-test/rails-32309',
- project_id: 1,
- location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-32309',
- created_at: '2020-06-29T10:23:47.838Z',
- cleanup_policy_started_at: null,
- delete_api_path: 'http://0.0.0.0:3000/api/v4/projects/1/registry/repositories/1',
+export const dockerCommands = {
+ dockerBuildCommand: 'foofoo',
+ dockerPushCommand: 'barbar',
+ dockerLoginCommand: 'bazbaz',
};
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index c09b7e0c067..d307dfe590c 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -1,5 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlPagination } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlKeysetPagination } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/details.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
@@ -8,25 +11,28 @@ import DetailsHeader from '~/registry/explorer/components/details_page/details_h
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 getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+
import {
- SET_MAIN_LOADING,
- SET_TAGS_LIST_SUCCESS,
- SET_TAGS_PAGINATION,
- SET_INITIAL_STATE,
- SET_IMAGE_DETAILS,
-} from '~/registry/explorer/stores/mutation_types';
-
-import { tagsListResponse, imageDetailsMock } from '../mock_data';
+ graphQLImageDetailsMock,
+ graphQLImageDetailsEmptyTagsMock,
+ graphQLDeleteImageRepositoryTagsMock,
+ containerRepositoryMock,
+ tagsMock,
+ tagsPageInfo,
+} from '../mock_data';
import { DeleteModal } from '../stubs';
+const localVue = createLocalVue();
+
describe('Details Page', () => {
let wrapper;
- let dispatchSpy;
- let store;
+ let apolloProvider;
const findDeleteModal = () => wrapper.find(DeleteModal);
- const findPagination = () => wrapper.find(GlPagination);
+ const findPagination = () => wrapper.find(GlKeysetPagination);
const findTagsLoader = () => wrapper.find(TagsLoader);
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
@@ -36,15 +42,46 @@ describe('Details Page', () => {
const routeId = 1;
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ };
+
+ const cleanTags = tagsMock.map(t => {
+ const result = { ...t };
+ // eslint-disable-next-line no-underscore-dangle
+ delete result.__typename;
+ return result;
+ });
+
+ const waitForApolloRequestRender = async () => {
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ };
+
const tagsArrayToSelectedTags = tags =>
tags.reduce((acc, c) => {
acc[c.name] = true;
return acc;
}, {});
- const mountComponent = ({ options } = {}) => {
+ const mountComponent = ({
+ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
+ options,
+ config = {},
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getContainerRepositoryDetailsQuery, resolver],
+ [deleteContainerRepositoryTagsMutation, mutationResolver],
+ ];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMount(component, {
- store,
+ localVue,
+ apolloProvider,
stubs: {
DeleteModal,
},
@@ -55,17 +92,17 @@ describe('Details Page', () => {
},
},
},
+ provide() {
+ return {
+ breadCrumbState,
+ config,
+ };
+ },
...options,
});
};
beforeEach(() => {
- store = createStore();
- dispatchSpy = jest.spyOn(store, 'dispatch');
- dispatchSpy.mockResolvedValue();
- store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
- store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
- store.commit(SET_IMAGE_DETAILS, imageDetailsMock);
jest.spyOn(Tracking, 'event');
});
@@ -74,85 +111,90 @@ describe('Details Page', () => {
wrapper = null;
});
- describe('lifecycle events', () => {
- it('calls the appropriate action on mount', () => {
- mountComponent();
- expect(dispatchSpy).toHaveBeenCalledWith('requestImageDetailsAndTagsList', routeId);
- });
- });
-
describe('when isLoading is true', () => {
- beforeEach(() => {
- store.commit(SET_MAIN_LOADING, true);
+ it('shows the loader', () => {
mountComponent();
- });
-
- afterEach(() => store.commit(SET_MAIN_LOADING, false));
- it('shows the loader', () => {
expect(findTagsLoader().exists()).toBe(true);
});
it('does not show the list', () => {
+ mountComponent();
+
expect(findTagsList().exists()).toBe(false);
});
it('does not show pagination', () => {
+ mountComponent();
+
expect(findPagination().exists()).toBe(false);
});
});
describe('when the list of tags is empty', () => {
- beforeEach(() => {
- store.commit(SET_TAGS_LIST_SUCCESS, []);
- mountComponent();
- });
+ const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
+
+ it('has the empty state', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
- it('has the empty state', () => {
expect(findEmptyTagsState().exists()).toBe(true);
});
- it('does not show the loader', () => {
+ it('does not show the loader', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
expect(findTagsLoader().exists()).toBe(false);
});
- it('does not show the list', () => {
+ it('does not show the list', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
expect(findTagsList().exists()).toBe(false);
});
});
describe('list', () => {
- beforeEach(() => {
+ it('exists', async () => {
mountComponent();
- });
- it('exists', () => {
+ await waitForApolloRequestRender();
+
expect(findTagsList().exists()).toBe(true);
});
- it('has the correct props bound', () => {
+ it('has the correct props bound', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
expect(findTagsList().props()).toMatchObject({
isMobile: false,
- tags: store.state.tags,
+ tags: cleanTags,
});
});
describe('deleteEvent', () => {
describe('single item', () => {
let tagToBeDeleted;
- beforeEach(() => {
- [tagToBeDeleted] = store.state.tags;
+ beforeEach(async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ [tagToBeDeleted] = cleanTags;
findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true });
});
- it('open the modal', () => {
+ it('open the modal', async () => {
expect(DeleteModal.methods.show).toHaveBeenCalled();
});
- it('maps the selection to itemToBeDeleted', () => {
- expect(wrapper.vm.itemsToBeDeleted).toEqual([tagToBeDeleted]);
- });
-
it('tracks a single delete event', () => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
@@ -161,18 +203,18 @@ describe('Details Page', () => {
});
describe('multiple items', () => {
- beforeEach(() => {
- findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags));
+ beforeEach(async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(cleanTags));
});
it('open the modal', () => {
expect(DeleteModal.methods.show).toHaveBeenCalled();
});
- it('maps the selection to itemToBeDeleted', () => {
- expect(wrapper.vm.itemsToBeDeleted).toEqual(store.state.tags);
- });
-
it('tracks a single delete event', () => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'bulk_registry_tag_delete',
@@ -183,40 +225,77 @@ describe('Details Page', () => {
});
describe('pagination', () => {
- beforeEach(() => {
+ it('exists', async () => {
mountComponent();
- });
- it('exists', () => {
+ await waitForApolloRequestRender();
+
expect(findPagination().exists()).toBe(true);
});
- it('is wired to the correct pagination props', () => {
- const pagination = findPagination();
- expect(pagination.props('perPage')).toBe(store.state.tagsPagination.perPage);
- expect(pagination.props('totalItems')).toBe(store.state.tagsPagination.total);
- expect(pagination.props('value')).toBe(store.state.tagsPagination.page);
+ it('is hidden when there are no more pages', async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(findPagination().exists()).toBe(false);
});
- it('fetch the data from the API when the v-model changes', () => {
- dispatchSpy.mockResolvedValue();
- findPagination().vm.$emit(GlPagination.model.event, 2);
- expect(store.dispatch).toHaveBeenCalledWith('requestTagsList', {
- page: 2,
+ it('is wired to the correct pagination props', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findPagination().props()).toMatchObject({
+ hasNextPage: tagsPageInfo.hasNextPage,
+ hasPreviousPage: tagsPageInfo.hasPreviousPage,
});
});
+
+ it('fetch next page when user clicks next', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findPagination().vm.$emit('next');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: tagsPageInfo.endCursor }),
+ );
+ });
+
+ it('fetch previous page when user clicks prev', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findPagination().vm.$emit('prev');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }),
+ );
+ });
});
describe('modal', () => {
- it('exists', () => {
+ it('exists', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
+
expect(findDeleteModal().exists()).toBe(true);
});
describe('cancel event', () => {
- it('tracks cancel_delete', () => {
+ it('tracks cancel_delete', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
+
findDeleteModal().vm.$emit('cancel');
+
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
label: 'registry_tag_delete',
});
@@ -224,45 +303,62 @@ describe('Details Page', () => {
});
describe('confirmDelete event', () => {
+ let mutationResolver;
+
+ beforeEach(() => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
describe('when one item is selected to be deleted', () => {
- beforeEach(() => {
- mountComponent();
- findTagsList().vm.$emit('delete', { [store.state.tags[0].name]: true });
- });
+ it('calls apollo mutation with the right parameters', async () => {
+ findTagsList().vm.$emit('delete', { [cleanTags[0].name]: true });
+
+ await wrapper.vm.$nextTick();
- it('dispatch requestDeleteTag with the right parameters', () => {
findDeleteModal().vm.$emit('confirmDelete');
- expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', {
- tag: store.state.tags[0],
- });
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [cleanTags[0].name] }),
+ );
});
});
describe('when more than one item is selected to be deleted', () => {
- beforeEach(() => {
- mountComponent();
- findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags));
- });
+ it('calls apollo mutation with the right parameters', async () => {
+ findTagsList().vm.$emit('delete', { ...tagsArrayToSelectedTags(tagsMock) });
+
+ await wrapper.vm.$nextTick();
- it('dispatch requestDeleteTags with the right parameters', () => {
findDeleteModal().vm.$emit('confirmDelete');
- expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
- ids: store.state.tags.map(t => t.name),
- });
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: tagsMock.map(t => t.name) }),
+ );
});
});
});
});
describe('Header', () => {
- it('exists', () => {
+ it('exists', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
expect(findDetailsHeader().exists()).toBe(true);
});
- it('has the correct props', () => {
+ it('has the correct props', async () => {
mountComponent();
- expect(findDetailsHeader().props()).toEqual({ imageName: imageDetailsMock.name });
+
+ await waitForApolloRequestRender();
+ expect(findDetailsHeader().props('image')).toMatchObject({
+ name: containerRepositoryMock.name,
+ project: {
+ visibility: containerRepositoryMock.project.visibility,
+ },
+ });
});
});
@@ -273,20 +369,25 @@ describe('Details Page', () => {
};
const deleteAlertType = 'success_tag';
- it('exists', () => {
+ it('exists', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
expect(findDeleteAlert().exists()).toBe(true);
});
- it('has the correct props', () => {
- store.commit(SET_INITIAL_STATE, { ...config });
+ it('has the correct props', async () => {
mountComponent({
options: {
data: () => ({
deleteAlertType,
}),
},
+ config,
});
+
+ await waitForApolloRequestRender();
+
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
});
});
@@ -298,30 +399,38 @@ describe('Details Page', () => {
};
describe('when expiration_policy_started is not null', () => {
+ let resolver;
+
beforeEach(() => {
- store.commit(SET_IMAGE_DETAILS, {
- ...imageDetailsMock,
- cleanup_policy_started_at: Date.now().toString(),
- });
+ resolver = jest.fn().mockResolvedValue(
+ graphQLImageDetailsMock({
+ expirationPolicyStartedAt: Date.now().toString(),
+ }),
+ );
});
- it('exists', () => {
- mountComponent();
+ it('exists', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true);
});
- it('has the correct props', () => {
- store.commit(SET_INITIAL_STATE, { ...config });
+ it('has the correct props', async () => {
+ mountComponent({ resolver, config });
- mountComponent();
+ await waitForApolloRequestRender();
expect(findPartialCleanupAlert().props()).toEqual({ ...config });
});
it('dismiss hides the component', async () => {
- mountComponent();
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true);
+
findPartialCleanupAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
@@ -331,11 +440,22 @@ describe('Details Page', () => {
});
describe('when expiration_policy_started is null', () => {
- it('the component is hidden', () => {
+ it('the component is hidden', async () => {
mountComponent();
+ await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
});
+
+ describe('Breadcrumb connection', () => {
+ it('when the details are fetched updates the name', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
+ });
+ });
});
diff --git a/spec/frontend/registry/explorer/pages/index_spec.js b/spec/frontend/registry/explorer/pages/index_spec.js
index 1dc5376cacf..b5f718b3e61 100644
--- a/spec/frontend/registry/explorer/pages/index_spec.js
+++ b/spec/frontend/registry/explorer/pages/index_spec.js
@@ -1,16 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/pages/index.vue';
-import { createStore } from '~/registry/explorer/stores/';
describe('List Page', () => {
let wrapper;
- let store;
const findRouterView = () => wrapper.find({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
- store,
stubs: {
RouterView: true,
},
@@ -18,7 +15,6 @@ describe('List Page', () => {
};
beforeEach(() => {
- store = createStore();
mountComponent();
});
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index b24422adb03..7d32a667011 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -1,5 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
@@ -9,27 +11,35 @@ import ProjectEmptyState from '~/registry/explorer/components/list_page/project_
import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { createStore } from '~/registry/explorer/stores/';
-import {
- SET_MAIN_LOADING,
- SET_IMAGES_LIST_SUCCESS,
- SET_PAGINATION,
- SET_INITIAL_STATE,
-} from '~/registry/explorer/stores/mutation_types';
+
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
IMAGE_REPOSITORY_LIST_LABEL,
SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants';
-import { imagesListResponse } from '../mock_data';
+
+import getProjectContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql';
+import getGroupContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql';
+import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+
+import {
+ graphQLImageListMock,
+ graphQLImageDeleteMock,
+ deletedContainerRepository,
+ graphQLImageDeleteMockError,
+ graphQLEmptyImageListMock,
+ graphQLEmptyGroupImageListMock,
+ pageInfo,
+} from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks';
+const localVue = createLocalVue();
+
describe('List Page', () => {
let wrapper;
- let dispatchSpy;
- let store;
+ let apolloProvider;
const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
@@ -47,9 +57,31 @@ describe('List Page', () => {
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
- const mountComponent = ({ mocks } = {}) => {
+ const waitForApolloRequestRender = async () => {
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ };
+
+ const mountComponent = ({
+ mocks,
+ resolver = jest.fn().mockResolvedValue(graphQLImageListMock),
+ groupResolver = jest.fn().mockResolvedValue(graphQLImageListMock),
+ mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
+ config = {},
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getProjectContainerRepositoriesQuery, resolver],
+ [getGroupContainerRepositoriesQuery, groupResolver],
+ [deleteContainerRepositoryMutation, mutationResolver],
+ ];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMount(component, {
- store,
+ localVue,
+ apolloProvider,
stubs: {
GlModal,
GlEmptyState,
@@ -64,42 +96,27 @@ describe('List Page', () => {
},
...mocks,
},
+ provide() {
+ return {
+ config,
+ };
+ },
});
};
- beforeEach(() => {
- store = createStore();
- dispatchSpy = jest.spyOn(store, 'dispatch');
- dispatchSpy.mockResolvedValue();
- store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data);
- store.commit(SET_PAGINATION, imagesListResponse.headers);
- });
-
afterEach(() => {
wrapper.destroy();
});
- describe('API calls', () => {
- it.each`
- imageList | name | called
- ${[]} | ${'foo'} | ${['requestImagesList']}
- ${imagesListResponse.data} | ${undefined} | ${['requestImagesList']}
- ${imagesListResponse.data} | ${'foo'} | ${undefined}
- `(
- 'with images equal $imageList and name $name dispatch calls $called',
- ({ imageList, name, called }) => {
- store.commit(SET_IMAGES_LIST_SUCCESS, imageList);
- dispatchSpy.mockClear();
- mountComponent({ mocks: { $route: { name } } });
-
- expect(dispatchSpy.mock.calls[0]).toEqual(called);
- },
- );
- });
-
- it('contains registry header', () => {
+ it('contains registry header', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
+
expect(findRegistryHeader().exists()).toBe(true);
+ expect(findRegistryHeader().props()).toMatchObject({
+ imagesCount: 2,
+ });
});
describe('connection error', () => {
@@ -109,88 +126,100 @@ describe('List Page', () => {
helpPagePath: 'bar',
};
- beforeEach(() => {
- store.commit(SET_INITIAL_STATE, config);
- mountComponent();
- });
-
- afterEach(() => {
- store.commit(SET_INITIAL_STATE, {});
- });
-
it('should show an empty state', () => {
+ mountComponent({ config });
+
expect(findEmptyState().exists()).toBe(true);
});
it('empty state should have an svg-path', () => {
- expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
+ mountComponent({ config });
+
+ expect(findEmptyState().props('svgPath')).toBe(config.containersErrorImage);
});
it('empty state should have a description', () => {
- expect(findEmptyState().html()).toContain('connection error');
+ mountComponent({ config });
+
+ expect(findEmptyState().props('title')).toContain('connection error');
});
it('should not show the loading or default state', () => {
+ mountComponent({ config });
+
expect(findSkeletonLoader().exists()).toBe(false);
expect(findImageList().exists()).toBe(false);
});
});
describe('isLoading is true', () => {
- beforeEach(() => {
- store.commit(SET_MAIN_LOADING, true);
+ it('shows the skeleton loader', () => {
mountComponent();
- });
-
- afterEach(() => store.commit(SET_MAIN_LOADING, false));
- it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
it('imagesList is not visible', () => {
+ mountComponent();
+
expect(findImageList().exists()).toBe(false);
});
it('cli commands is not visible', () => {
+ mountComponent();
+
expect(findCliCommands().exists()).toBe(false);
});
});
describe('list is empty', () => {
- beforeEach(() => {
- store.commit(SET_IMAGES_LIST_SUCCESS, []);
- mountComponent();
- return waitForPromises();
- });
+ describe('project page', () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLEmptyImageListMock);
- it('cli commands is not visible', () => {
- expect(findCliCommands().exists()).toBe(false);
- });
+ it('cli commands is not visible', async () => {
+ mountComponent({ resolver });
- it('project empty state is visible', () => {
- expect(findProjectEmptyState().exists()).toBe(true);
- });
+ await waitForApolloRequestRender();
- describe('is group page is true', () => {
- beforeEach(() => {
- store.commit(SET_INITIAL_STATE, { isGroupPage: true });
- mountComponent();
+ expect(findCliCommands().exists()).toBe(false);
});
- afterEach(() => {
- store.commit(SET_INITIAL_STATE, { isGroupPage: undefined });
+ it('project empty state is visible', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findProjectEmptyState().exists()).toBe(true);
});
+ });
+ describe('group page', () => {
+ const groupResolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);
+
+ const config = {
+ isGroupPage: true,
+ };
+
+ it('group empty state is visible', async () => {
+ mountComponent({ groupResolver, config });
+
+ await waitForApolloRequestRender();
- it('group empty state is visible', () => {
expect(findGroupEmptyState().exists()).toBe(true);
});
- it('cli commands is not visible', () => {
+ it('cli commands is not visible', async () => {
+ mountComponent({ groupResolver, config });
+
+ await waitForApolloRequestRender();
+
expect(findCliCommands().exists()).toBe(false);
});
- it('list header is not visible', () => {
+ it('list header is not visible', async () => {
+ mountComponent({ groupResolver, config });
+
+ await waitForApolloRequestRender();
+
expect(findListHeader().exists()).toBe(false);
});
});
@@ -198,55 +227,91 @@ describe('List Page', () => {
describe('list is not empty', () => {
describe('unfiltered state', () => {
- beforeEach(() => {
+ it('quick start is visible', async () => {
mountComponent();
- });
- it('quick start is visible', () => {
+ await waitForApolloRequestRender();
+
expect(findCliCommands().exists()).toBe(true);
});
- it('list component is visible', () => {
+ it('list component is visible', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
expect(findImageList().exists()).toBe(true);
});
- it('list header is visible', () => {
+ it('list header is visible', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
const header = findListHeader();
expect(header.exists()).toBe(true);
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
});
describe('delete image', () => {
- const itemToDelete = { path: 'bar' };
- it('should call deleteItem when confirming deletion', () => {
- dispatchSpy.mockResolvedValue();
- findImageList().vm.$emit('delete', itemToDelete);
- expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
+ const deleteImage = async () => {
+ await wrapper.vm.$nextTick();
+
+ findImageList().vm.$emit('delete', deletedContainerRepository);
findDeleteModal().vm.$emit('ok');
- expect(store.dispatch).toHaveBeenCalledWith(
- 'requestDeleteImage',
- wrapper.vm.itemToDelete,
+
+ await waitForApolloRequestRender();
+ };
+
+ it('should call deleteItem when confirming deletion', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
+ mountComponent({ mutationResolver });
+
+ await deleteImage();
+
+ expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
+ expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
+
+ const updatedImage = findImageList()
+ .props('images')
+ .find(i => i.id === deletedContainerRepository.id);
+
+ expect(updatedImage.status).toBe(deletedContainerRepository.status);
+ });
+
+ it('should show a success alert when delete request is successful', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
+ mountComponent({ mutationResolver });
+
+ await deleteImage();
+
+ const alert = findDeleteAlert();
+ expect(alert.exists()).toBe(true);
+ expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
+ DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
);
});
- it('should show a success alert when delete request is successful', () => {
- dispatchSpy.mockResolvedValue();
- findImageList().vm.$emit('delete', itemToDelete);
- expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
- return wrapper.vm.handleDeleteImage().then(() => {
+ describe('when delete request fails it shows an alert', () => {
+ it('user recoverable error', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMockError);
+ mountComponent({ mutationResolver });
+
+ await deleteImage();
+
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
- DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
+ DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
);
});
- });
- it('should show an error alert when delete request fails', () => {
- dispatchSpy.mockRejectedValue();
- findImageList().vm.$emit('delete', itemToDelete);
- expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
- return wrapper.vm.handleDeleteImage().then(() => {
+ it('network error', async () => {
+ const mutationResolver = jest.fn().mockRejectedValue();
+ mountComponent({ mutationResolver });
+
+ await deleteImage();
+
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
@@ -258,38 +323,68 @@ describe('List Page', () => {
});
describe('search', () => {
- it('has a search box element', () => {
+ const doSearch = async () => {
+ await waitForApolloRequestRender();
+ findSearchBox().vm.$emit('submit', 'centos6');
+ await wrapper.vm.$nextTick();
+ };
+
+ it('has a search box element', async () => {
mountComponent();
+
+ await waitForApolloRequestRender();
+
const searchBox = findSearchBox();
expect(searchBox.exists()).toBe(true);
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
});
- it('performs a search', () => {
- mountComponent();
- findSearchBox().vm.$emit('submit', 'foo');
- expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
- name: 'foo',
- });
+ it('performs a search', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await doSearch();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ name: 'centos6' }));
});
- it('when search result is empty displays an empty search message', () => {
- mountComponent();
- store.commit(SET_IMAGES_LIST_SUCCESS, []);
- return wrapper.vm.$nextTick().then(() => {
- expect(findEmptySearchMessage().exists()).toBe(true);
- });
+ it('when search result is empty displays an empty search message', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ resolver.mockResolvedValue(graphQLEmptyImageListMock);
+
+ await doSearch();
+
+ expect(findEmptySearchMessage().exists()).toBe(true);
});
});
describe('pagination', () => {
- it('pageChange event triggers the appropriate store function', () => {
- mountComponent();
- findImageList().vm.$emit('pageChange', 2);
- expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
- pagination: { page: 2 },
- name: wrapper.vm.search,
- });
+ it('prev-page event triggers a fetchMore request', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findImageList().vm.$emit('prev-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ first: null, before: pageInfo.startCursor }),
+ );
+ });
+
+ it('next-page event triggers a fetchMore request', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findImageList().vm.$emit('next-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: pageInfo.endCursor }),
+ );
});
});
});
@@ -324,11 +419,11 @@ describe('List Page', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
- dispatchSpy.mockResolvedValue();
});
it('send an event when delete button is clicked', () => {
findImageList().vm.$emit('delete', {});
+
testTrackingCall('click_button');
});
diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js
deleted file mode 100644
index dcd4d8015a4..00000000000
--- a/spec/frontend/registry/explorer/stores/actions_spec.js
+++ /dev/null
@@ -1,362 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import createFlash from '~/flash';
-import Api from '~/api';
-import axios from '~/lib/utils/axios_utils';
-import * as actions from '~/registry/explorer/stores/actions';
-import * as types from '~/registry/explorer/stores/mutation_types';
-import { reposServerResponse, registryServerResponse } from '../mock_data';
-import * as utils from '~/registry/explorer/utils';
-import {
- FETCH_IMAGES_LIST_ERROR_MESSAGE,
- FETCH_TAGS_LIST_ERROR_MESSAGE,
- FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
-} from '~/registry/explorer/constants/index';
-
-jest.mock('~/flash.js');
-jest.mock('~/registry/explorer/utils');
-
-describe('Actions RegistryExplorer Store', () => {
- let mock;
- const endpoint = `${TEST_HOST}/endpoint.json`;
-
- const url = `${endpoint}/1}`;
- jest.spyOn(utils, 'pathGenerator').mockReturnValue(url);
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('sets initial state', done => {
- const initialState = {
- config: {
- endpoint,
- },
- };
-
- testAction(
- actions.setInitialState,
- initialState,
- null,
- [{ type: types.SET_INITIAL_STATE, payload: initialState }],
- [],
- done,
- );
- });
-
- it('setShowGarbageCollectionTip', done => {
- testAction(
- actions.setShowGarbageCollectionTip,
- true,
- null,
- [{ type: types.SET_SHOW_GARBAGE_COLLECTION_TIP, payload: true }],
- [],
- done,
- );
- });
-
- describe('receives api responses', () => {
- const response = {
- data: [1, 2, 3],
- headers: {
- page: 1,
- perPage: 10,
- },
- };
-
- it('images list response', done => {
- testAction(
- actions.receiveImagesListSuccess,
- response,
- null,
- [
- { type: types.SET_IMAGES_LIST_SUCCESS, payload: response.data },
- { type: types.SET_PAGINATION, payload: response.headers },
- ],
- [],
- done,
- );
- });
-
- it('tags list response', done => {
- testAction(
- actions.receiveTagsListSuccess,
- response,
- null,
- [
- { type: types.SET_TAGS_LIST_SUCCESS, payload: response.data },
- { type: types.SET_TAGS_PAGINATION, payload: response.headers },
- ],
- [],
- done,
- );
- });
- });
-
- describe('fetch images list', () => {
- it('sets the imagesList and pagination', done => {
- mock.onGet(endpoint).replyOnce(200, reposServerResponse, {});
-
- testAction(
- actions.requestImagesList,
- {},
- {
- config: {
- endpoint,
- },
- },
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [{ type: 'receiveImagesListSuccess', payload: { data: reposServerResponse, headers: {} } }],
- done,
- );
- });
-
- it('should create flash on error', done => {
- testAction(
- actions.requestImagesList,
- {},
- {
- config: {
- endpoint: null,
- },
- },
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- () => {
- expect(createFlash).toHaveBeenCalledWith({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
- done();
- },
- );
- });
- });
-
- describe('fetch tags list', () => {
- it('sets the tagsList', done => {
- mock.onGet(url).replyOnce(200, registryServerResponse, {});
-
- testAction(
- actions.requestTagsList,
- {},
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [
- {
- type: 'receiveTagsListSuccess',
- payload: { data: registryServerResponse, headers: {} },
- },
- ],
- done,
- );
- });
-
- it('should create flash on error', done => {
- testAction(
- actions.requestTagsList,
- {},
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- () => {
- expect(createFlash).toHaveBeenCalledWith({ message: FETCH_TAGS_LIST_ERROR_MESSAGE });
- done();
- },
- );
- });
- });
-
- describe('request delete single tag', () => {
- it('successfully performs the delete request', done => {
- const deletePath = 'delete/path';
- mock.onDelete(deletePath).replyOnce(200);
-
- testAction(
- actions.requestDeleteTag,
- {
- tag: {
- destroy_path: deletePath,
- },
- },
- {
- tagsPagination: {},
- },
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [
- {
- type: 'setShowGarbageCollectionTip',
- payload: true,
- },
- {
- type: 'requestTagsList',
- payload: {},
- },
- ],
- done,
- );
- });
-
- it('should turn off loading on error', done => {
- testAction(
- actions.requestDeleteTag,
- {
- tag: {
- destroy_path: null,
- },
- },
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- ).catch(() => done());
- });
- });
-
- describe('requestImageDetailsAndTagsList', () => {
- it('sets the imageDetails and dispatch requestTagsList', done => {
- const resolvedValue = { foo: 'bar' };
- jest.spyOn(Api, 'containerRegistryDetails').mockResolvedValue({ data: resolvedValue });
-
- testAction(
- actions.requestImageDetailsAndTagsList,
- 1,
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_IMAGE_DETAILS, payload: resolvedValue },
- ],
- [
- {
- type: 'requestTagsList',
- },
- ],
- done,
- );
- });
-
- it('should create flash on error', done => {
- jest.spyOn(Api, 'containerRegistryDetails').mockRejectedValue();
- testAction(
- actions.requestImageDetailsAndTagsList,
- 1,
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- () => {
- expect(createFlash).toHaveBeenCalledWith({ message: FETCH_IMAGE_DETAILS_ERROR_MESSAGE });
- done();
- },
- );
- });
- });
-
- describe('request delete multiple tags', () => {
- it('successfully performs the delete request', done => {
- mock.onDelete(url).replyOnce(200);
-
- testAction(
- actions.requestDeleteTags,
- {
- ids: [1, 2],
- },
- {
- tagsPagination: {},
- },
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [
- {
- type: 'setShowGarbageCollectionTip',
- payload: true,
- },
- {
- type: 'requestTagsList',
- payload: {},
- },
- ],
- done,
- );
- });
-
- it('should turn off loading on error', done => {
- mock.onDelete(url).replyOnce(500);
-
- testAction(
- actions.requestDeleteTags,
- {
- ids: [1, 2],
- },
- {
- tagsPagination: {},
- },
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- ).catch(() => done());
- });
- });
-
- describe('request delete single image', () => {
- const image = {
- destroy_path: 'delete/path',
- };
-
- it('successfully performs the delete request', done => {
- mock.onDelete(image.destroy_path).replyOnce(200);
-
- testAction(
- actions.requestDeleteImage,
- image,
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.UPDATE_IMAGE, payload: { ...image, deleting: true } },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- done,
- );
- });
-
- it('should turn off loading on error', done => {
- mock.onDelete(image.destroy_path).replyOnce(400);
- testAction(
- actions.requestDeleteImage,
- image,
- {},
- [
- { type: types.SET_MAIN_LOADING, payload: true },
- { type: types.SET_MAIN_LOADING, payload: false },
- ],
- [],
- ).catch(() => done());
- });
- });
-});
diff --git a/spec/frontend/registry/explorer/stores/getters_spec.js b/spec/frontend/registry/explorer/stores/getters_spec.js
deleted file mode 100644
index 4cab65d2bb0..00000000000
--- a/spec/frontend/registry/explorer/stores/getters_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as getters from '~/registry/explorer/stores/getters';
-
-describe('Getters RegistryExplorer store', () => {
- let state;
-
- describe.each`
- getter | prefix | configParameter | suffix
- ${'dockerBuildCommand'} | ${'docker build -t'} | ${'repositoryUrl'} | ${'.'}
- ${'dockerPushCommand'} | ${'docker push'} | ${'repositoryUrl'} | ${null}
- ${'dockerLoginCommand'} | ${'docker login'} | ${'registryHostUrlWithPort'} | ${null}
- `('$getter', ({ getter, prefix, configParameter, suffix }) => {
- beforeEach(() => {
- state = {
- config: { repositoryUrl: 'foo', registryHostUrlWithPort: 'bar' },
- };
- });
-
- it(`returns ${prefix} concatenated with ${configParameter} and optionally suffixed with ${suffix}`, () => {
- const expectedPieces = [prefix, state.config[configParameter], suffix].filter(p => p);
- expect(getters[getter](state)).toBe(expectedPieces.join(' '));
- });
- });
-
- describe('showGarbageCollection', () => {
- it.each`
- result | showGarbageCollectionTip | isAdmin
- ${true} | ${true} | ${true}
- ${false} | ${true} | ${false}
- ${false} | ${false} | ${true}
- `(
- 'return $result when showGarbageCollectionTip $showGarbageCollectionTip and isAdmin is $isAdmin',
- ({ result, showGarbageCollectionTip, isAdmin }) => {
- state = {
- config: { isAdmin },
- showGarbageCollectionTip,
- };
- expect(getters.showGarbageCollection(state)).toBe(result);
- },
- );
- });
-});
diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js
deleted file mode 100644
index 1908d3f0350..00000000000
--- a/spec/frontend/registry/explorer/stores/mutations_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import mutations from '~/registry/explorer/stores/mutations';
-import * as types from '~/registry/explorer/stores/mutation_types';
-
-describe('Mutations Registry Explorer Store', () => {
- let mockState;
-
- beforeEach(() => {
- mockState = {};
- });
-
- describe('SET_INITIAL_STATE', () => {
- it('should set the initial state', () => {
- const payload = {
- endpoint: 'foo',
- isGroupPage: '',
- expirationPolicy: { foo: 'bar' },
- isAdmin: '',
- };
- const expectedState = {
- ...mockState,
- config: { ...payload, isGroupPage: false, isAdmin: false },
- };
- mutations[types.SET_INITIAL_STATE](mockState, {
- ...payload,
- expirationPolicy: JSON.stringify(payload.expirationPolicy),
- });
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_IMAGES_LIST_SUCCESS', () => {
- it('should set the images list', () => {
- const images = [{ name: 'foo' }, { name: 'bar' }];
- const defaultStatus = { deleting: false, failedDelete: false };
- const expectedState = {
- ...mockState,
- images: [{ name: 'foo', ...defaultStatus }, { name: 'bar', ...defaultStatus }],
- };
- mutations[types.SET_IMAGES_LIST_SUCCESS](mockState, images);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('UPDATE_IMAGE', () => {
- it('should update an image', () => {
- mockState.images = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }];
- const payload = { id: 1, name: 'baz' };
- const expectedState = {
- ...mockState,
- images: [payload, { id: 2, name: 'bar' }],
- };
- mutations[types.UPDATE_IMAGE](mockState, payload);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_TAGS_LIST_SUCCESS', () => {
- it('should set the tags list', () => {
- const tags = [1, 2, 3];
- const expectedState = { ...mockState, tags };
- mutations[types.SET_TAGS_LIST_SUCCESS](mockState, tags);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_MAIN_LOADING', () => {
- it('should set the isLoading', () => {
- const expectedState = { ...mockState, isLoading: true };
- mutations[types.SET_MAIN_LOADING](mockState, true);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_SHOW_GARBAGE_COLLECTION_TIP', () => {
- it('should set the showGarbageCollectionTip', () => {
- const expectedState = { ...mockState, showGarbageCollectionTip: true };
- mutations[types.SET_SHOW_GARBAGE_COLLECTION_TIP](mockState, true);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_PAGINATION', () => {
- const generatePagination = () => [
- {
- 'X-PAGE': '1',
- 'X-PER-PAGE': '20',
- 'X-TOTAL': '100',
- 'X-TOTAL-PAGES': '5',
- 'X-NEXT-PAGE': '2',
- 'X-PREV-PAGE': '0',
- },
- {
- page: 1,
- perPage: 20,
- total: 100,
- totalPages: 5,
- nextPage: 2,
- previousPage: 0,
- },
- ];
-
- it('should set the images pagination', () => {
- const [headers, expectedResult] = generatePagination();
- const expectedState = { ...mockState, pagination: expectedResult };
- mutations[types.SET_PAGINATION](mockState, headers);
-
- expect(mockState).toEqual(expectedState);
- });
-
- it('should set the tags pagination', () => {
- const [headers, expectedResult] = generatePagination();
- const expectedState = { ...mockState, tagsPagination: expectedResult };
- mutations[types.SET_TAGS_PAGINATION](mockState, headers);
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('SET_IMAGE_DETAILS', () => {
- it('should set imageDetails', () => {
- const expectedState = { ...mockState, imageDetails: { foo: 'bar' } };
- mutations[types.SET_IMAGE_DETAILS](mockState, { foo: 'bar' });
-
- expect(mockState).toEqual(expectedState);
- });
- });
-});
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js
index b6c0ee67757..d6fba863ee0 100644
--- a/spec/frontend/registry/explorer/stubs.js
+++ b/spec/frontend/registry/explorer/stubs.js
@@ -1,35 +1,33 @@
+import {
+ GlModal as RealGlModal,
+ GlEmptyState as RealGlEmptyState,
+ GlSkeletonLoader as RealGlSkeletonLoader,
+} from '@gitlab/ui';
+import { RouterLinkStub } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
-export const GlModal = {
+export const GlModal = stubComponent(RealGlModal, {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
methods: {
show: jest.fn(),
},
-};
+});
-export const GlEmptyState = {
+export const GlEmptyState = stubComponent(RealGlEmptyState, {
template: '<div><slot name="description"></slot></div>',
- name: 'GlEmptyStateSTub',
-};
+});
-export const RouterLink = {
- template: `<div><slot></slot></div>`,
- props: ['to'],
-};
+export const RouterLink = RouterLinkStub;
-export const DeleteModal = {
- template: '<div></div>',
+export const DeleteModal = stubComponent(RealDeleteModal, {
methods: {
show: jest.fn(),
},
- props: RealDeleteModal.props,
-};
+});
-export const GlSkeletonLoader = {
- template: `<div><slot></slot></div>`,
- props: ['width', 'height'],
-};
+export const GlSkeletonLoader = stubComponent(RealGlSkeletonLoader);
export const ListItem = {
...RealListItem,
diff --git a/spec/frontend/registry/explorer/utils_spec.js b/spec/frontend/registry/explorer/utils_spec.js
deleted file mode 100644
index 7a5d6958a09..00000000000
--- a/spec/frontend/registry/explorer/utils_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { pathGenerator } from '~/registry/explorer/utils';
-
-describe('Utils', () => {
- describe('pathGenerator', () => {
- const imageDetails = {
- path: 'foo/bar/baz',
- name: 'baz',
- id: 1,
- };
-
- beforeEach(() => {
- window.gon.relative_url_root = null;
- });
-
- it('returns the fetch url when no ending is passed', () => {
- expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json');
- });
-
- it('returns the url with an ending when is passed', () => {
- expect(pathGenerator(imageDetails, '/foo')).toBe('/foo/bar/registry/repository/1/tags/foo');
- });
-
- describe.each`
- path | name | result
- ${'foo/foo'} | ${''} | ${'/foo/foo/registry/repository/1/tags?format=json'}
- ${'foo/foo/foo'} | ${'foo'} | ${'/foo/foo/registry/repository/1/tags?format=json'}
- ${'baz/foo/foo/foo'} | ${'foo'} | ${'/baz/foo/foo/registry/repository/1/tags?format=json'}
- ${'baz/foo/foo/foo'} | ${'foo'} | ${'/baz/foo/foo/registry/repository/1/tags?format=json'}
- ${'foo/foo/baz/foo/foo'} | ${'foo/foo'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'}
- ${'foo/foo/baz/foo/bar'} | ${'foo/bar'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'}
- ${'baz/foo/foo'} | ${'foo'} | ${'/baz/foo/registry/repository/1/tags?format=json'}
- ${'baz/foo/bar'} | ${'foo'} | ${'/baz/foo/bar/registry/repository/1/tags?format=json'}
- `('when path is $path and name is $name', ({ name, path, result }) => {
- it('returns the correct value', () => {
- expect(pathGenerator({ id: 1, name, path })).toBe(result);
- });
-
- it('produces a correct relative url', () => {
- window.gon.relative_url_root = '/gitlab';
- expect(pathGenerator({ id: 1, name, path })).toBe(`/gitlab${result}`);
- });
- });
-
- it('returns the url unchanged when imageDetails have no name', () => {
- const imageDetailsWithoutName = {
- path: 'foo/bar/baz',
- name: '',
- id: 1,
- };
-
- expect(pathGenerator(imageDetailsWithoutName)).toBe(
- '/foo/bar/baz/registry/repository/1/tags?format=json',
- );
- });
- });
-});
diff --git a/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap b/spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap
index 032007bba51..7062773b46b 100644
--- a/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap
@@ -76,25 +76,25 @@ Array [
Object {
"default": false,
"key": "SEVEN_DAYS",
- "label": "7 days until tags are automatically removed",
+ "label": "7 days",
"variable": 7,
},
Object {
"default": false,
"key": "FOURTEEN_DAYS",
- "label": "14 days until tags are automatically removed",
+ "label": "14 days",
"variable": 14,
},
Object {
"default": false,
"key": "THIRTY_DAYS",
- "label": "30 days until tags are automatically removed",
+ "label": "30 days",
"variable": 30,
},
Object {
"default": true,
"key": "NINETY_DAYS",
- "label": "90 days until tags are automatically removed",
+ "label": "90 days",
"variable": 90,
},
]
diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
new file mode 100644
index 00000000000..d7f89ce070e
--- /dev/null
+++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Settings Form Cadence matches snapshot 1`] = `
+<expiration-dropdown-stub
+ class="gl-mr-7 gl-mb-0!"
+ data-testid="cadence-dropdown"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
+ label="Run cleanup:"
+ name="cadence"
+ value="EVERY_DAY"
+/>
+`;
+
+exports[`Settings Form Enable matches snapshot 1`] = `
+<expiration-toggle-stub
+ class="gl-mb-0!"
+ data-testid="enable-toggle"
+ value="true"
+/>
+`;
+
+exports[`Settings Form Keep N matches snapshot 1`] = `
+<expiration-dropdown-stub
+ data-testid="keep-n-dropdown"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
+ label="Keep the most recent:"
+ name="keep-n"
+ value="TEN_TAGS"
+/>
+`;
+
+exports[`Settings Form Keep Regex matches snapshot 1`] = `
+<expiration-input-stub
+ data-testid="keep-regex-input"
+ description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
+ error=""
+ label="Keep tags matching:"
+ name="keep-regex"
+ placeholder=""
+ value="sss"
+/>
+`;
+
+exports[`Settings Form OlderThan matches snapshot 1`] = `
+<expiration-dropdown-stub
+ data-testid="older-than-dropdown"
+ formoptions="[object Object],[object Object],[object Object],[object Object]"
+ label="Remove tags older than:"
+ name="older-than"
+ value="FOURTEEN_DAYS"
+/>
+`;
+
+exports[`Settings Form Remove regex matches snapshot 1`] = `
+<expiration-input-stub
+ data-testid="remove-regex-input"
+ description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
+ error=""
+ label="Remove tags matching:"
+ name="remove-regex"
+ placeholder=".*"
+ value="asdasdssssdfdf"
+/>
+`;
diff --git a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js
new file mode 100644
index 00000000000..e0cac317ad6
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js
@@ -0,0 +1,83 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_dropdown.vue';
+
+describe('ExpirationDropdown', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ formOptions: [{ key: 'foo', label: 'bar' }, { key: 'baz', label: 'zab' }],
+ };
+
+ const findFormSelect = () => wrapper.find(GlFormSelect);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findOptions = () => wrapper.findAll('[data-testid="option"]');
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a form-select component', () => {
+ mountComponent();
+ expect(findFormSelect().exists()).toBe(true);
+ });
+
+ it('has the correct options', () => {
+ mountComponent();
+
+ expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
+ });
+ });
+
+ describe('model', () => {
+ it('assign the right props to the form-select component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findFormSelect().props()).toMatchObject({
+ value,
+ disabled,
+ });
+ expect(findFormSelect().attributes('id')).toBe(defaultProps.name);
+ });
+
+ it('assign the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ id: `${defaultProps.name}-form-group`,
+ 'label-for': defaultProps.name,
+ label: defaultProps.label,
+ });
+ });
+
+ it('emits input event when form-select emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findFormSelect().vm.$emit('input', emittedValue);
+
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_input_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js
new file mode 100644
index 00000000000..849f85aa265
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_input_spec.js
@@ -0,0 +1,169 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_input.vue';
+import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
+
+describe('ExpirationInput', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ placeholder: 'placeholder-baz',
+ description: '%{linkStart}description-foo%{linkEnd}',
+ };
+
+ const tagsRegexHelpPagePath = 'fooPath';
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findLabel = () => wrapper.find('[data-testid="label"]');
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+ const findDescriptionLink = () => wrapper.find(GlLink);
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlSprintf,
+ GlFormGroup,
+ },
+ provide: {
+ tagsRegexHelpPagePath,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a label', () => {
+ mountComponent();
+
+ expect(findLabel().text()).toBe(defaultProps.label);
+ });
+
+ it('has a textarea component', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(defaultProps.description);
+ });
+
+ it('has a description link', () => {
+ mountComponent();
+
+ const link = findDescriptionLink();
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(tagsRegexHelpPagePath);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the textarea component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findInput().attributes()).toMatchObject({
+ id: defaultProps.name,
+ value,
+ placeholder: defaultProps.placeholder,
+ disabled: `${disabled}`,
+ trim: '',
+ });
+ });
+
+ it('emits input event when textarea emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findInput().vm.$emit('input', emittedValue);
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+
+ describe('regex textarea validation', () => {
+ const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
+
+ describe('when error contains an error message', () => {
+ const errorMessage = 'something went wrong';
+
+ it('shows the error message on the relevant field', () => {
+ mountComponent({ error: errorMessage });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+
+ it('gives precedence to API errors compared to local ones', () => {
+ mountComponent({
+ error: errorMessage,
+ value: invalidString,
+ });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+ });
+
+ describe('when error is empty', () => {
+ describe('if the user did not type', () => {
+ it('validation is not emitted', () => {
+ mountComponent();
+
+ expect(wrapper.emitted('validation')).toBeUndefined();
+ });
+
+ it('no error message is shown', () => {
+ mountComponent();
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findFormGroup().attributes('invalid-feedback')).toBe('');
+ });
+ });
+
+ describe('when the user typed something', () => {
+ describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
+ beforeEach(() => {
+ // since the component has no state we both emit the event and set the prop
+ mountComponent({ value: invalidString });
+
+ findInput().vm.$emit('input', invalidString);
+ });
+
+ it('textAreaValidation state is false', () => {
+ expect(findFormGroup().props('state')).toBe(false);
+ expect(findInput().attributes('state')).toBeUndefined();
+ });
+
+ it('emits the @validation event with false payload', () => {
+ expect(wrapper.emitted('validation')).toEqual([[false]]);
+ });
+ });
+
+ it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => {
+ mountComponent();
+
+ findInput().vm.$emit('input', 'foo');
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findInput().attributes('state')).toBe('true');
+ expect(wrapper.emitted('validation')).toEqual([[true]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
new file mode 100644
index 00000000000..c594b1f449d
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
@@ -0,0 +1,62 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_run_text.vue';
+import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+ const value = 'foo';
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has an input component', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: NEXT_CLEANUP_LABEL,
+ });
+ });
+ });
+
+ describe('formattedValue', () => {
+ it.each`
+ valueProp | enabled | expected
+ ${value} | ${true} | ${value}
+ ${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT}
+ `(
+ 'when value is $valueProp and enabled is $enabled the input value is $expected',
+ ({ valueProp, enabled, expected }) => {
+ mountComponent({ value: valueProp, enabled });
+
+ expect(findInput().attributes('value')).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
new file mode 100644
index 00000000000..99ff7a7f77a
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlToggle, GlSprintf } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_toggle.vue';
+import {
+ ENABLED_TOGGLE_DESCRIPTION,
+ DISABLED_TOGGLE_DESCRIPTION,
+} from '~/registry/settings/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find(GlToggle);
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlSprintf,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a toggle component', () => {
+ mountComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().exists()).toBe(true);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the toggle component', () => {
+ mountComponent({ value: true, disabled: true });
+
+ expect(findToggle().props()).toMatchObject({
+ value: true,
+ disabled: true,
+ });
+ });
+
+ it('emits input event when toggle is updated', () => {
+ mountComponent();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(wrapper.emitted('input')).toEqual([[false]]);
+ });
+ });
+
+ describe('toggle description', () => {
+ it('says enabled when the toggle is on', () => {
+ mountComponent({ value: true });
+
+ expect(findDescription().text()).toMatchInterpolatedText(ENABLED_TOGGLE_DESCRIPTION);
+ });
+
+ it('says disabled when the toggle is off', () => {
+ mountComponent({ value: false });
+
+ expect(findDescription().text()).toMatchInterpolatedText(DISABLED_TOGGLE_DESCRIPTION);
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
index a784396f47a..c31c7bdf99b 100644
--- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
@@ -3,15 +3,19 @@ import VueApollo from 'vue-apollo';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import component from '~/registry/settings/components/registry_settings_app.vue';
-import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
+import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from '~/registry/settings/components/settings_form.vue';
-import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/shared/constants';
import {
+ FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/registry/settings/constants';
-import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
+import {
+ expirationPolicyPayload,
+ emptyExpirationPolicyPayload,
+ containerExpirationPolicyData,
+} from '../mock_data';
const localVue = createLocalVue();
@@ -62,6 +66,29 @@ describe('Registry Settings App', () => {
wrapper.destroy();
});
+ describe('isEdited status', () => {
+ it.each`
+ description | apiResponse | workingCopy | result
+ ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
+ ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
+ ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
+ ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
+ ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
+ `('$description', async ({ apiResponse, workingCopy, result }) => {
+ const requests = mountComponentWithApollo({
+ provide: { ...defaultProvidedValues, enableHistoricEntries: true },
+ resolver: jest.fn().mockResolvedValue(apiResponse),
+ });
+ await Promise.all(requests);
+
+ findSettingsComponent().vm.$emit('input', workingCopy);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findSettingsComponent().props('isEdited')).toBe(result);
+ });
+ });
+
it('renders the setting form', async () => {
const requests = mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index 4346cfadcc8..b89269c0ae4 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -4,13 +4,12 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
-import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
-import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
-import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
+import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
+import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
-} from '~/registry/shared/constants';
+} from '~/registry/settings/constants';
import { GlCard, GlLoadingIcon } from '../../shared/stubs';
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
@@ -39,9 +38,15 @@ describe('Settings Form', () => {
};
const findForm = () => wrapper.find({ ref: 'form-element' });
- const findFields = () => wrapper.find(expirationPolicyFields);
- const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
- const findSaveButton = () => wrapper.find({ ref: 'save-button' });
+
+ const findCancelButton = () => wrapper.find('[data-testid="cancel-button"');
+ const findSaveButton = () => wrapper.find('[data-testid="save-button"');
+ const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]');
+ const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]');
+ const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]');
+ const findKeepRegexInput = () => wrapper.find('[data-testid="keep-regex-input"]');
+ const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
+ const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]');
const mountComponent = ({
props = defaultProps,
@@ -109,45 +114,136 @@ describe('Settings Form', () => {
wrapper.destroy();
});
- describe('data binding', () => {
- it('v-model change update the settings property', () => {
+ describe.each`
+ model | finder | fieldName | type | defaultValue
+ ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
+ ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
+ ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
+ ${'nameRegexKeep'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''}
+ ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
+ ${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''}
+ `('$fieldName', ({ model, finder, type, defaultValue }) => {
+ it('matches snapshot', () => {
mountComponent();
- findFields().vm.$emit('input', { newValue: 'foo' });
- expect(wrapper.emitted('input')).toEqual([['foo']]);
+
+ expect(finder().element).toMatchSnapshot();
});
- it('v-model change update the api error property', () => {
- const apiErrors = { baz: 'bar' };
- mountComponent({ data: { apiErrors } });
- expect(findFields().props('apiErrors')).toEqual(apiErrors);
- findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' });
- expect(findFields().props('apiErrors')).toEqual({});
+ it('input event triggers a model update', () => {
+ mountComponent();
+
+ finder().vm.$emit('input', 'foo');
+ expect(wrapper.emitted('input')[0][0]).toMatchObject({
+ [model]: 'foo',
+ });
});
it('shows the default option when none are selected', () => {
mountComponent({ props: { value: {} } });
- expect(findFields().props('value')).toEqual({
- cadence: 'EVERY_DAY',
- keepN: 'TEN_TAGS',
- olderThan: 'NINETY_DAYS',
- });
+ expect(finder().props('value')).toEqual(defaultValue);
});
+
+ if (type !== 'toggle') {
+ it.each`
+ isLoading | mutationLoading | enabledValue
+ ${false} | ${false} | ${false}
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${false}
+ `(
+ 'is disabled when is loading is $isLoading, mutationLoading is $mutationLoading and enabled is $enabledValue',
+ ({ isLoading, mutationLoading, enabledValue }) => {
+ mountComponent({
+ props: { isLoading, value: { enabled: enabledValue } },
+ data: { mutationLoading },
+ });
+ expect(finder().props('disabled')).toEqual(true);
+ },
+ );
+ } else {
+ it.each`
+ isLoading | mutationLoading
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${true}
+ `(
+ 'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading',
+ ({ isLoading, mutationLoading }) => {
+ mountComponent({
+ props: { isLoading, value: {} },
+ data: { mutationLoading },
+ });
+ expect(finder().props('disabled')).toEqual(true);
+ },
+ );
+ }
+
+ if (type === 'textarea') {
+ it('input event updates the api error property', async () => {
+ const apiErrors = { [model]: 'bar' };
+ mountComponent({ data: { apiErrors } });
+
+ finder().vm.$emit('input', 'foo');
+ expect(finder().props('error')).toEqual('bar');
+
+ await wrapper.vm.$nextTick();
+
+ expect(finder().props('error')).toEqual('');
+ });
+
+ it('validation event updates buttons disabled state', async () => {
+ mountComponent();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+
+ finder().vm.$emit('validation', false);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+ }
+
+ if (type === 'dropdown') {
+ it('has the correct formOptions', () => {
+ mountComponent();
+ expect(finder().props('formOptions')).toEqual(wrapper.vm.$options.formOptions[model]);
+ });
+ }
});
describe('form', () => {
describe('form reset event', () => {
- beforeEach(() => {
+ it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('reset');
- });
- it('calls the appropriate function', () => {
+
expect(wrapper.emitted('reset')).toEqual([[]]);
});
it('tracks the reset event', () => {
+ mountComponent();
+
+ findForm().trigger('reset');
+
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
});
+
+ it('resets the errors objects', async () => {
+ mountComponent({
+ data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } },
+ });
+
+ findForm().trigger('reset');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findKeepRegexInput().props('error')).toBe('');
+ expect(findRemoveRegexInput().props('error')).toBe('');
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
});
describe('form submit event ', () => {
@@ -209,6 +305,7 @@ describe('Settings Form', () => {
});
});
});
+
describe('global errors', () => {
it('shows an error', async () => {
const handlers = mountComponentWithApollo({
@@ -230,7 +327,7 @@ describe('Settings Form', () => {
graphQLErrors: [
{
extensions: {
- problems: [{ path: ['name'], message: 'baz' }],
+ problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
},
},
],
@@ -241,7 +338,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
- expect(findFields().props('apiErrors')).toEqual({ name: 'baz' });
+ expect(findKeepRegexInput().props('error')).toEqual('baz');
});
});
});
@@ -257,23 +354,21 @@ describe('Settings Form', () => {
});
it.each`
- isLoading | isEdited | mutationLoading | isDisabled
- ${true} | ${true} | ${true} | ${true}
- ${false} | ${true} | ${true} | ${true}
- ${false} | ${false} | ${true} | ${true}
- ${true} | ${false} | ${false} | ${true}
- ${false} | ${false} | ${false} | ${true}
- ${false} | ${true} | ${false} | ${false}
+ isLoading | isEdited | mutationLoading
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${true} | ${false} | ${false}
+ ${false} | ${false} | ${false}
`(
- 'when isLoading is $isLoading and isEdited is $isEdited and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
- ({ isEdited, isLoading, mutationLoading, isDisabled }) => {
+ 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled',
+ ({ isEdited, isLoading, mutationLoading }) => {
mountComponent({
props: { ...defaultProps, isEdited, isLoading },
data: { mutationLoading },
});
- const expectation = isDisabled ? 'true' : undefined;
- expect(findCancelButton().attributes('disabled')).toBe(expectation);
+ expect(findCancelButton().props('disabled')).toBe(true);
},
);
});
@@ -284,24 +379,24 @@ describe('Settings Form', () => {
expect(findSaveButton().attributes('type')).toBe('submit');
});
+
it.each`
- isLoading | fieldsAreValid | mutationLoading | isDisabled
- ${true} | ${true} | ${true} | ${true}
- ${false} | ${true} | ${true} | ${true}
- ${false} | ${false} | ${true} | ${true}
- ${true} | ${false} | ${false} | ${true}
- ${false} | ${false} | ${false} | ${true}
- ${false} | ${true} | ${false} | ${false}
+ isLoading | localErrors | mutationLoading
+ ${true} | ${{}} | ${true}
+ ${true} | ${{}} | ${false}
+ ${false} | ${{}} | ${true}
+ ${false} | ${{ foo: false }} | ${true}
+ ${true} | ${{ foo: false }} | ${false}
+ ${false} | ${{ foo: false }} | ${false}
`(
- 'when isLoading is $isLoading and fieldsAreValid is $fieldsAreValid and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
- ({ fieldsAreValid, isLoading, mutationLoading, isDisabled }) => {
+ 'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled',
+ ({ localErrors, isLoading, mutationLoading }) => {
mountComponent({
props: { ...defaultProps, isLoading },
- data: { mutationLoading, fieldsAreValid },
+ data: { mutationLoading, localErrors },
});
- const expectation = isDisabled ? 'true' : undefined;
- expect(findSaveButton().attributes('disabled')).toBe(expectation);
+ expect(findSaveButton().props('disabled')).toBe(true);
},
);
diff --git a/spec/frontend/registry/settings/graphql/cache_updated_spec.js b/spec/frontend/registry/settings/graphql/cache_updated_spec.js
index e5f69a08285..d88a5576f26 100644
--- a/spec/frontend/registry/settings/graphql/cache_updated_spec.js
+++ b/spec/frontend/registry/settings/graphql/cache_updated_spec.js
@@ -1,5 +1,5 @@
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
-import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
+import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
describe('Registry settings cache update', () => {
let client;
diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js
index 7f3772ce7fe..7cc645fcf55 100644
--- a/spec/frontend/registry/settings/mock_data.js
+++ b/spec/frontend/registry/settings/mock_data.js
@@ -1,13 +1,18 @@
+export const containerExpirationPolicyData = () => ({
+ cadence: 'EVERY_DAY',
+ enabled: true,
+ keepN: 'TEN_TAGS',
+ nameRegex: 'asdasdssssdfdf',
+ nameRegexKeep: 'sss',
+ olderThan: 'FOURTEEN_DAYS',
+ nextRunAt: '2020-11-19T07:37:03.941Z',
+});
+
export const expirationPolicyPayload = override => ({
data: {
project: {
containerExpirationPolicy: {
- cadence: 'EVERY_DAY',
- enabled: true,
- keepN: 'TEN_TAGS',
- nameRegex: 'asdasdssssdfdf',
- nameRegexKeep: 'sss',
- olderThan: 'FOURTEEN_DAYS',
+ ...containerExpirationPolicyData(),
...override,
},
},
@@ -26,12 +31,7 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {})
data: {
updateContainerExpirationPolicy: {
containerExpirationPolicy: {
- cadence: 'EVERY_DAY',
- enabled: true,
- keepN: 'TEN_TAGS',
- nameRegex: 'asdasdssssdfdf',
- nameRegexKeep: 'sss',
- olderThan: 'FOURTEEN_DAYS',
+ ...containerExpirationPolicyData(),
...override,
},
errors,
diff --git a/spec/frontend/registry/shared/utils_spec.js b/spec/frontend/registry/settings/utils_spec.js
index edb0c3261be..f92d51db307 100644
--- a/spec/frontend/registry/shared/utils_spec.js
+++ b/spec/frontend/registry/settings/utils_spec.js
@@ -2,7 +2,7 @@ import {
formOptionsGenerator,
optionLabelGenerator,
olderThanTranslationGenerator,
-} from '~/registry/shared/utils';
+} from '~/registry/settings/utils';
describe('Utils', () => {
describe('optionLabelGenerator', () => {
@@ -11,10 +11,7 @@ describe('Utils', () => {
[{ variable: 1 }, { variable: 2 }],
olderThanTranslationGenerator,
);
- expect(result).toEqual([
- { variable: 1, label: '1 day until tags are automatically removed' },
- { variable: 2, label: '2 days until tags are automatically removed' },
- ]);
+ expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]);
});
});
diff --git a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
deleted file mode 100644
index 2ceb2655d40..00000000000
--- a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
+++ /dev/null
@@ -1,148 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Expiration Policy Form renders 1`] = `
-<div
- class="gl-line-height-20"
->
- <gl-form-group-stub
- id="expiration-policy-toggle-group"
- label="Cleanup policy:"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-toggle"
- >
- <div
- class="gl-display-flex"
- >
- <gl-toggle-stub
- id="expiration-policy-toggle"
- labelposition="top"
- />
-
- <span
- class="gl-mb-3 gl-ml-3 gl-line-height-20"
- >
- <strong>
- Disabled
- </strong>
- - Tags matching the patterns defined below will be scheduled for deletion
- </span>
- </div>
- </gl-form-group-stub>
-
- <gl-form-group-stub
- id="expiration-policy-interval-group"
- label="Expiration interval:"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-interval"
- >
- <gl-form-select-stub
- disabled="true"
- id="expiration-policy-interval"
- >
- <option
- value="foo"
- >
-
- Foo
-
- </option>
- <option
- value="bar"
- >
-
- Bar
-
- </option>
- </gl-form-select-stub>
- </gl-form-group-stub>
- <gl-form-group-stub
- id="expiration-policy-schedule-group"
- label="Expiration schedule:"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-schedule"
- >
- <gl-form-select-stub
- disabled="true"
- id="expiration-policy-schedule"
- >
- <option
- value="foo"
- >
-
- Foo
-
- </option>
- <option
- value="bar"
- >
-
- Bar
-
- </option>
- </gl-form-select-stub>
- </gl-form-group-stub>
- <gl-form-group-stub
- id="expiration-policy-latest-group"
- label="Number of tags to retain:"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-latest"
- >
- <gl-form-select-stub
- disabled="true"
- id="expiration-policy-latest"
- >
- <option
- value="foo"
- >
-
- Foo
-
- </option>
- <option
- value="bar"
- >
-
- Bar
-
- </option>
- </gl-form-select-stub>
- </gl-form-group-stub>
-
- <gl-form-group-stub
- id="expiration-policy-name-matching-group"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-name-matching"
- >
-
- <gl-form-textarea-stub
- disabled="true"
- id="expiration-policy-name-matching"
- noresize="true"
- placeholder=""
- trim=""
- value=""
- />
- </gl-form-group-stub>
- <gl-form-group-stub
- id="expiration-policy-keep-name-group"
- label-align="right"
- label-cols="3"
- label-for="expiration-policy-keep-name"
- >
-
- <gl-form-textarea-stub
- disabled="true"
- id="expiration-policy-keep-name"
- noresize="true"
- placeholder=""
- trim=""
- value=""
- />
- </gl-form-group-stub>
-</div>
-`;
diff --git a/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js b/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js
deleted file mode 100644
index bee9bca5369..00000000000
--- a/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
-import component from '~/registry/shared/components/expiration_policy_fields.vue';
-
-import { NAME_REGEX_LENGTH, ENABLED_TEXT, DISABLED_TEXT } from '~/registry/shared/constants';
-import { formOptions } from '../mock_data';
-
-describe('Expiration Policy Form', () => {
- let wrapper;
-
- const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
-
- const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
- const findFormElements = (name, parent = wrapper) =>
- parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
-
- const mountComponent = props => {
- wrapper = shallowMount(component, {
- stubs: {
- GlSprintf,
- },
- propsData: {
- formOptions,
- ...props,
- },
- methods: {
- // override idGenerator to avoid having to test with dynamic uid
- idGenerator: value => value,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders', () => {
- mountComponent();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe.each`
- elementName | modelName | value | disabledByToggle
- ${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
- ${'interval'} | ${'olderThan'} | ${'foo'} | ${'disabled'}
- ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
- ${'latest'} | ${'keepN'} | ${'foo'} | ${'disabled'}
- ${'name-matching'} | ${'nameRegex'} | ${'foo'} | ${'disabled'}
- ${'keep-name'} | ${'nameRegexKeep'} | ${'bar'} | ${'disabled'}
- `(
- `${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
- ({ elementName, modelName, value, disabledByToggle }) => {
- it(`${elementName} form group exist in the dom`, () => {
- mountComponent();
- const formGroup = findFormGroup(elementName);
- expect(formGroup.exists()).toBe(true);
- });
-
- it(`${elementName} form group has a label-for property`, () => {
- mountComponent();
- const formGroup = findFormGroup(elementName);
- expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
- });
-
- it(`${elementName} form group has a label-cols property`, () => {
- mountComponent({ labelCols: '1' });
- const formGroup = findFormGroup(elementName);
- return wrapper.vm.$nextTick().then(() => {
- expect(formGroup.attributes('label-cols')).toBe('1');
- });
- });
-
- it(`${elementName} form group has a label-align property`, () => {
- mountComponent({ labelAlign: 'foo' });
- const formGroup = findFormGroup(elementName);
- return wrapper.vm.$nextTick().then(() => {
- expect(formGroup.attributes('label-align')).toBe('foo');
- });
- });
-
- it(`${elementName} form group contains an input element`, () => {
- mountComponent();
- const formGroup = findFormGroup(elementName);
- expect(findFormElements(elementName, formGroup).exists()).toBe(true);
- });
-
- it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
- mountComponent();
- const formGroup = findFormGroup(elementName);
- const element = findFormElements(elementName, formGroup);
-
- const modelUpdateEvent = element.vm.$options.model
- ? element.vm.$options.model.event
- : 'input';
- element.vm.$emit(modelUpdateEvent, value);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('input')).toEqual([
- [{ newValue: { [modelName]: value }, modified: modelName }],
- ]);
- });
- });
-
- it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
- mountComponent({ settings: { enabled: false } });
- const formGroup = findFormGroup(elementName);
- const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
- expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
- });
- },
- );
-
- describe('when isLoading is true', () => {
- beforeEach(() => {
- mountComponent({ isLoading: true });
- });
-
- it.each`
- elementName
- ${'toggle'}
- ${'interval'}
- ${'schedule'}
- ${'latest'}
- ${'name-matching'}
- ${'keep-name'}
- `(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
- expect(findFormElements(elementName).attributes('disabled')).toBe('true');
- });
- });
-
- describe.each`
- modelName | elementName
- ${'nameRegex'} | ${'name-matching'}
- ${'nameRegexKeep'} | ${'keep-name'}
- `('regex textarea validation', ({ modelName, elementName }) => {
- const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
-
- describe('when apiError contains an error message', () => {
- const errorMessage = 'something went wrong';
-
- it('shows the error message on the relevant field', () => {
- mountComponent({ apiErrors: { [modelName]: errorMessage } });
- expect(findFormGroup(elementName).attributes('invalid-feedback')).toBe(errorMessage);
- });
-
- it('gives precedence to API errors compared to local ones', () => {
- mountComponent({
- apiErrors: { [modelName]: errorMessage },
- value: { [modelName]: invalidString },
- });
- expect(findFormGroup(elementName).attributes('invalid-feedback')).toBe(errorMessage);
- });
- });
-
- describe('when apiErrors is empty', () => {
- it('if the user did not type validation is null', async () => {
- mountComponent({ value: { [modelName]: '' } });
- expect(findFormGroup(elementName).attributes('state')).toBeUndefined();
- expect(wrapper.emitted('validated')).toBeTruthy();
- });
-
- it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => {
- mountComponent({ value: { [modelName]: 'foo' } });
-
- const formGroup = findFormGroup(elementName);
- const formElement = findFormElements(elementName, formGroup);
- expect(formGroup.attributes('state')).toBeTruthy();
- expect(formElement.attributes('state')).toBeTruthy();
- });
-
- describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
- beforeEach(() => {
- mountComponent({ value: { [modelName]: invalidString } });
- });
-
- it('textAreaValidation state is false', () => {
- expect(findFormGroup(elementName).attributes('state')).toBeUndefined();
- // we are forced to check the model attribute because falsy attrs are all casted to undefined in attrs
- // while in this case false shows an error and null instead shows nothing.
- expect(wrapper.vm.textAreaValidation[modelName].state).toBe(false);
- });
-
- it('emit the @invalidated event', () => {
- expect(wrapper.emitted('invalidated')).toBeTruthy();
- });
- });
- });
- });
-
- describe('help text', () => {
- it('toggleDescriptionText show disabled when settings.enabled is false', () => {
- mountComponent();
- const toggleHelpText = findFormGroup('toggle').find('span');
- expect(toggleHelpText.html()).toContain(DISABLED_TEXT);
- });
-
- it('toggleDescriptionText show enabled when settings.enabled is true', () => {
- mountComponent({ value: { enabled: true } });
- const toggleHelpText = findFormGroup('toggle').find('span');
- expect(toggleHelpText.html()).toContain(ENABLED_TEXT);
- });
- });
-});
diff --git a/spec/frontend/registry/shared/mock_data.js b/spec/frontend/registry/shared/mock_data.js
deleted file mode 100644
index 411363c2c95..00000000000
--- a/spec/frontend/registry/shared/mock_data.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export const options = [{ key: 'foo', label: 'Foo' }, { key: 'bar', label: 'Bar', default: true }];
-export const stringifiedOptions = JSON.stringify(options);
-export const stringifiedFormOptions = {
- cadenceOptions: stringifiedOptions,
- keepNOptions: stringifiedOptions,
- olderThanOptions: stringifiedOptions,
-};
-export const formOptions = {
- cadence: options,
- keepN: options,
- olderThan: options,
-};
diff --git a/spec/frontend/registry/shared/stubs.js b/spec/frontend/registry/shared/stubs.js
index f6b88d70e49..ad41eb42df4 100644
--- a/spec/frontend/registry/shared/stubs.js
+++ b/spec/frontend/registry/shared/stubs.js
@@ -9,3 +9,23 @@ export const GlCard = {
</div>
`,
};
+
+export const GlFormGroup = {
+ name: 'gl-form-group-stub',
+ props: ['state'],
+ template: `
+ <div>
+ <slot name="label"></slot>
+ <slot></slot>
+ <slot name="description"></slot>
+ </div>`,
+};
+
+export const GlFormSelect = {
+ name: 'gl-form-select-stub',
+ props: ['disabled', 'value'],
+ template: `
+ <div>
+ <slot></slot>
+ </div>`,
+};