Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
94e614c94c
commit
6f9edd1a4c
|
@ -1,3 +1,3 @@
|
|||
We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly.
|
||||
|
||||
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account.
|
||||
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab/issues). You can log into GitLab.com using your GitHub account.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Thank you for taking the time to contribute back to GitLab!
|
||||
|
||||
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
|
||||
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
|
||||
|
|
|
@ -24,17 +24,6 @@ package-and-qa-manual:
|
|||
when: manual
|
||||
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
|
||||
|
||||
package-and-qa-manual:master:
|
||||
extends:
|
||||
- .package-and-qa-base
|
||||
- .only-code-qa-changes
|
||||
only:
|
||||
refs:
|
||||
- master@gitlab-org/gitlab-foss
|
||||
- master@gitlab-org/gitlab
|
||||
when: manual
|
||||
needs: ["build-qa-image", "gitlab:assets:compile"]
|
||||
|
||||
package-and-qa:
|
||||
extends:
|
||||
- .package-and-qa-base
|
||||
|
@ -44,3 +33,14 @@ package-and-qa:
|
|||
- master
|
||||
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
|
||||
allow_failure: true
|
||||
|
||||
schedule:package-and-qa:
|
||||
extends:
|
||||
- .package-and-qa-base
|
||||
- .only-code-qa-changes
|
||||
only:
|
||||
refs:
|
||||
- schedules@gitlab-org/gitlab
|
||||
- schedules@gitlab-org/gitlab-foss
|
||||
needs: ["build-qa-image", "gitlab:assets:compile"]
|
||||
allow_failure: true
|
||||
|
|
|
@ -2,17 +2,10 @@
|
|||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
filtered by the "regression" or "bug" label.
|
||||
filtered by the "regression" or "bug" label:
|
||||
|
||||
For the Community Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
|
||||
|
||||
For the Enterprise Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
|
||||
- https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=bug
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
--->
|
||||
|
|
|
@ -24,7 +24,7 @@ Remove the `:feature_name` feature flag ...
|
|||
|
||||
If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example.
|
||||
|
||||
- `gitlab-org/gitlab-ce`/`gitlab-org/gitlab-ee` projects
|
||||
- `gitlab-org/gitlab` project
|
||||
- `gitlab-org`/`gitlab-com` groups
|
||||
- ...
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
## Confidence
|
||||
|
||||
<!-- How do we know this is a problem? Please provide and link to any supporting information (e.g. data, customer verbatims) and use this basis to provide a numerical assessment on our confidence level in this problem's severity:
|
||||
<!-- How do we know this is a problem? Please provide and link to any supporting information (e.g. data, customer verbatims) and use this basis to provide a numerical assessment on our confidence level in this problem's severity:
|
||||
|
||||
100% = High confidence
|
||||
80% = Medium confidence
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
## Effort
|
||||
|
||||
<!-- How much effort do we think it will be to solve this problem? Please include all counterparts (Product, UX, Engineering, etc) in your assessment and quantify the number of person-months needed to dedicate to the effort.
|
||||
<!-- How much effort do we think it will be to solve this problem? Please include all counterparts (Product, UX, Engineering, etc) in your assessment and quantify the number of person-months needed to dedicate to the effort.
|
||||
|
||||
For example, if the solution will take a product manager, designer, and engineer two weeks of effort - you may quantify this as 1.5 (based on 0.5 months x 3 people). -->
|
||||
|
||||
|
|
|
@ -18,13 +18,7 @@ Set the title to: `Security Release: 12.2.X, 12.1.X, and 12.0.X`
|
|||
|
||||
## Security Issues:
|
||||
|
||||
### CE
|
||||
|
||||
* {https://gitlab.com/gitlab-org/gitlab-ce/issues link}
|
||||
|
||||
### EE
|
||||
|
||||
* {https://gitlab.com/gitlab-org/gitlab-ee/issues link}
|
||||
* {https://gitlab.com/gitlab-org/gitlab/issues link}
|
||||
|
||||
## Security Issues in dev.gitlab.org:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<!-- This issue outlines testing activities related to a particular issue or epic.
|
||||
|
||||
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-ce/issues/50353)
|
||||
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-foss/issues/50353)
|
||||
|
||||
This and other comments should be removed as you write the plan -->
|
||||
|
||||
|
@ -63,7 +63,7 @@ intersection of Components and Attributes.
|
|||
Some features might be simple enough that they only involve one Component, while
|
||||
more complex features could involve multiple or even all.
|
||||
|
||||
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
|
||||
Example (from https://gitlab.com/gitlab-org/gitlab-foss/issues/50353):
|
||||
* Repository is
|
||||
* Intuitive
|
||||
* It's easy to select the desired file template
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
dockerConnectionErrorText() {
|
||||
return sprintf(
|
||||
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
|
||||
issue with your project name or path.
|
||||
issue with your project name or path.
|
||||
%{docLinkStart}More Information%{docLinkEnd}`),
|
||||
{
|
||||
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`,
|
||||
|
@ -58,8 +58,8 @@ export default {
|
|||
},
|
||||
introText() {
|
||||
return sprintf(
|
||||
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
|
||||
project can have its own space to store its Docker images.
|
||||
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
|
||||
project can have its own space to store its Docker images.
|
||||
%{docLinkStart}More Information%{docLinkEnd}`),
|
||||
{
|
||||
docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
|
||||
|
@ -109,7 +109,7 @@ export default {
|
|||
:svg-path="containersErrorImage"
|
||||
>
|
||||
<template #description>
|
||||
<p v-html="dockerConnectionErrorText"></p>
|
||||
<p class="js-character-error-text" v-html="dockerConnectionErrorText"></p>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
}
|
||||
},
|
||||
handleDeleteRepository() {
|
||||
this.deleteItem(this.repo)
|
||||
return this.deleteItem(this.repo)
|
||||
.then(() => {
|
||||
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
|
||||
this.fetchRepos();
|
||||
|
@ -67,7 +67,8 @@ export default {
|
|||
<div class="container-image">
|
||||
<div class="container-image-head">
|
||||
<gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
|
||||
<icon :name="iconName" /> {{ repo.name }}
|
||||
<icon :name="iconName" />
|
||||
{{ repo.name }}
|
||||
</gl-button>
|
||||
|
||||
<clipboard-button
|
||||
|
|
|
@ -198,8 +198,9 @@ export default {
|
|||
:title="s__('ContainerRegistry|Remove selected images')"
|
||||
:aria-label="s__('ContainerRegistry|Remove selected images')"
|
||||
@click="deleteMultipleItems()"
|
||||
><icon name="remove"
|
||||
/></gl-button>
|
||||
>
|
||||
<icon name="remove" />
|
||||
</gl-button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -223,9 +224,9 @@ export default {
|
|||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span v-gl-tooltip.bottom class="monospace" :title="item.revision">
|
||||
{{ item.shortRevision }}
|
||||
</span>
|
||||
<span v-gl-tooltip.bottom class="monospace" :title="item.revision">{{
|
||||
item.shortRevision
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ formatSize(item.size) }}
|
||||
|
@ -236,9 +237,9 @@ export default {
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">
|
||||
{{ timeFormated(item.createdAt) }}
|
||||
</span>
|
||||
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
|
||||
timeFormated(item.createdAt)
|
||||
}}</span>
|
||||
</td>
|
||||
|
||||
<td class="content action-buttons">
|
||||
|
@ -262,6 +263,7 @@ export default {
|
|||
v-if="shouldRenderPagination"
|
||||
:change="onPageChange"
|
||||
:page-info="repo.pagination"
|
||||
class="js-registry-pagination"
|
||||
/>
|
||||
|
||||
<gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
module Clusters
|
||||
class KubernetesNamespaceFinder
|
||||
attr_reader :cluster, :project, :environment_slug
|
||||
attr_reader :cluster, :project, :environment_name
|
||||
|
||||
def initialize(cluster, project:, environment_slug:, allow_blank_token: false)
|
||||
def initialize(cluster, project:, environment_name:, allow_blank_token: false)
|
||||
@cluster = cluster
|
||||
@project = project
|
||||
@environment_slug = environment_slug
|
||||
@environment_name = environment_name
|
||||
@allow_blank_token = allow_blank_token
|
||||
end
|
||||
|
||||
|
@ -20,7 +20,11 @@ module Clusters
|
|||
attr_reader :allow_blank_token
|
||||
|
||||
def find_namespace(with_environment:)
|
||||
relation = with_environment ? namespaces.with_environment_slug(environment_slug) : namespaces
|
||||
relation = if with_environment
|
||||
namespaces.with_environment_name(environment_name)
|
||||
else
|
||||
namespaces
|
||||
end
|
||||
|
||||
relation.find_by_project_id(project.id)
|
||||
end
|
||||
|
|
|
@ -172,7 +172,7 @@ module Clusters
|
|||
persisted_namespace = Clusters::KubernetesNamespaceFinder.new(
|
||||
self,
|
||||
project: project,
|
||||
environment_slug: environment.slug
|
||||
environment_name: environment.name
|
||||
).execute
|
||||
|
||||
persisted_namespace&.namespace || Gitlab::Kubernetes::DefaultNamespace.new(self, project: project).from_environment_slug(environment.slug)
|
||||
|
|
|
@ -27,7 +27,7 @@ module Clusters
|
|||
algorithm: 'aes-256-cbc'
|
||||
|
||||
scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) }
|
||||
scope :with_environment_slug, -> (slug) { joins(:environment).where(environments: { slug: slug }) }
|
||||
scope :with_environment_name, -> (name) { joins(:environment).where(environments: { name: name }) }
|
||||
|
||||
def token_name
|
||||
"#{namespace}-token"
|
||||
|
|
|
@ -105,19 +105,11 @@ module Clusters
|
|||
|
||||
private
|
||||
|
||||
##
|
||||
# Environment slug can be predicted given an environment
|
||||
# name, so even if the environment isn't persisted yet we
|
||||
# still know what to look for.
|
||||
def environment_slug(name)
|
||||
Gitlab::Slug::Environment.new(name).generate
|
||||
end
|
||||
|
||||
def find_persisted_namespace(project, environment_name:)
|
||||
Clusters::KubernetesNamespaceFinder.new(
|
||||
cluster,
|
||||
project: project,
|
||||
environment_slug: environment_slug(environment_name)
|
||||
environment_name: environment_name
|
||||
).execute
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug that caused a merge to show an error message
|
||||
merge_request: 17466
|
||||
author:
|
||||
type: fixed
|
|
@ -134,7 +134,7 @@ graph RL;
|
|||
M[coverage];
|
||||
N[pages];
|
||||
O[static-analysis];
|
||||
P["package-and-qa-manual:master<br/>(master schedule only)"];
|
||||
P["schedule:package-and-qa<br/>(master schedule only)"];
|
||||
Q[package-and-qa];
|
||||
R[package-and-qa-manual];
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ module Gitlab
|
|||
Clusters::KubernetesNamespaceFinder.new(
|
||||
deployment_cluster,
|
||||
project: environment.project,
|
||||
environment_slug: environment.slug,
|
||||
environment_name: environment.name,
|
||||
allow_blank_token: true
|
||||
).execute
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
|
|||
described_class.new(
|
||||
cluster,
|
||||
project: project,
|
||||
environment_slug: 'production',
|
||||
environment_name: 'production',
|
||||
allow_blank_token: allow_blank_token
|
||||
)
|
||||
end
|
||||
|
@ -22,8 +22,8 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
|
|||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:production) { create(:environment, project: project, slug: 'production') }
|
||||
let(:staging) { create(:environment, project: project, slug: 'staging') }
|
||||
let(:production) { create(:environment, project: project, name: 'production') }
|
||||
let(:staging) { create(:environment, project: project, name: 'staging') }
|
||||
|
||||
let(:cluster) { create(:cluster, :group, :provided_by_user) }
|
||||
let(:project) { create(:project) }
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import registry from '~/registry/components/app.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { TEST_HOST } from '../../helpers/test_constants';
|
||||
import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
|
||||
|
||||
describe('Registry List', () => {
|
||||
let wrapper;
|
||||
|
||||
const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
|
||||
const findNoContainerImagesText = w => w.find('.js-no-container-images-text');
|
||||
const findSpinner = w => w.find('.gl-spinner');
|
||||
const findCharacterErrorText = w => w.find('.js-character-error-text');
|
||||
|
||||
const propsData = {
|
||||
endpoint: `${TEST_HOST}/foo`,
|
||||
helpPagePath: 'foo',
|
||||
noContainersImage: 'foo',
|
||||
containersErrorImage: 'foo',
|
||||
repositoryUrl: 'foo',
|
||||
};
|
||||
|
||||
const setMainEndpoint = jest.fn();
|
||||
const fetchRepos = jest.fn();
|
||||
|
||||
const methods = {
|
||||
setMainEndpoint,
|
||||
fetchRepos,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(registry, {
|
||||
propsData,
|
||||
computed: {
|
||||
repos() {
|
||||
return parsedReposServerResponse;
|
||||
},
|
||||
},
|
||||
methods,
|
||||
});
|
||||
});
|
||||
|
||||
describe('with data', () => {
|
||||
it('should render a list of CollapsibeContainerRegisty', () => {
|
||||
const containers = findCollapsibleContainer(wrapper);
|
||||
expect(wrapper.vm.repos.length).toEqual(reposServerResponse.length);
|
||||
expect(containers.length).toEqual(reposServerResponse.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without data', () => {
|
||||
let localWrapper;
|
||||
beforeEach(() => {
|
||||
localWrapper = mount(registry, {
|
||||
propsData,
|
||||
computed: {
|
||||
repos() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty message', () => {
|
||||
const noContainerImagesText = findNoContainerImagesText(localWrapper);
|
||||
expect(noContainerImagesText.text()).toEqual(
|
||||
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('while loading data', () => {
|
||||
let localWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
localWrapper = mount(registry, {
|
||||
propsData,
|
||||
computed: {
|
||||
repos() {
|
||||
return [];
|
||||
},
|
||||
isLoading() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a loading spinner', () => {
|
||||
const spinner = findSpinner(localWrapper);
|
||||
expect(spinner.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid characters in path', () => {
|
||||
let localWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
localWrapper = mount(registry, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
characterError: true,
|
||||
},
|
||||
computed: {
|
||||
repos() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render invalid characters error message', () => {
|
||||
const characterErrorText = findCharacterErrorText(localWrapper);
|
||||
expect(characterErrorText.text()).toEqual(
|
||||
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More Information',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
import Vue from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
|
||||
import { repoPropsData } from '../mock_data';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
|
||||
describe('collapsible registry container', () => {
|
||||
let wrapper;
|
||||
|
||||
const findDeleteBtn = w => w.find('.js-remove-repo');
|
||||
const findContainerImageTags = w => w.find('.container-image-tags');
|
||||
const findToggleRepos = w => w.findAll('.js-toggle-repo');
|
||||
|
||||
beforeEach(() => {
|
||||
createFlash.mockClear();
|
||||
// This is needed due to console.error called by vue to emit a warning that stop the tests
|
||||
// see https://github.com/vuejs/vue-test-utils/issues/532
|
||||
Vue.config.silent = true;
|
||||
wrapper = mount(collapsibleComponent, {
|
||||
propsData: {
|
||||
repo: repoPropsData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.config.silent = false;
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
beforeEach(() => {
|
||||
const fetchList = jest.fn();
|
||||
wrapper.setMethods({ fetchList });
|
||||
});
|
||||
|
||||
const expectIsClosed = () => {
|
||||
const container = findContainerImageTags(wrapper);
|
||||
expect(container.exists()).toBe(false);
|
||||
expect(wrapper.vm.iconName).toEqual('angle-right');
|
||||
};
|
||||
|
||||
it('should be closed by default', () => {
|
||||
expectIsClosed();
|
||||
});
|
||||
it('should be open when user clicks on closed repo', () => {
|
||||
const toggleRepos = findToggleRepos(wrapper);
|
||||
toggleRepos.at(0).trigger('click');
|
||||
const container = findContainerImageTags(wrapper);
|
||||
expect(container.exists()).toBe(true);
|
||||
expect(wrapper.vm.fetchList).toHaveBeenCalled();
|
||||
});
|
||||
it('should be closed when the user clicks on an opened repo', done => {
|
||||
const toggleRepos = findToggleRepos(wrapper);
|
||||
toggleRepos.at(0).trigger('click');
|
||||
Vue.nextTick(() => {
|
||||
toggleRepos.at(0).trigger('click');
|
||||
Vue.nextTick(() => {
|
||||
expectIsClosed();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete repo', () => {
|
||||
it('should be possible to delete a repo', () => {
|
||||
const deleteBtn = findDeleteBtn(wrapper);
|
||||
expect(deleteBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should call deleteItem when confirming deletion', () => {
|
||||
const deleteItem = jest.fn().mockResolvedValue();
|
||||
const fetchRepos = jest.fn().mockResolvedValue();
|
||||
wrapper.setMethods({ deleteItem, fetchRepos });
|
||||
wrapper.vm.handleDeleteRepository();
|
||||
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(wrapper.vm.repo);
|
||||
});
|
||||
|
||||
it('should show an error when there is API error', () => {
|
||||
const deleteItem = jest.fn().mockRejectedValue('error');
|
||||
wrapper.setMethods({ deleteItem });
|
||||
return wrapper.vm.handleDeleteRepository().then(() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,211 @@
|
|||
import Vue from 'vue';
|
||||
import tableRegistry from '~/registry/components/table_registry.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { repoPropsData } from '../mock_data';
|
||||
|
||||
const [firstImage, secondImage] = repoPropsData.list;
|
||||
|
||||
describe('table registry', () => {
|
||||
let wrapper;
|
||||
|
||||
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
|
||||
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
|
||||
const findDeleteButton = w => w.find('.js-delete-registry');
|
||||
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
|
||||
const findPagination = w => w.find('.js-registry-pagination');
|
||||
const bulkDeletePath = 'path';
|
||||
|
||||
beforeEach(() => {
|
||||
// This is needed due to console.error called by vue to emit a warning that stop the tests
|
||||
// see https://github.com/vuejs/vue-test-utils/issues/532
|
||||
Vue.config.silent = true;
|
||||
wrapper = mount(tableRegistry, {
|
||||
propsData: {
|
||||
repo: repoPropsData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.config.silent = false;
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render a table with the registry list', () => {
|
||||
expect(wrapper.findAll('.registry-image-row').length).toEqual(repoPropsData.list.length);
|
||||
});
|
||||
|
||||
it('should render registry tag', () => {
|
||||
const tds = wrapper.findAll('.registry-image-row td');
|
||||
expect(tds.at(0).classes()).toContain('check');
|
||||
expect(tds.at(1).html()).toContain(repoPropsData.list[0].tag);
|
||||
expect(tds.at(2).html()).toContain(repoPropsData.list[0].shortRevision);
|
||||
expect(tds.at(3).html()).toContain(repoPropsData.list[0].layers);
|
||||
expect(tds.at(3).html()).toContain(repoPropsData.list[0].size);
|
||||
expect(tds.at(4).html()).toContain(wrapper.vm.timeFormated(repoPropsData.list[0].createdAt));
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi select', () => {
|
||||
it('selecting a row should enable delete button', done => {
|
||||
const deleteBtn = findDeleteButton(wrapper);
|
||||
const checkboxes = findSelectCheckboxes(wrapper);
|
||||
|
||||
expect(deleteBtn.attributes('disabled')).toBe('disabled');
|
||||
|
||||
checkboxes.at(0).trigger('click');
|
||||
Vue.nextTick(() => {
|
||||
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('selecting all checkbox should select all rows and enable delete button', done => {
|
||||
const selectAll = findSelectAllCheckbox(wrapper);
|
||||
const checkboxes = findSelectCheckboxes(wrapper);
|
||||
selectAll.trigger('click');
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const checked = checkboxes.filter(w => w.element.checked);
|
||||
expect(checked.length).toBe(checkboxes.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
|
||||
const checkboxes = findSelectCheckboxes(wrapper);
|
||||
const selectAll = findSelectAllCheckbox(wrapper);
|
||||
selectAll.trigger('click');
|
||||
selectAll.trigger('click');
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const checked = checkboxes.filter(w => !w.element.checked);
|
||||
expect(checked.length).toBe(checkboxes.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete multiple items when multiple items are selected', done => {
|
||||
const multiDeleteItems = jest.fn().mockResolvedValue();
|
||||
wrapper.setMethods({ multiDeleteItems });
|
||||
const selectAll = findSelectAllCheckbox(wrapper);
|
||||
selectAll.trigger('click');
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const deleteBtn = findDeleteButton(wrapper);
|
||||
expect(wrapper.vm.itemsToBeDeleted).toEqual([0, 1]);
|
||||
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
|
||||
wrapper.vm.handleMultipleDelete();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
|
||||
expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
|
||||
path: bulkDeletePath,
|
||||
items: [firstImage.tag, secondImage.tag],
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show an error message if bulkDeletePath is not set', () => {
|
||||
const showError = jest.fn();
|
||||
wrapper.setMethods({ showError });
|
||||
wrapper.setProps({
|
||||
repo: {
|
||||
...repoPropsData,
|
||||
tagsPath: null,
|
||||
},
|
||||
});
|
||||
wrapper.vm.handleMultipleDelete();
|
||||
expect(wrapper.vm.showError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete registry', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({ itemsToBeDeleted: [0] });
|
||||
});
|
||||
|
||||
it('should be possible to delete a registry', () => {
|
||||
const deleteBtn = findDeleteButton(wrapper);
|
||||
const deleteBtns = findDeleteButtonsRow(wrapper);
|
||||
expect(wrapper.vm.itemsToBeDeleted).toEqual([0]);
|
||||
expect(deleteBtn).toBeDefined();
|
||||
expect(deleteBtn.attributes('disable')).toBe(undefined);
|
||||
expect(deleteBtns.is('button')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow deletion row by row', () => {
|
||||
const deleteBtns = findDeleteButtonsRow(wrapper);
|
||||
const deleteSingleItem = jest.fn();
|
||||
const deleteItem = jest.fn().mockResolvedValue();
|
||||
wrapper.setMethods({ deleteSingleItem, deleteItem });
|
||||
deleteBtns.at(0).trigger('click');
|
||||
expect(wrapper.vm.deleteSingleItem).toHaveBeenCalledWith(0);
|
||||
wrapper.vm.handleSingleDelete(1);
|
||||
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
let localWrapper = null;
|
||||
const repo = {
|
||||
repoPropsData,
|
||||
pagination: {
|
||||
total: 20,
|
||||
perPage: 2,
|
||||
nextPage: 2,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localWrapper = mount(tableRegistry, {
|
||||
propsData: {
|
||||
repo,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should exist', () => {
|
||||
const pagination = findPagination(localWrapper);
|
||||
expect(pagination.exists()).toBe(true);
|
||||
});
|
||||
it('should be visible when pagination is needed', () => {
|
||||
const pagination = findPagination(localWrapper);
|
||||
expect(pagination.isVisible()).toBe(true);
|
||||
localWrapper.setProps({
|
||||
repo: {
|
||||
pagination: {
|
||||
total: 0,
|
||||
perPage: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(localWrapper.vm.shouldRenderPagination).toBe(false);
|
||||
});
|
||||
it('should have a change function that update the list when run', () => {
|
||||
const fetchList = jest.fn().mockResolvedValue();
|
||||
localWrapper.setMethods({ fetchList });
|
||||
localWrapper.vm.onPageChange(1);
|
||||
expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal content', () => {
|
||||
it('should show the singular title and image name when deleting a single image', () => {
|
||||
wrapper.setData({ itemsToBeDeleted: [1] });
|
||||
wrapper.vm.setModalDescription(0);
|
||||
expect(wrapper.vm.modalTitle).toBe('Remove image');
|
||||
expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
|
||||
});
|
||||
|
||||
it('should show the plural title and image count when deleting more than one image', () => {
|
||||
wrapper.setData({ itemsToBeDeleted: [1, 2] });
|
||||
wrapper.vm.setModalDescription();
|
||||
|
||||
expect(wrapper.vm.modalTitle).toBe('Remove images');
|
||||
expect(wrapper.vm.modalDescription).toContain('<b>2</b> images');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,189 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as actions from '~/registry/stores/actions';
|
||||
import * as types from '~/registry/stores/mutation_types';
|
||||
import { TEST_HOST } from '../../helpers/test_constants';
|
||||
import testAction from '../../helpers/vuex_action_helper';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
import {
|
||||
reposServerResponse,
|
||||
registryServerResponse,
|
||||
parsedReposServerResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
|
||||
describe('Actions Registry Store', () => {
|
||||
let mock;
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
state = {
|
||||
endpoint: `${TEST_HOST}/endpoint.json`,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('fetchRepos', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
|
||||
});
|
||||
|
||||
it('should set receveived repos', done => {
|
||||
testAction(
|
||||
actions.fetchRepos,
|
||||
null,
|
||||
state,
|
||||
[
|
||||
{ type: types.TOGGLE_MAIN_LOADING },
|
||||
{ type: types.TOGGLE_MAIN_LOADING },
|
||||
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create flash on API error', done => {
|
||||
testAction(
|
||||
actions.fetchRepos,
|
||||
null,
|
||||
{
|
||||
endpoint: null,
|
||||
},
|
||||
[{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING }],
|
||||
[],
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
done();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchList', () => {
|
||||
let repo;
|
||||
beforeEach(() => {
|
||||
state.repos = parsedReposServerResponse;
|
||||
[, repo] = state.repos;
|
||||
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
|
||||
});
|
||||
|
||||
it('should set received list', done => {
|
||||
testAction(
|
||||
actions.fetchList,
|
||||
{ repo },
|
||||
state,
|
||||
[
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
|
||||
{
|
||||
type: types.SET_REGISTRY_LIST,
|
||||
payload: {
|
||||
repo,
|
||||
resp: registryServerResponse,
|
||||
headers: expect.anything(),
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create flash on API error', done => {
|
||||
const updatedRepo = {
|
||||
...repo,
|
||||
tagsPath: null,
|
||||
};
|
||||
testAction(
|
||||
actions.fetchList,
|
||||
{
|
||||
repo: updatedRepo,
|
||||
},
|
||||
state,
|
||||
[
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
|
||||
],
|
||||
[],
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
done();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMainEndpoint', () => {
|
||||
it('should commit set main endpoint', done => {
|
||||
testAction(
|
||||
actions.setMainEndpoint,
|
||||
'endpoint',
|
||||
state,
|
||||
[{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleLoading', () => {
|
||||
it('should commit toggle main loading', done => {
|
||||
testAction(
|
||||
actions.toggleLoading,
|
||||
null,
|
||||
state,
|
||||
[{ type: types.TOGGLE_MAIN_LOADING }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteItem and multiDeleteItems', () => {
|
||||
let deleted;
|
||||
const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
|
||||
|
||||
const expectDelete = done => {
|
||||
expect(mock.history.delete.length).toBe(1);
|
||||
expect(deleted).toBe(true);
|
||||
done();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
deleted = false;
|
||||
mock.onDelete(destroyPath).replyOnce(() => {
|
||||
deleted = true;
|
||||
return [200];
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteItem should perform DELETE request on destroyPath', done => {
|
||||
testAction(
|
||||
actions.deleteItem,
|
||||
{
|
||||
destroyPath,
|
||||
},
|
||||
state,
|
||||
)
|
||||
.then(() => {
|
||||
expectDelete(done);
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('multiDeleteItems should perform DELETE request on path', done => {
|
||||
testAction(actions.multiDeleteItems, { path: destroyPath, items: [1] }, state)
|
||||
.then(() => {
|
||||
expectDelete(done);
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,129 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Vue from 'vue';
|
||||
import registry from '~/registry/components/app.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import { reposServerResponse } from '../mock_data';
|
||||
|
||||
describe('Registry List', () => {
|
||||
const Component = Vue.extend(registry);
|
||||
const props = {
|
||||
endpoint: `${TEST_HOST}/foo`,
|
||||
helpPagePath: 'foo',
|
||||
noContainersImage: 'foo',
|
||||
containersErrorImage: 'foo',
|
||||
repositoryUrl: 'foo',
|
||||
};
|
||||
let vm;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('with data', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
|
||||
|
||||
vm = mountComponent(Component, { ...props });
|
||||
});
|
||||
|
||||
it('should render a list of repos', done => {
|
||||
setTimeout(() => {
|
||||
expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelectorAll('.container-image').length).toEqual(
|
||||
reposServerResponse.length,
|
||||
);
|
||||
done();
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
describe('delete repository', () => {
|
||||
it('should be possible to delete a repo', done => {
|
||||
setTimeout(() => {
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle repository', () => {
|
||||
it('should open the container', done => {
|
||||
setTimeout(() => {
|
||||
Vue.nextTick(() => {
|
||||
vm.$el.querySelector('.js-toggle-repo').click();
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
|
||||
).toContain('angle-up');
|
||||
done();
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without data', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
|
||||
|
||||
vm = mountComponent(Component, { ...props });
|
||||
});
|
||||
|
||||
it('should render empty message', done => {
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
|
||||
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
|
||||
);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('while loading data', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
|
||||
|
||||
vm = mountComponent(Component, { ...props });
|
||||
});
|
||||
|
||||
it('should render a loading spinner', done => {
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid characters in path', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
|
||||
|
||||
vm = mountComponent(Component, {
|
||||
...props,
|
||||
characterError: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render invalid characters error message', done => {
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('p')).not.toContain(
|
||||
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,87 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Vue from 'vue';
|
||||
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
|
||||
import store from '~/registry/stores';
|
||||
import * as types from '~/registry/stores/mutation_types';
|
||||
|
||||
import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
|
||||
|
||||
describe('collapsible registry container', () => {
|
||||
let vm;
|
||||
let mock;
|
||||
const Component = Vue.extend(collapsibleComponent);
|
||||
|
||||
const findDeleteBtn = () => vm.$el.querySelector('.js-remove-repo');
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
|
||||
|
||||
store.commit(types.SET_REPOS_LIST, reposServerResponse);
|
||||
|
||||
vm = new Component({
|
||||
store,
|
||||
propsData: {
|
||||
repo: repoPropsData,
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should be closed by default', () => {
|
||||
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
|
||||
expect(vm.iconName).toEqual('angle-right');
|
||||
});
|
||||
|
||||
it('should be open when user clicks on closed repo', done => {
|
||||
vm.$el.querySelector('.js-toggle-repo').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
|
||||
expect(vm.iconName).toEqual('angle-up');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be closed when the user clicks on an opened repo', done => {
|
||||
vm.$el.querySelector('.js-toggle-repo').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
vm.$el.querySelector('.js-toggle-repo').click();
|
||||
setTimeout(() => {
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
|
||||
expect(vm.iconName).toEqual('angle-right');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete repo', () => {
|
||||
it('should be possible to delete a repo', () => {
|
||||
expect(findDeleteBtn()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should call deleteItem when confirming deletion', done => {
|
||||
findDeleteBtn().click();
|
||||
spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve());
|
||||
|
||||
Vue.nextTick(() => {
|
||||
document.querySelector(`#${vm.modalId} .btn-danger`).click();
|
||||
|
||||
expect(vm.deleteItem).toHaveBeenCalledWith(vm.repo);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,189 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import tableRegistry from '~/registry/components/table_registry.vue';
|
||||
import store from '~/registry/stores';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { repoPropsData } from '../mock_data';
|
||||
|
||||
const [firstImage, secondImage] = repoPropsData.list;
|
||||
|
||||
describe('table registry', () => {
|
||||
let vm;
|
||||
const Component = Vue.extend(tableRegistry);
|
||||
const bulkDeletePath = 'path';
|
||||
|
||||
const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry');
|
||||
const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row');
|
||||
const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input');
|
||||
const findAllRowCheckboxes = () =>
|
||||
Array.from(vm.$el.querySelectorAll('.js-select-checkbox input'));
|
||||
const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`);
|
||||
|
||||
const createComponent = () => {
|
||||
vm = mountComponentWithStore(Component, {
|
||||
store,
|
||||
props: {
|
||||
repo: repoPropsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectAllCheckboxes = () => vm.selectAll();
|
||||
const deselectAllCheckboxes = () => vm.deselectAll();
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render a table with the registry list', () => {
|
||||
expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
|
||||
});
|
||||
|
||||
it('should render registry tag', () => {
|
||||
const textRendered = vm.$el
|
||||
.querySelector('.table tbody tr')
|
||||
.textContent.trim()
|
||||
// replace additional whitespace characters (e.g. new lines) with a single empty space
|
||||
.replace(/\s\s+/g, ' ');
|
||||
|
||||
expect(textRendered).toContain(repoPropsData.list[0].tag);
|
||||
expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
|
||||
expect(textRendered).toContain(repoPropsData.list[0].layers);
|
||||
expect(textRendered).toContain(repoPropsData.list[0].size);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi select', () => {
|
||||
it('should support multiselect and selecting a row should enable delete button', done => {
|
||||
findSelectAllCheckbox().click();
|
||||
selectAllCheckboxes();
|
||||
|
||||
expect(findSelectAllCheckbox().checked).toBe(true);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(findDeleteBtn().disabled).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('selecting all checkbox should select all rows and enable delete button', done => {
|
||||
selectAllCheckboxes();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
|
||||
|
||||
expect(checkedValues.length).toBe(repoPropsData.list.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
|
||||
selectAllCheckboxes();
|
||||
deselectAllCheckboxes();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
|
||||
|
||||
expect(checkedValues.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete multiple items when multiple items are selected', done => {
|
||||
selectAllCheckboxes();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.itemsToBeDeleted).toEqual([0, 1]);
|
||||
expect(findDeleteBtn().disabled).toBe(false);
|
||||
|
||||
findDeleteBtn().click();
|
||||
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const modal = confirmationModal();
|
||||
confirmationModal('.btn-danger').click();
|
||||
|
||||
expect(modal).toExist();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.itemsToBeDeleted).toEqual([]);
|
||||
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
|
||||
path: bulkDeletePath,
|
||||
items: [firstImage.tag, secondImage.tag],
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete registry', () => {
|
||||
beforeEach(() => {
|
||||
vm.itemsToBeDeleted = [0];
|
||||
});
|
||||
|
||||
it('should be possible to delete a registry', done => {
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.itemsToBeDeleted).toEqual([0]);
|
||||
expect(findDeleteBtn()).toBeDefined();
|
||||
expect(findDeleteBtn().disabled).toBe(false);
|
||||
expect(findDeleteBtnRow()).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => {
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.itemsToBeDeleted).toEqual([0]);
|
||||
expect(findDeleteBtn().disabled).toBe(false);
|
||||
findDeleteBtn().click();
|
||||
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
|
||||
|
||||
Vue.nextTick(() => {
|
||||
confirmationModal('.btn-danger').click();
|
||||
|
||||
expect(vm.itemsToBeDeleted).toEqual([]);
|
||||
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
|
||||
path: bulkDeletePath,
|
||||
items: [firstImage.tag],
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('should be possible to change the page', () => {
|
||||
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal content', () => {
|
||||
it('should show the singular title and image name when deleting a single image', done => {
|
||||
findDeleteBtnRow().click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.modalTitle).toBe('Remove image');
|
||||
expect(vm.modalDescription).toContain(firstImage.tag);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the plural title and image count when deleting more than one image', done => {
|
||||
selectAllCheckboxes();
|
||||
vm.setModalDescription();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.modalTitle).toBe('Remove images');
|
||||
expect(vm.modalDescription).toContain('<b>2</b> images');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,132 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as actions from '~/registry/stores/actions';
|
||||
import * as types from '~/registry/stores/mutation_types';
|
||||
import state from '~/registry/stores/state';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import testAction from '../../helpers/vuex_action_helper';
|
||||
import {
|
||||
reposServerResponse,
|
||||
registryServerResponse,
|
||||
parsedReposServerResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
describe('Actions Registry Store', () => {
|
||||
let mockedState;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedState = state();
|
||||
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('server requests', () => {
|
||||
describe('fetchRepos', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
|
||||
});
|
||||
|
||||
it('should set receveived repos', done => {
|
||||
testAction(
|
||||
actions.fetchRepos,
|
||||
null,
|
||||
mockedState,
|
||||
[
|
||||
{ type: types.TOGGLE_MAIN_LOADING },
|
||||
{ type: types.TOGGLE_MAIN_LOADING },
|
||||
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchList', () => {
|
||||
let repo;
|
||||
beforeEach(() => {
|
||||
mockedState.repos = parsedReposServerResponse;
|
||||
[, repo] = mockedState.repos;
|
||||
|
||||
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
|
||||
});
|
||||
|
||||
it('should set received list', done => {
|
||||
testAction(
|
||||
actions.fetchList,
|
||||
{ repo },
|
||||
mockedState,
|
||||
[
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
|
||||
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
|
||||
{
|
||||
type: types.SET_REGISTRY_LIST,
|
||||
payload: {
|
||||
repo,
|
||||
resp: registryServerResponse,
|
||||
headers: jasmine.anything(),
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMainEndpoint', () => {
|
||||
it('should commit set main endpoint', done => {
|
||||
testAction(
|
||||
actions.setMainEndpoint,
|
||||
'endpoint',
|
||||
mockedState,
|
||||
[{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleLoading', () => {
|
||||
it('should commit toggle main loading', done => {
|
||||
testAction(
|
||||
actions.toggleLoading,
|
||||
null,
|
||||
mockedState,
|
||||
[{ type: types.TOGGLE_MAIN_LOADING }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteItem', () => {
|
||||
it('should perform DELETE request on destroyPath', done => {
|
||||
const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
|
||||
let deleted = false;
|
||||
mock.onDelete(destroyPath).replyOnce(() => {
|
||||
deleted = true;
|
||||
return [200];
|
||||
});
|
||||
testAction(
|
||||
actions.deleteItem,
|
||||
{
|
||||
destroyPath,
|
||||
},
|
||||
mockedState,
|
||||
)
|
||||
.then(() => {
|
||||
expect(mock.history.delete.length).toBe(1);
|
||||
expect(deleted).toBe(true);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -546,7 +546,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
|
|||
|
||||
before do
|
||||
expect(Clusters::KubernetesNamespaceFinder).to receive(:new)
|
||||
.with(cluster, project: environment.project, environment_slug: environment.slug)
|
||||
.with(cluster, project: environment.project, environment_name: environment.name)
|
||||
.and_return(double(execute: persisted_namespace))
|
||||
end
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_environment_slug' do
|
||||
describe '.with_environment_name' do
|
||||
let(:cluster) { create(:cluster, :group) }
|
||||
let(:environment) { create(:environment, slug: slug) }
|
||||
let(:environment) { create(:environment, name: name) }
|
||||
|
||||
let(:slug) { 'production' }
|
||||
let(:name) { 'production' }
|
||||
|
||||
subject { described_class.with_environment_slug(slug) }
|
||||
subject { described_class.with_environment_name(name) }
|
||||
|
||||
context 'there is no associated environment' do
|
||||
let!(:namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: environment.project) }
|
||||
|
@ -48,12 +48,12 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
|
|||
)
|
||||
end
|
||||
|
||||
context 'with a matching slug' do
|
||||
context 'with a matching name' do
|
||||
it { is_expected.to eq [namespace] }
|
||||
end
|
||||
|
||||
context 'without a matching slug' do
|
||||
let(:environment) { create(:environment, slug: 'staging') }
|
||||
context 'without a matching name' do
|
||||
let(:environment) { create(:environment, name: 'staging') }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
|
|
@ -218,7 +218,7 @@ describe Clusters::Platforms::Kubernetes do
|
|||
|
||||
before do
|
||||
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
|
||||
.with(cluster, project: project, environment_slug: environment_slug)
|
||||
.with(cluster, project: project, environment_name: environment_name)
|
||||
.and_return(double(execute: persisted_namespace))
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue