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/packages_and_registries/container_registry/explorer/components/list_page')
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap15
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap83
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js87
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js94
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js37
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js223
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js88
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js45
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js135
9 files changed, 807 insertions, 0 deletions
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 00000000000..56579847468
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Group Empty state to match the default snapshot 1`] = `
+<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
+ href="baz"
+ target="_blank"
+ >
+ More Information
+ </gl-link-stub>
+ </p>
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
new file mode 100644
index 00000000000..46b07b4c2d6
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Project Empty state to match the default snapshot 1`] = `
+<div>
+ <p>
+ 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>
+ CLI Commands
+ </h5>
+
+ <p>
+ 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>
+
+ <gl-form-input-group-stub
+ class="gl-mb-4"
+ predefinedoptions="[object Object]"
+ value=""
+ >
+ <gl-form-input-stub
+ class="gl-font-monospace!"
+ readonly=""
+ type="text"
+ value="bazbaz"
+ />
+ </gl-form-input-group-stub>
+
+ <p
+ class="gl-mb-4"
+ >
+
+ You can add an image to this registry with the following commands:
+
+ </p>
+
+ <gl-form-input-group-stub
+ class="gl-mb-4"
+ predefinedoptions="[object Object]"
+ value=""
+ >
+ <gl-form-input-stub
+ class="gl-font-monospace!"
+ readonly=""
+ type="text"
+ value="foofoo"
+ />
+ </gl-form-input-group-stub>
+
+ <gl-form-input-group-stub
+ predefinedoptions="[object Object]"
+ value=""
+ >
+ <gl-form-input-stub
+ class="gl-font-monospace!"
+ readonly=""
+ type="text"
+ value="barbar"
+ />
+ </gl-form-input-group-stub>
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
new file mode 100644
index 00000000000..e8ddad2d8ca
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -0,0 +1,87 @@
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
+import {
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
+ CLEANUP_STATUS_SCHEDULED,
+ CLEANUP_STATUS_ONGOING,
+ CLEANUP_STATUS_UNFINISHED,
+ UNFINISHED_STATUS,
+ UNSCHEDULED_STATUS,
+ SCHEDULED_STATUS,
+ ONGOING_STATUS,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+
+describe('cleanup_status', () => {
+ let wrapper;
+
+ const findMainIcon = () => wrapper.findByTestId('main-icon');
+ const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
+
+ const mountComponent = (propsData = { status: SCHEDULED_STATUS }) => {
+ wrapper = shallowMountExtended(CleanupStatus, {
+ propsData,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ status | visible | text
+ ${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED}
+ ${SCHEDULED_STATUS} | ${true} | ${CLEANUP_STATUS_SCHEDULED}
+ ${ONGOING_STATUS} | ${true} | ${CLEANUP_STATUS_ONGOING}
+ ${UNSCHEDULED_STATUS} | ${false} | ${''}
+ `(
+ 'when the status is $status is $visible that the component is mounted and has the correct text',
+ ({ status, visible, text }) => {
+ mountComponent({ status });
+
+ expect(findMainIcon().exists()).toBe(visible);
+ expect(wrapper.text()).toBe(text);
+ },
+ );
+
+ describe('main icon', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findMainIcon().exists()).toBe(true);
+ });
+
+ it(`has the orange class when the status is ${UNFINISHED_STATUS}`, () => {
+ mountComponent({ status: UNFINISHED_STATUS });
+
+ expect(findMainIcon().classes('gl-text-orange-500')).toBe(true);
+ });
+ });
+
+ describe('extra info icon', () => {
+ it.each`
+ status | visible
+ ${UNFINISHED_STATUS} | ${true}
+ ${SCHEDULED_STATUS} | ${false}
+ ${ONGOING_STATUS} | ${false}
+ `(
+ 'when the status is $status is $visible that the extra icon is visible',
+ ({ status, visible }) => {
+ mountComponent({ status });
+
+ expect(findExtraInfoIcon().exists()).toBe(visible);
+ },
+ );
+
+ it(`has a tooltip`, () => {
+ mountComponent({ status: UNFINISHED_STATUS });
+
+ const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe(CLEANUP_TIMED_OUT_ERROR_MESSAGE);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
new file mode 100644
index 00000000000..4039fba869b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
@@ -0,0 +1,94 @@
+import { GlDropdown } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import QuickstartDropdown from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import {
+ QUICK_START,
+ LOGIN_COMMAND_LABEL,
+ COPY_LOGIN_TITLE,
+ BUILD_COMMAND_LABEL,
+ COPY_BUILD_TITLE,
+ PUSH_COMMAND_LABEL,
+ COPY_PUSH_TITLE,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import Tracking from '~/tracking';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+import { dockerCommands } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('cli_commands', () => {
+ let wrapper;
+
+ const config = {
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'bar',
+ };
+
+ const findDropdownButton = () => wrapper.find(GlDropdown);
+ const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
+
+ const mountComponent = () => {
+ wrapper = mount(QuickstartDropdown, {
+ localVue,
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('shows the correct text on the button', () => {
+ expect(findDropdownButton().text()).toContain(QUICK_START);
+ });
+
+ it('clicking on the dropdown emit a tracking event', () => {
+ findDropdownButton().vm.$emit('shown');
+ expect(Tracking.event).toHaveBeenCalledWith(
+ undefined,
+ 'click_dropdown',
+ expect.objectContaining({ label: 'quickstart_dropdown' }),
+ );
+ });
+
+ describe.each`
+ 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(() => {
+ codeInstruction = findCodeInstruction().at(index);
+ });
+
+ it('exists', () => {
+ expect(codeInstruction.exists()).toBe(true);
+ });
+
+ it(`has the correct props`, () => {
+ expect(codeInstruction.props()).toMatchObject({
+ label: labelText,
+ instruction: command,
+ copyText: titleText,
+ trackingAction: trackedEvent,
+ trackingLabel: 'quickstart_dropdown',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
new file mode 100644
index 00000000000..027cdf732bc
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -0,0 +1,37 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import groupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
+import { GlEmptyState } from '../../stubs';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Registry Group Empty state', () => {
+ let wrapper;
+ const config = {
+ noContainersImage: 'foo',
+ helpPagePath: 'baz',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMount(groupEmptyState, {
+ localVue,
+ stubs: {
+ GlEmptyState,
+ GlSprintf,
+ },
+ provide() {
+ return { config };
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
new file mode 100644
index 00000000000..411bef54e40
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -0,0 +1,223 @@
+import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import DeleteButton from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
+import {
+ ROW_SCHEDULED_FOR_DELETION,
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ IMAGE_DELETE_SCHEDULED_STATUS,
+ SCHEDULED_STATUS,
+ ROOT_IMAGE_TEXT,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import { imagesListResponse } from '../../mock_data';
+import { RouterLink } from '../../stubs';
+
+describe('Image List Row', () => {
+ let wrapper;
+ const [item] = imagesListResponse;
+
+ const findDetailsLink = () => wrapper.find('[data-testid="details-link"]');
+ const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
+ const findDeleteBtn = () => wrapper.findComponent(DeleteButton);
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
+ const findCleanupStatus = () => wrapper.findComponent(CleanupStatus);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findListItemComponent = () => wrapper.findComponent(ListItem);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(Component, {
+ stubs: {
+ RouterLink,
+ GlSprintf,
+ ListItem,
+ },
+ propsData: {
+ item,
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('list item component', () => {
+ describe('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, status: IMAGE_DELETE_SCHEDULED_STATUS } });
+
+ const tooltip = getBinding(wrapper.element, 'gl-tooltip');
+ expect(tooltip.value.disabled).toBe(false);
+ });
+ });
+
+ it('is disabled when the item is in deleting status', () => {
+ mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
+
+ expect(findListItemComponent().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('image title and path', () => {
+ it('contains a link to the details page', () => {
+ mountComponent();
+
+ const link = findDetailsLink();
+ expect(link.text()).toBe(item.path);
+ expect(findDetailsLink().props('to')).toMatchObject({
+ name: 'details',
+ params: {
+ id: getIdFromGraphQLId(item.id),
+ },
+ });
+ });
+
+ it(`when the image has no name appends ${ROOT_IMAGE_TEXT} to the path`, () => {
+ mountComponent({ item: { ...item, name: '' } });
+
+ expect(findDetailsLink().text()).toBe(`${item.path}/ ${ROOT_IMAGE_TEXT}`);
+ });
+
+ it('contains a clipboard button', () => {
+ mountComponent();
+ const button = findClipboardButton();
+ expect(button.exists()).toBe(true);
+ expect(button.props('text')).toBe(item.location);
+ expect(button.props('title')).toBe(item.location);
+ });
+
+ describe('cleanup status component', () => {
+ it.each`
+ expirationPolicyCleanupStatus | shown
+ ${null} | ${false}
+ ${SCHEDULED_STATUS} | ${true}
+ `(
+ 'when expirationPolicyCleanupStatus is $expirationPolicyCleanupStatus it is $shown that the component exists',
+ ({ expirationPolicyCleanupStatus, shown }) => {
+ mountComponent({ item: { ...item, expirationPolicyCleanupStatus } });
+
+ expect(findCleanupStatus().exists()).toBe(shown);
+
+ if (shown) {
+ expect(findCleanupStatus().props()).toMatchObject({
+ status: expirationPolicyCleanupStatus,
+ });
+ }
+ },
+ );
+ });
+
+ describe('when the item is deleting', () => {
+ beforeEach(() => {
+ mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
+ });
+
+ it('the router link is disabled', () => {
+ // we check the event prop as is the only workaround to disable a router link
+ expect(findDetailsLink().props('event')).toBe('');
+ });
+ it('the clipboard button is disabled', () => {
+ expect(findClipboardButton().attributes('disabled')).toBe('true');
+ });
+ });
+ });
+
+ describe('delete button', () => {
+ it('exists', () => {
+ mountComponent();
+ expect(findDeleteBtn().exists()).toBe(true);
+ });
+
+ it('has the correct props', () => {
+ mountComponent();
+
+ expect(findDeleteBtn().props()).toMatchObject({
+ title: REMOVE_REPOSITORY_LABEL,
+ 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`
+ canDelete | status | state
+ ${false} | ${''} | ${true}
+ ${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
+ ${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
+ ${true} | ${''} | ${false}
+ `(
+ '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);
+ },
+ );
+ });
+
+ describe('tags count', () => {
+ it('exists', () => {
+ mountComponent();
+ expect(findTagsCount().exists()).toBe(true);
+ });
+
+ it('contains a tag icon', () => {
+ mountComponent();
+ const icon = findTagsCount().find(GlIcon);
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe('tag');
+ });
+
+ describe('loading state', () => {
+ it('shows a loader when metadataLoading is true', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('hides the tags count while loading', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findTagsCount().exists()).toBe(false);
+ });
+ });
+
+ describe('tags count text', () => {
+ it('with one tag in the image', () => {
+ mountComponent({ item: { ...item, tagsCount: 1 } });
+
+ expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
+ });
+ it('with more than one tag in the image', () => {
+ mountComponent({ item: { ...item, tagsCount: 3 } });
+
+ expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
new file mode 100644
index 00000000000..e0119954ed4
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -0,0 +1,88 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ImageListRow from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
+
+import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data';
+
+describe('Image List', () => {
+ let wrapper;
+
+ const findRow = () => wrapper.findAll(ImageListRow);
+ const findPagination = () => wrapper.find(GlKeysetPagination);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(Component, {
+ propsData: {
+ images: imagesListResponse,
+ pageInfo: defaultPageInfo,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('list', () => {
+ it('contains one list element for each image', () => {
+ 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');
+ expect(wrapper.emitted('delete')).toEqual([['foo']]);
+ });
+
+ it('passes down the metadataLoading prop', () => {
+ mountComponent({ metadataLoading: true });
+ expect(findRow().at(0).props('metadataLoading')).toBe(true);
+ });
+ });
+
+ describe('pagination', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ 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({ pageInfo: { ...defaultPageInfo, 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();
+
+ findPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ });
+
+ it('emits "next-page" when the user clicks the forward page button', () => {
+ mountComponent();
+
+ findPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
new file mode 100644
index 00000000000..21748ae2813
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -0,0 +1,45 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import projectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
+import { dockerCommands } from '../../mock_data';
+import { GlEmptyState } from '../../stubs';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Registry Project Empty state', () => {
+ let wrapper;
+ const config = {
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'bar',
+ helpPagePath: 'baz',
+ twoFactorAuthHelpLink: 'barBaz',
+ personalAccessTokensHelpLink: 'fooBaz',
+ noContainersImage: 'bazFoo',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMount(projectEmptyState, {
+ localVue,
+ stubs: {
+ GlEmptyState,
+ GlSprintf,
+ },
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
new file mode 100644
index 00000000000..92cfeb7633e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -0,0 +1,135 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
+import {
+ CONTAINER_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+ EXPIRATION_POLICY_DISABLED_TEXT,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+
+jest.mock('~/lib/utils/datetime_utility', () => ({
+ approximateDuration: jest.fn(),
+ calculateRemainingMilliseconds: jest.fn(),
+}));
+
+describe('registry_header', () => {
+ let wrapper;
+
+ const findTitleArea = () => wrapper.find(TitleArea);
+ const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
+ const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
+ const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
+
+ const mountComponent = (propsData, slots) => {
+ wrapper = shallowMount(Component, {
+ stubs: {
+ GlSprintf,
+ TitleArea,
+ },
+ propsData,
+ slots,
+ });
+ return wrapper.vm.$nextTick();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('header', () => {
+ it('has a title', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findTitleArea().props()).toMatchObject({
+ title: CONTAINER_REGISTRY_TITLE,
+ metadataLoading: true,
+ });
+ });
+
+ it('has a commands slot', () => {
+ mountComponent(null, { commands: '<div data-testid="commands-slot">baz</div>' });
+
+ expect(findCommandsSlot().text()).toBe('baz');
+ });
+
+ describe('sub header parts', () => {
+ describe('images count', () => {
+ it('exists', async () => {
+ await mountComponent({ imagesCount: 1 });
+
+ expect(findImagesCountSubHeader().exists()).toBe(true);
+ });
+
+ it('when there is one image', async () => {
+ await mountComponent({ imagesCount: 1 });
+
+ expect(findImagesCountSubHeader().props()).toMatchObject({
+ text: '1 Image repository',
+ icon: 'container-image',
+ });
+ });
+
+ it('when there is more than one image', async () => {
+ await mountComponent({ imagesCount: 3 });
+
+ expect(findImagesCountSubHeader().props('text')).toBe('3 Image repositories');
+ });
+ });
+
+ describe('expiration policy', () => {
+ it('when is disabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: false },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
+ });
+
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(true);
+ expect(text.props()).toMatchObject({
+ text: EXPIRATION_POLICY_DISABLED_TEXT,
+ icon: 'expire',
+ size: 'xl',
+ });
+ });
+
+ it('when is enabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: true },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
+ });
+
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(true);
+ expect(text.props('text')).toBe('Expiration policy will run in ');
+ });
+ it('when the expiration policy is completely disabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: true },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
+ hideExpirationPolicyData: true,
+ });
+
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('info messages', () => {
+ describe('default message', () => {
+ it('is correctly bound to title_area props', () => {
+ mountComponent({ helpPagePath: 'foo' });
+
+ expect(findTitleArea().props('infoMessages')).toEqual([
+ { text: LIST_INTRO_TEXT, link: 'foo' },
+ ]);
+ });
+ });
+ });
+});