summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js9
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js68
-rw-r--r--spec/frontend/ide/stores/integration_spec.js15
-rw-r--r--spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap21
-rw-r--r--spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap119
-rw-r--r--spec/frontend/registry/explorer/components/group_empty_state_spec.js40
-rw-r--r--spec/frontend/registry/explorer/components/project_empty_state_spec.js44
-rw-r--r--spec/frontend/registry/explorer/mock_data.js51
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js293
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js205
-rw-r--r--spec/frontend/registry/explorer/stores/actions_spec.js9
-rw-r--r--spec/frontend/registry/explorer/stores/mutations_spec.js5
-rw-r--r--spec/frontend/registry/explorer/stubs.js11
13 files changed, 805 insertions, 85 deletions
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 5e29a76f804..2230e0880bb 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -1,11 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions';
-import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
-jest.mock('~/flash');
jest.mock('~/code_navigation/utils');
describe('Code navigation actions', () => {
@@ -25,13 +23,6 @@ describe('Code navigation actions', () => {
describe('requestDataError', () => {
it('commits REQUEST_DATA_ERROR', () =>
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []));
-
- it('creates a flash message', () =>
- testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then(
- () => {
- expect(createFlash).toHaveBeenCalled();
- },
- ));
});
describe('fetchData', () => {
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index dd729651a61..6df963b0d55 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -534,27 +534,21 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
- it('adds a newline to the end of the file if it doesnt already exist', done => {
- callAction('content')
- .then(() => {
- expect(tmpFile.content).toBe('content\n');
-
- done();
+ it('adds file into stagedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
})
- .catch(done.fail);
- });
-
- it('adds file into changedFiles array', done => {
- callAction()
.then(() => {
- expect(store.state.changedFiles.length).toBe(1);
+ expect(store.state.stagedFiles.length).toBe(1);
done();
})
.catch(done.fail);
});
- it('adds file not more than once into changedFiles array', done => {
+ it('adds file not more than once into stagedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
@@ -567,7 +561,7 @@ describe('IDE store file actions', () => {
}),
)
.then(() => {
- expect(store.state.changedFiles.length).toBe(1);
+ expect(store.state.stagedFiles.length).toBe(1);
done();
})
@@ -594,52 +588,6 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
- describe('when `gon.feature.stageAllByDefault` is true', () => {
- const originalGonFeatures = Object.assign({}, gon.features);
-
- beforeAll(() => {
- gon.features = { stageAllByDefault: true };
- });
-
- afterAll(() => {
- gon.features = originalGonFeatures;
- });
-
- it('adds file into stagedFiles array', done => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content',
- })
- .then(() => {
- expect(store.state.stagedFiles.length).toBe(1);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('adds file not more than once into stagedFiles array', done => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content',
- })
- .then(() =>
- store.dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content 123',
- }),
- )
- .then(() => {
- expect(store.state.stagedFiles.length).toBe(1);
-
- done();
- })
- .catch(done.fail);
- });
- });
-
it('bursts unused seal', done => {
store
.dispatch('changeFileContent', {
diff --git a/spec/frontend/ide/stores/integration_spec.js b/spec/frontend/ide/stores/integration_spec.js
index 443de18f288..f95f036f572 100644
--- a/spec/frontend/ide/stores/integration_spec.js
+++ b/spec/frontend/ide/stores/integration_spec.js
@@ -61,19 +61,14 @@ describe('IDE store integration', () => {
store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' });
});
- it('has changed and staged', () => {
- expect(store.state.changedFiles).toEqual([
- expect.objectContaining({
- path: TEST_PATH,
- tempFile: true,
- deleted: false,
- }),
- ]);
-
+ it('is added to staged as modified', () => {
expect(store.state.stagedFiles).toEqual([
expect.objectContaining({
path: TEST_PATH,
- deleted: true,
+ deleted: false,
+ staged: true,
+ changed: true,
+ tempFile: false,
}),
]);
});
diff --git a/spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 00000000000..3761369c944
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Group Empty state to match the default snapshot 1`] = `
+<div
+ class="container-message"
+ svg-path="foo"
+ title="There are no container images available in this group"
+>
+ <p
+ class="js-no-container-images-text"
+ >
+ 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
+ href="baz"
+ target="_blank"
+ >
+ More Information
+ </gl-link-stub>
+ </p>
+</div>
+`;
diff --git a/spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap
new file mode 100644
index 00000000000..19767aefd1a
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap
@@ -0,0 +1,119 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Project Empty state to match the default snapshot 1`] = `
+<div
+ class="container-message"
+ svg-path="bazFoo"
+ title="There are no container images stored for this project"
+>
+ <p
+ class="js-no-container-images-text"
+ >
+ With the Container Registry, every project can have its own space to store its Docker images.
+ <gl-link-stub
+ href="baz"
+ target="_blank"
+ >
+ More Information
+ </gl-link-stub>
+ </p>
+
+ <h5>
+ Quick Start
+ </h5>
+
+ <p
+ class="js-not-logged-in-to-registry-text"
+ >
+ If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
+ <gl-link-stub
+ href="barBaz"
+ target="_blank"
+ >
+ Two-Factor Authentication
+ </gl-link-stub>
+ enabled, use a
+ <gl-link-stub
+ href="fooBaz"
+ target="_blank"
+ >
+ Personal Access Token
+ </gl-link-stub>
+ instead of a password.
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <clipboard-button-stub
+ class="input-group-text"
+ cssclass="btn-default"
+ text="docker login bar"
+ title="Copy login command"
+ tooltipplacement="top"
+ />
+ </span>
+ </div>
+
+ <p />
+
+ <p>
+
+ You can add an image to this registry with the following commands:
+
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <clipboard-button-stub
+ class="input-group-text"
+ cssclass="btn-default"
+ text="docker build -t foo ."
+ title="Copy build command"
+ tooltipplacement="top"
+ />
+ </span>
+ </div>
+
+ <div
+ class="input-group"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <clipboard-button-stub
+ class="input-group-text"
+ cssclass="btn-default"
+ text="docker push foo"
+ title="Copy push command"
+ tooltipplacement="top"
+ />
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/registry/explorer/components/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/group_empty_state_spec.js
new file mode 100644
index 00000000000..1b4de534317
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/group_empty_state_spec.js
@@ -0,0 +1,40 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import { GlEmptyState } from '../stubs';
+import groupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Registry Group Empty state', () => {
+ let wrapper;
+ let store;
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ config: {
+ noContainersImage: 'foo',
+ helpPagePath: 'baz',
+ },
+ },
+ });
+ wrapper = shallowMount(groupEmptyState, {
+ localVue,
+ store,
+ stubs: {
+ GlEmptyState,
+ GlSprintf,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/project_empty_state_spec.js
new file mode 100644
index 00000000000..8d4b6ca60a2
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/project_empty_state_spec.js
@@ -0,0 +1,44 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import { GlEmptyState } from '../stubs';
+import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Registry Project Empty state', () => {
+ let wrapper;
+ let store;
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ config: {
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'bar',
+ helpPagePath: 'baz',
+ twoFactorAuthHelpLink: 'barBaz',
+ personalAccessTokensHelpLink: 'fooBaz',
+ noContainersImage: 'bazFoo',
+ },
+ },
+ });
+ wrapper = shallowMount(projectEmptyState, {
+ localVue,
+ store,
+ stubs: {
+ GlEmptyState,
+ GlSprintf,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index 309e2ecd9bd..2d8cd4e42bc 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -1,3 +1,11 @@
+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 = [
{
destroy_path: 'path',
@@ -36,3 +44,46 @@ export const registryServerResponse = [
created_at: 1505828744434,
},
];
+
+export const imagesListResponse = {
+ data: [
+ {
+ path: 'foo',
+ location: 'location',
+ destroy_path: 'path',
+ },
+ {
+ path: 'bar',
+ location: 'location-2',
+ destroy_path: 'path-2',
+ },
+ ],
+ headers,
+};
+
+export const tagsListResponse = {
+ data: [
+ {
+ tag: 'centos6',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ short_revision: 'b118ab5b0',
+ size: 19,
+ layers: 10,
+ location: 'location',
+ path: 'bar',
+ created_at: 1505828744434,
+ destroy_path: 'path',
+ },
+ {
+ tag: 'test-image',
+ revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
+ short_revision: 'b969de599',
+ size: 19,
+ layers: 10,
+ path: 'foo',
+ location: 'location-2',
+ created_at: 1505828744434,
+ },
+ ],
+ headers,
+};
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
new file mode 100644
index 00000000000..205fb0e33dc
--- /dev/null
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -0,0 +1,293 @@
+import { mount } from '@vue/test-utils';
+import { GlTable, GlPagination, GlLoadingIcon } from '@gitlab/ui';
+import Tracking from '~/tracking';
+import stubChildren from 'helpers/stub_children';
+import component from '~/registry/explorer/pages/details.vue';
+import store from '~/registry/explorer/stores/';
+import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
+import { tagsListResponse } from '../mock_data';
+import { GlModal } from '../stubs';
+
+describe('Details Page', () => {
+ let wrapper;
+ let dispatchSpy;
+
+ const findDeleteModal = () => wrapper.find(GlModal);
+ const findPagination = () => wrapper.find(GlPagination);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findTagsTable = () => wrapper.find(GlTable);
+ const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
+ const findFirstRowItem = ref => wrapper.find({ ref });
+ const findBulkDeleteButton = () => wrapper.find({ ref: 'bulkDeleteButton' });
+ // findAll and refs seems to no work falling back to class
+ const findAllDeleteButtons = () => wrapper.findAll('.js-delete-registry');
+ const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
+ const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
+
+ const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
+
+ beforeEach(() => {
+ wrapper = mount(component, {
+ store,
+ stubs: {
+ ...stubChildren(component),
+ GlModal,
+ GlSprintf: false,
+ GlTable: false,
+ },
+ mocks: {
+ $route: {
+ params: {
+ id: routeId,
+ },
+ },
+ },
+ });
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ store.dispatch('receiveTagsListSuccess', tagsListResponse);
+ jest.spyOn(Tracking, 'event');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when isLoading is true', () => {
+ beforeAll(() => store.commit(SET_MAIN_LOADING, true));
+
+ afterAll(() => store.commit(SET_MAIN_LOADING, false));
+
+ it('has a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not have a main content', () => {
+ expect(findTagsTable().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(false);
+ expect(findDeleteModal().exists()).toBe(false);
+ });
+ });
+
+ describe('table', () => {
+ it.each([
+ 'rowCheckbox',
+ 'rowName',
+ 'rowShortRevision',
+ 'rowSize',
+ 'rowTime',
+ 'singleDeleteButton',
+ ])('%s exist in the table', element => {
+ expect(findFirstRowItem(element).exists()).toBe(true);
+ });
+
+ describe('header checkbox', () => {
+ it('exists', () => {
+ expect(findMainCheckbox().exists()).toBe(true);
+ });
+
+ it('if selected set selectedItem and allSelected', () => {
+ findMainCheckbox().vm.$emit('change');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findMainCheckbox().attributes('checked')).toBeTruthy();
+ expect(findCheckedCheckboxes()).toHaveLength(store.state.tags.length);
+ });
+ });
+
+ it('if deselect unset selectedItem and allSelected', () => {
+ wrapper.setData({ selectedItems: [1, 2], selectAllChecked: true });
+ findMainCheckbox().vm.$emit('change');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findMainCheckbox().attributes('checked')).toBe(undefined);
+ expect(findCheckedCheckboxes()).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('row checkbox', () => {
+ it('if selected adds item to selectedItems', () => {
+ findFirstRowItem('rowCheckbox').vm.$emit('change');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.selectedItems).toEqual([1]);
+ expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy();
+ });
+ });
+
+ it('if deselect remove index from selectedItems', () => {
+ wrapper.setData({ selectedItems: [1] });
+ findFirstRowItem('rowCheckbox').vm.$emit('change');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.selectedItems.length).toBe(0);
+ expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('header delete button', () => {
+ it('exists', () => {
+ expect(findBulkDeleteButton().exists()).toBe(true);
+ });
+
+ it('is disabled if no item is selected', () => {
+ expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
+ });
+
+ it('is enabled if at least one item is selected', () => {
+ wrapper.setData({ selectedItems: [1] });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
+ });
+ });
+
+ describe('on click', () => {
+ it('when one item is selected', () => {
+ wrapper.setData({ selectedItems: [1] });
+ findBulkDeleteButton().vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteModal().html()).toContain(
+ 'You are about to remove <b>foo</b>. Are you sure?',
+ );
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+
+ it('when multiple items are selected', () => {
+ wrapper.setData({ selectedItems: [0, 1] });
+ findBulkDeleteButton().vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteModal().html()).toContain(
+ 'You are about to remove <b>2</b> tags. Are you sure?',
+ );
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'bulk_registry_tag_delete',
+ });
+ });
+ });
+ });
+ });
+
+ describe('row delete button', () => {
+ it('exists', () => {
+ expect(
+ findAllDeleteButtons()
+ .at(0)
+ .exists(),
+ ).toBe(true);
+ });
+
+ it('is disabled if the item has no destroy_path', () => {
+ expect(
+ findAllDeleteButtons()
+ .at(1)
+ .attributes('disabled'),
+ ).toBe('true');
+ });
+
+ it('on click', () => {
+ findAllDeleteButtons()
+ .at(0)
+ .vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteModal().html()).toContain(
+ 'You are about to remove <b>bar</b>. Are you sure?',
+ );
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ it('exists', () => {
+ 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('fetch the data from the API when the v-model changes', () => {
+ dispatchSpy.mockResolvedValue();
+ wrapper.setData({ currentPage: 2 });
+ expect(store.dispatch).toHaveBeenCalledWith('requestTagsList', {
+ id: wrapper.vm.$route.params.id,
+ pagination: { page: 2 },
+ });
+ });
+ });
+
+ describe('modal', () => {
+ it('exists', () => {
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+
+ describe('when ok event is emitted', () => {
+ beforeEach(() => {
+ dispatchSpy.mockResolvedValue();
+ });
+
+ it('tracks confirm_delete', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'confirm_delete', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+
+ it('when only one element is selected', () => {
+ const deleteModal = findDeleteModal();
+
+ wrapper.setData({ itemsToBeDeleted: [0] });
+ deleteModal.vm.$emit('ok');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
+ tag: store.state.tags[0],
+ imageId: wrapper.vm.$route.params.id,
+ });
+ // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
+ expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
+ expect(findCheckedCheckboxes()).toHaveLength(0);
+ });
+ });
+
+ it('when multiple elements are selected', () => {
+ const deleteModal = findDeleteModal();
+
+ wrapper.setData({ itemsToBeDeleted: [0, 1] });
+ deleteModal.vm.$emit('ok');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
+ ids: store.state.tags.map(t => t.name),
+ imageId: wrapper.vm.$route.params.id,
+ });
+ // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
+ expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
+ expect(findCheckedCheckboxes()).toHaveLength(0);
+ });
+ });
+ });
+
+ it('tracks cancel_delete when cancel event is emitted', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
new file mode 100644
index 00000000000..f463dc49035
--- /dev/null
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -0,0 +1,205 @@
+import VueRouter from 'vue-router';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import Tracking from '~/tracking';
+import component from '~/registry/explorer/pages/list.vue';
+import store from '~/registry/explorer/stores/';
+import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
+import { imagesListResponse } from '../mock_data';
+import { GlModal, GlEmptyState } from '../stubs';
+
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+
+describe('List Page', () => {
+ let wrapper;
+ let dispatchSpy;
+
+ const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
+ const findDeleteModal = () => wrapper.find(GlModal);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findImagesList = () => wrapper.find({ ref: 'imagesList' });
+ const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
+ const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
+ const findPagination = () => wrapper.find(GlPagination);
+
+ beforeEach(() => {
+ wrapper = shallowMount(component, {
+ localVue,
+ store,
+ stubs: {
+ GlModal,
+ GlEmptyState,
+ GlSprintf,
+ },
+ });
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ store.dispatch('receiveImagesListSuccess', imagesListResponse);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('connection error', () => {
+ const config = {
+ characterError: true,
+ containersErrorImage: 'foo',
+ helpPagePath: 'bar',
+ };
+
+ beforeAll(() => {
+ store.dispatch('setInitialState', config);
+ });
+
+ afterAll(() => {
+ store.dispatch('setInitialState', {});
+ });
+
+ it('should show an empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('empty state should have an svg-path', () => {
+ expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
+ });
+
+ it('empty state should have a description', () => {
+ expect(findEmptyState().html()).toContain('connection error');
+ });
+
+ it('should not show the loading or default state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findImagesList().exists()).toBe(false);
+ });
+ });
+
+ describe('when isLoading is true', () => {
+ beforeAll(() => store.commit(SET_MAIN_LOADING, true));
+
+ afterAll(() => store.commit(SET_MAIN_LOADING, false));
+
+ it('shows the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('imagesList is not visible', () => {
+ expect(findImagesList().exists()).toBe(false);
+ });
+ });
+
+ describe('list', () => {
+ describe('listElement', () => {
+ let listElements;
+ let firstElement;
+
+ beforeEach(() => {
+ listElements = findRowItems();
+ [firstElement] = store.state.images;
+ });
+
+ it('contains one list element for each image', () => {
+ expect(listElements.length).toBe(store.state.images.length);
+ });
+
+ it('contains a link to the details page', () => {
+ const link = findDetailsLink();
+ expect(link.html()).toContain(firstElement.path);
+ expect(link.props('to').name).toBe('details');
+ });
+
+ it('contains a clipboard button', () => {
+ const button = findClipboardButton();
+ expect(button.exists()).toBe(true);
+ expect(button.props('text')).toBe(firstElement.location);
+ expect(button.props('title')).toBe(firstElement.location);
+ });
+
+ describe('delete image', () => {
+ it('should be possible to delete a repo', () => {
+ const deleteBtn = findDeleteBtn();
+ expect(deleteBtn.exists()).toBe(true);
+ });
+
+ it('should call deleteItem when confirming deletion', () => {
+ dispatchSpy.mockResolvedValue();
+ const itemToDelete = wrapper.vm.images[0];
+ wrapper.setData({ itemToDelete });
+ findDeleteModal().vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'requestDeleteImage',
+ itemToDelete.destroy_path,
+ );
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ it('exists', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('is wired to the correct pagination props', () => {
+ const pagination = findPagination();
+ expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
+ expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
+ expect(pagination.props('value')).toBe(store.state.pagination.page);
+ });
+
+ it('fetch the data from the API when the v-model changes', () => {
+ dispatchSpy.mockReturnValue();
+ wrapper.setData({ currentPage: 2 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
+ });
+ });
+ });
+ });
+
+ describe('modal', () => {
+ it('exists', () => {
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+
+ it('contains a description with the path of the item to delete', () => {
+ wrapper.setData({ itemToDelete: { path: 'foo' } });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteModal().html()).toContain('foo');
+ });
+ });
+ });
+
+ describe('tracking', () => {
+ const testTrackingCall = action => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
+ label: 'registry_repository_delete',
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ dispatchSpy.mockReturnValue();
+ });
+
+ it('send an event when delete button is clicked', () => {
+ const deleteBtn = findDeleteBtn();
+ deleteBtn.vm.$emit('click');
+ testTrackingCall('click_button');
+ });
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ testTrackingCall('cancel_delete');
+ });
+ it('send an event when confirm is clicked on modal', () => {
+ dispatchSpy.mockReturnValue();
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+ testTrackingCall('confirm_delete');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js
index 0df3ef68441..c1b977216ce 100644
--- a/spec/frontend/registry/explorer/stores/actions_spec.js
+++ b/spec/frontend/registry/explorer/stores/actions_spec.js
@@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => {
});
describe('fetch tags list', () => {
- const url = window.btoa(`${endpoint}/1}`);
+ const url = `${endpoint}/1}`;
+ const path = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` }));
it('sets the tagsList', done => {
- mock.onGet(window.atob(url)).replyOnce(200, registryServerResponse, {});
+ mock.onGet(url).replyOnce(200, registryServerResponse, {});
testAction(
actions.requestTagsList,
- { id: url },
+ { id: path },
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
@@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
it('should create flash on error', done => {
testAction(
actions.requestTagsList,
- { id: url },
+ { id: path },
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js
index 5766f3082d6..43f6a95db10 100644
--- a/spec/frontend/registry/explorer/stores/mutations_spec.js
+++ b/spec/frontend/registry/explorer/stores/mutations_spec.js
@@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
- const expectedState = { ...mockState, config: { endpoint: 'foo' } };
- mutations[types.SET_INITIAL_STATE](mockState, { endpoint: 'foo' });
+ const payload = { endpoint: 'foo', isGroupPage: true };
+ const expectedState = { ...mockState, config: payload };
+ mutations[types.SET_INITIAL_STATE](mockState, payload);
expect(mockState).toEqual(expectedState);
});
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js
new file mode 100644
index 00000000000..2c2c7587af9
--- /dev/null
+++ b/spec/frontend/registry/explorer/stubs.js
@@ -0,0 +1,11 @@
+export const GlModal = {
+ template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
+ methods: {
+ show: jest.fn(),
+ },
+};
+
+export const GlEmptyState = {
+ template: '<div><slot name="description"></slot></div>',
+ name: 'GlEmptyStateSTub',
+};