Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-10-14 09:07:54 +00:00
parent eccfaf7c24
commit 5ff1b520ba
33 changed files with 948 additions and 35 deletions

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
stages:
- prepare

View file

@ -14,10 +14,10 @@
variables:
GIT_STRATEGY: none
environment:
name: review-docs/$CI_COMMIT_REF_SLUG
name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693
url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
url: http://docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
before_script:
# We don't clone the repo by using GIT_STRATEGY: none and only download the
@ -39,7 +39,7 @@ review-docs-deploy:
review-docs-cleanup:
extends: .review-docs
environment:
name: review-docs/$CI_COMMIT_REF_SLUG
name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID
action: stop
script:
- ./trigger-build-docs cleanup

View file

@ -13,7 +13,7 @@
- .default-before_script
- .assets-compile-cache
- .only-code-qa-changes
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]

View file

@ -123,7 +123,7 @@
- name: redis:alpine
.use-pg10:
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -137,7 +137,7 @@
- name: docker.elastic.co/elasticsearch/elasticsearch:5.6.12
.use-pg10-ee:
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]

View file

@ -465,7 +465,7 @@ gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
# DNS Lookup
gem 'net-dns', '~> 0.9.0'
gem 'gitlab-net-dns', '~> 0.9.1'
# Countries list
gem 'countries', '~> 3.0'

View file

@ -370,6 +370,7 @@ GEM
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.0.0)
gitlab-markup (1.7.0)
gitlab-net-dns (0.9.1)
gitlab-peek (0.0.1)
railties (>= 4.0.0)
gitlab-sidekiq-fetcher (0.5.2)
@ -596,7 +597,6 @@ GEM
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
nap (1.1.0)
net-dns (0.9.0)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (5.2.0)
@ -1173,6 +1173,7 @@ DEPENDENCIES
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
gitlab-markup (~> 1.7.0)
gitlab-net-dns (~> 0.9.1)
gitlab-peek (~> 0.0.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 2.7)
@ -1222,7 +1223,6 @@ DEPENDENCIES
mini_magick
minitest (~> 5.11.0)
nakayoshi_fork (~> 0.0.4)
net-dns (~> 0.9.0)
net-ldap
net-ntp
net-ssh (~> 5.2)

View file

@ -0,0 +1,40 @@
<script>
import { GlLink } from '@gitlab/ui';
export default {
components: {
GlLink,
},
props: {
artifacts: {
type: Array,
required: true,
},
},
};
</script>
<template>
<table class="table m-0">
<thead class="thead-white text-nowrap">
<tr class="d-none d-sm-table-row">
<th class="w-0"></th>
<th>{{ __('Artifact') }}</th>
<th class="w-50"></th>
<th>{{ __('Job') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in artifacts" :key="item.text">
<td class="w-0"></td>
<td>
<gl-link :href="item.url" target="_blank">{{ item.text }}</gl-link>
</td>
<td class="w-0"></td>
<td>
<gl-link :href="item.job_path">{{ item.job_name }}</gl-link>
</td>
</tr>
</tbody>
</table>
</template>

View file

@ -0,0 +1,36 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import ArtifactsList from './artifacts_list.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue';
import createStore from '../stores/artifacts_list';
export default {
store: createStore(),
components: {
ArtifactsList,
MrCollapsibleExtension,
},
props: {
endpoint: {
type: String,
required: true,
},
},
computed: {
...mapState(['artifacts', 'isLoading', 'hasError']),
...mapGetters(['title']),
},
created() {
this.setEndpoint(this.endpoint);
this.fetchArtifacts();
},
methods: {
...mapActions(['setEndpoint', 'fetchArtifacts']),
},
};
</script>
<template>
<mr-collapsible-extension :title="title" :is-loading="isLoading" :has-error="hasError">
<artifacts-list :artifacts="artifacts" />
</mr-collapsible-extension>
</template>

View file

@ -0,0 +1,81 @@
<script>
import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
GlLink,
GlLoadingIcon,
Icon,
},
props: {
title: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
hasError: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isCollapsed: true,
};
},
computed: {
arrowIconName() {
return this.isCollapsed ? 'angle-right' : 'angle-down';
},
ariaLabel() {
return this.isCollapsed ? __('Expand') : __('Collapse');
},
isButtonDisabled() {
return this.isLoading || this.hasError;
},
},
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
},
},
};
</script>
<template>
<div>
<div class="mr-widget-extension d-flex align-items-center pl-3">
<gl-button
class="btn-blank btn s32 square append-right-default"
:aria-label="ariaLabel"
:disabled="isButtonDisabled"
@click="toggleCollapsed"
>
<gl-loading-icon v-if="isLoading" />
<icon v-else :name="arrowIconName" class="js-icon" />
</gl-button>
<gl-button
variant="link"
class="js-title"
:disabled="isButtonDisabled"
:class="{ 'border-0': isButtonDisabled }"
@click="toggleCollapsed"
>
<template v-if="isCollapsed">{{ title }}</template>
<template v-else>{{ __('Collapse') }}</template>
</gl-button>
</div>
<div v-if="!isCollapsed" class="border-top js-slot-container">
<slot></slot>
</div>
</div>
</template>

View file

@ -1,5 +1,6 @@
<script>
import _ from 'underscore';
import ArtifactsApp from './artifacts_list_app.vue';
import Deployment from './deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
@ -15,6 +16,7 @@ import MrWidgetPipeline from './mr_widget_pipeline.vue';
export default {
name: 'MrWidgetPipelineContainer',
components: {
ArtifactsApp,
Deployment,
MrWidgetContainer,
MrWidgetPipeline,
@ -79,6 +81,9 @@ export default {
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<template v-slot:footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
<artifacts-app :endpoint="mr.exposedArtifactsPath" />
</div>
<div v-if="deployments.length" class="mr-widget-extension">
<deployment
v-for="deployment in deployments"

View file

@ -0,0 +1,74 @@
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import httpStatusCodes from '~/lib/utils/http_status';
import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const requestArtifacts = ({ commit }) => commit(types.REQUEST_ARTIFACTS);
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
export const fetchArtifacts = ({ state, dispatch }) => {
dispatch('requestArtifacts');
eTagPoll = new Poll({
resource: {
getArtifacts(endpoint) {
return axios.get(endpoint);
},
},
data: state.endpoint,
method: 'getArtifacts',
successCallback: ({ data, status }) => {
dispatch('receiveArtifactsSuccess', {
data,
status,
});
},
errorCallback: () => dispatch('receiveArtifactsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
axios
.get(state.endpoint)
.then(({ data, status }) => dispatch('receiveArtifactsSuccess', { data, status }))
.catch(() => dispatch('receiveArtifactsError'));
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartPolling');
} else {
dispatch('stopPolling');
}
});
};
export const receiveArtifactsSuccess = ({ commit }, response) => {
// With 204 we keep polling and don't update the state
if (response.status === httpStatusCodes.OK) {
commit(types.RECEIVE_ARTIFACTS_SUCCESS, response.data);
}
};
export const receiveArtifactsError = ({ commit }) => commit(types.RECEIVE_ARTIFACTS_ERROR);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,16 @@
import { s__, n__ } from '~/locale';
export const title = state => {
if (state.isLoading) {
return s__('BuildArtifacts|Loading artifacts');
}
if (state.hasError) {
return s__('BuildArtifacts|An error occurred while fetching the artifacts');
}
return n__('View exposed artifact', 'View %d exposed artifacts', state.artifacts.length);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,16 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import * as getters from './getters';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});

View file

@ -0,0 +1,5 @@
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_ARTIFACTS = 'REQUEST_ARTIFACTS';
export const RECEIVE_ARTIFACTS_SUCCESS = 'RECEIVE_ARTIFACTS_SUCCESS';
export const RECEIVE_ARTIFACTS_ERROR = 'RECEIVE_ARTIFACTS_ERROR';

View file

@ -0,0 +1,22 @@
import * as types from './mutation_types';
export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.REQUEST_ARTIFACTS](state) {
state.isLoading = true;
},
[types.RECEIVE_ARTIFACTS_SUCCESS](state, response) {
state.hasError = false;
state.isLoading = false;
state.artifacts = response;
},
[types.RECEIVE_ARTIFACTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.artifacts = [];
},
};

View file

@ -0,0 +1,8 @@
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
artifacts: [],
});

View file

@ -100,6 +100,7 @@ export default class MergeRequestStore {
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.testResultsPath = data.test_reports_path;
this.exposedArtifactsPath = data.exposed_artifacts_path;
this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);

View file

@ -508,7 +508,6 @@
.btn-group
%a.btn Edit
%a.btn.btn-danger Remove
.file-contenta.code
= render 'shared/file_highlight', blob: blob
%h2#markdown Markdown

View file

@ -0,0 +1,5 @@
---
title: Creates Vue and Vuex app to render exposed artifacts
merge_request: 17934
author:
type: added

View file

@ -377,6 +377,14 @@ The certificate to be used needs to be installed on all Gitaly nodes and on all
client nodes that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: **Note**
The self-signed certificate must specify the address you use to access the
Gitaly server. If you are addressing the Gitaly server by a hostname, you can
either use the Common Name field for this, or add it as a Subject Alternative
Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
NOTE: **Note:**
It is possible to configure Gitaly servers with both an
unencrypted listening address `listen_addr` and an encrypted listening

View file

@ -300,17 +300,17 @@ You will need to push a branch to those repositories, it doesn't work for forks.
The `review-docs-deploy*` job will:
1. Create a new branch in the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs)
project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
CE, etc.
1. Trigger a cross project pipeline and build the docs site with your changes
project named after the scheme: `docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID`,
where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ee` for
EE, `omnibus` for Omnibus GitLab, etc, and `CI_MERGE_REQUEST_IID` is the ID
of the respective merge request.
1. Trigger a cross project pipeline and build the docs site with your changes.
After a few minutes, the Review App will be deployed and you will be able to
preview the changes. The docs URL can be found in two places:
- In the merge request widget
- In the output of the `review-docs-deploy*` job, which also includes the
triggered pipeline so that you can investigate whether something went wrong
In case the review app URL returns 404, this means that either the site is not
yet deployed, or something went wrong with the remote pipeline. Give it a few
minutes and it should appear online, otherwise you can check the status of the
remote pipeline from the link in the merge request's job output.
If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
TIP: **Tip:**
Someone with no merge rights to the GitLab projects (think of forks from
@ -343,12 +343,11 @@ If you want to know the in-depth details, here's what's really happening:
1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/trigger-build-docs)
script with the `deploy` flag, which in turn:
1. Takes your branch name and applies the following:
- The slug of the branch name is used to avoid special characters since
ultimately this will be used by NGINX.
- The `preview-` prefix is added to avoid conflicts if there's a remote branch
with the same name that you created in the merge request.
- The final branch name is truncated to 42 characters to avoid filesystem
limitations with long branch names (> 63 chars).
- The `docs-preview-` prefix is added.
- The product slug is used to know the project the review app originated
from.
- The number of the merge request is added so that you can know by the
`gitlab-docs` branch name the merge request it originated from.
1. The remote branch is then created if it doesn't exist (meaning you can
re-run the manual job as many times as you want and this step will be skipped).
1. A new cross-project pipeline is triggered in the docs project.
@ -369,6 +368,7 @@ The following GitLab features are used among others:
- [Review Apps](../../ci/review_apps/index.md)
- [Artifacts](../../ci/yaml/README.md#artifacts)
- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
- [Pipelines for merge requests](../../ci/merge_request_pipelines/index.md)
## Testing

View file

@ -38,7 +38,7 @@ The current stages are:
## Default image
The default image is currently
`dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
`gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
It includes Ruby 2.6.3, Go 1.11, Git 2.22, Chrome 73, Node 12, Yarn 1.16,
PostgreSQL 9.6, and Graphics Magick 1.3.33.

View file

@ -1964,6 +1964,9 @@ msgstr ""
msgid "Arrange charts"
msgstr ""
msgid "Artifact"
msgstr ""
msgid "Artifact ID"
msgstr ""
@ -2661,6 +2664,12 @@ msgstr ""
msgid "Browse files"
msgstr ""
msgid "BuildArtifacts|An error occurred while fetching the artifacts"
msgstr ""
msgid "BuildArtifacts|Loading artifacts"
msgstr ""
msgid "Built-in"
msgstr ""
@ -18067,6 +18076,11 @@ msgstr ""
msgid "View epics list"
msgstr ""
msgid "View exposed artifact"
msgid_plural "View %d exposed artifacts"
msgstr[0] ""
msgstr[1] ""
msgid "View file @ "
msgstr ""

View file

@ -16,14 +16,12 @@ end
GITLAB_DOCS_REPO = 'gitlab-org/gitlab-docs'.freeze
#
# Truncate the remote docs branch name otherwise we hit the filesystem
# limit and the directory name where NGINX serves the site won't match
# the branch name.
# This is the branch that will be created in the gitlab-docs project.
# Name it after the product we're previewing and the ID of the MR that
# kicked the review app.
#
def docs_branch
# The maximum string length a file can have on a filesystem (ext4)
# is 63 characters. CI_ENVIRONMENT_SLUG is limited to 24 characters.
ENV["CI_ENVIRONMENT_SLUG"]
"docs-preview-#{slug}-#{ENV["CI_MERGE_REQUEST_IID"]}"
end
#

View file

@ -0,0 +1,121 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import createStore from '~/vue_merge_request_widget/stores/artifacts_list';
import { artifactsList } from './mock_data';
describe('Merge Requests Artifacts list app', () => {
let wrapper;
let mock;
const store = createStore();
const localVue = createLocalVue();
localVue.use(Vuex);
const actionSpies = {
fetchArtifacts: jest.fn(),
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
const createComponent = () => {
wrapper = mount(localVue.extend(ArtifactsListApp), {
propsData: {
endpoint: TEST_HOST,
},
store,
methods: {
...actionSpies,
},
localVue,
sync: false,
});
};
const findButtons = () => wrapper.findAll('button');
const findTitle = () => wrapper.find('.js-title');
const findTableRows = () => wrapper.findAll('tbody tr');
describe('while loading', () => {
beforeEach(() => {
createComponent();
store.dispatch('requestArtifacts');
return wrapper.vm.$nextTick();
});
it('renders a loading icon', () => {
const loadingIcon = wrapper.find(GlLoadingIcon);
expect(loadingIcon.exists()).toBe(true);
});
it('renders loading text', () => {
expect(findTitle().text()).toBe('Loading artifacts');
});
it('renders disabled buttons', () => {
const buttons = findButtons();
expect(buttons.at(0).attributes('disabled')).toBe('disabled');
expect(buttons.at(1).attributes('disabled')).toBe('disabled');
});
});
describe('with results', () => {
beforeEach(() => {
createComponent();
mock.onGet(wrapper.vm.$store.state.endpoint).reply(200, artifactsList, {});
store.dispatch('receiveArtifactsSuccess', {
data: artifactsList,
status: 200,
});
return wrapper.vm.$nextTick();
});
it('renders a title with the number of artifacts', () => {
expect(findTitle().text()).toBe('View 2 exposed artifacts');
});
it('renders both buttons enabled', () => {
const buttons = findButtons();
expect(buttons.at(0).attributes('disabled')).toBe(undefined);
expect(buttons.at(1).attributes('disabled')).toBe(undefined);
});
describe('on click', () => {
it('renders the list of artifacts', () => {
findTitle().trigger('click');
wrapper.vm.$nextTick(() => {
expect(findTableRows().length).toEqual(2);
});
});
});
});
describe('with error', () => {
beforeEach(() => {
createComponent();
mock.onGet(wrapper.vm.$store.state.endpoint).reply(500, {}, {});
store.dispatch('receiveArtifactsError');
return wrapper.vm.$nextTick();
});
it('renders the error state', () => {
expect(findTitle().text()).toBe('An error occurred while fetching the artifacts');
});
it('renders disabled buttons', () => {
const buttons = findButtons();
expect(buttons.at(0).attributes('disabled')).toBe('disabled');
expect(buttons.at(1).attributes('disabled')).toBe('disabled');
});
});
});

View file

@ -0,0 +1,61 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import ArtifactsList from '~/vue_merge_request_widget/components/artifacts_list.vue';
import { artifactsList } from './mock_data';
describe('Artifacts List', () => {
let wrapper;
const localVue = createLocalVue();
const data = {
artifacts: artifactsList,
};
const mountComponent = props => {
wrapper = shallowMount(localVue.extend(ArtifactsList), {
propsData: {
...props,
},
sync: false,
localVue,
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
mountComponent(data);
});
it('renders list of artifacts', () => {
expect(wrapper.findAll('tbody tr').length).toEqual(data.artifacts.length);
});
it('renders link for the artifact', () => {
expect(wrapper.find(GlLink).attributes('href')).toEqual(data.artifacts[0].url);
});
it('renders artifact name', () => {
expect(wrapper.find(GlLink).text()).toEqual(data.artifacts[0].text);
});
it('renders job url', () => {
expect(
wrapper
.findAll(GlLink)
.at(1)
.attributes('href'),
).toEqual(data.artifacts[0].job_path);
});
it('renders job name', () => {
expect(
wrapper
.findAll(GlLink)
.at(1)
.text(),
).toEqual(data.artifacts[0].job_name);
});
});

View file

@ -0,0 +1,15 @@
// eslint-disable-next-line import/prefer-default-export
export const artifactsList = [
{
text: 'result.txt',
url: 'bar',
job_name: 'generate-artifact',
job_path: 'bar',
},
{
text: 'foo.txt',
url: 'foo',
job_name: 'foo-artifact',
job_path: 'foo',
},
];

View file

@ -0,0 +1,105 @@
import { mount } from '@vue/test-utils';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
import { GlLoadingIcon } from '@gitlab/ui';
describe('Merge Request Collapsible Extension', () => {
let wrapper;
const data = {
title: 'View artifacts',
};
const mountComponent = props => {
wrapper = mount(MrCollapsibleSection, {
propsData: {
...props,
},
slots: {
default: '<div class="js-slot">Foo</div>',
},
});
};
const findTitle = () => wrapper.find('.js-title');
afterEach(() => {
wrapper.destroy();
});
describe('while collapsed', () => {
beforeEach(() => {
mountComponent(data);
});
it('renders provided title', () => {
expect(findTitle().text()).toBe(data.title);
});
it('renders angle-right icon', () => {
expect(wrapper.vm.arrowIconName).toBe('angle-right');
});
describe('onClick', () => {
beforeEach(() => {
wrapper.find('button').trigger('click');
});
it('rendes the provided slot', () => {
expect(wrapper.find('.js-slot').isVisible()).toBe(true);
});
it('renders `Collapse` as the title', () => {
expect(findTitle().text()).toBe('Collapse');
});
it('renders angle-down icon', () => {
expect(wrapper.vm.arrowIconName).toBe('angle-down');
});
});
});
describe('while loading', () => {
beforeEach(() => {
mountComponent(Object.assign({}, data, { isLoading: true }));
});
it('renders the buttons disabled', () => {
expect(
wrapper
.findAll('button')
.at(0)
.attributes('disabled'),
).toEqual('disabled');
expect(
wrapper
.findAll('button')
.at(1)
.attributes('disabled'),
).toEqual('disabled');
});
it('renders loading spinner', () => {
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
});
});
describe('with error', () => {
beforeEach(() => {
mountComponent(Object.assign({}, data, { hasError: true }));
});
it('renders the buttons disabled', () => {
expect(
wrapper
.findAll('button')
.at(0)
.attributes('disabled'),
).toEqual('disabled');
expect(
wrapper
.findAll('button')
.at(1)
.attributes('disabled'),
).toEqual('disabled');
});
});
});

View file

@ -0,0 +1,32 @@
import { title } from '~/vue_merge_request_widget/stores/artifacts_list/getters';
import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
import { artifactsList } from '../../components/mock_data';
describe('Artifacts Store Getters', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('title', () => {
describe('when is loading', () => {
it('returns loading message', () => {
localState.isLoading = true;
expect(title(localState)).toBe('Loading artifacts');
});
});
describe('when has error', () => {
it('returns error message', () => {
localState.hasError = true;
expect(title(localState)).toBe('An error occurred while fetching the artifacts');
});
});
describe('when it has artifacts', () => {
it('returns artifacts message', () => {
localState.artifacts = artifactsList;
expect(title(localState)).toBe('View 2 exposed artifacts');
});
});
});
});

View file

@ -0,0 +1,78 @@
import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations';
import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
describe('Artifacts Store Mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('SET_ENDPOINT', () => {
it('should set endpoint', () => {
mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
expect(stateCopy.endpoint).toEqual('endpoint.json');
});
});
describe('REQUEST_ARTIFACTS', () => {
it('should set isLoading to true', () => {
mutations[types.REQUEST_ARTIFACTS](stateCopy);
expect(stateCopy.isLoading).toEqual(true);
});
});
describe('REECEIVE_ARTIFACTS_SUCCESS', () => {
const artifacts = [
{
text: 'result.txt',
url: 'asda',
job_name: 'generate-artifact',
job_path: 'asda',
},
{
text: 'file.txt',
url: 'asda',
job_name: 'generate-artifact',
job_path: 'asda',
},
];
beforeEach(() => {
mutations[types.RECEIVE_ARTIFACTS_SUCCESS](stateCopy, artifacts);
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to false', () => {
expect(stateCopy.hasError).toEqual(false);
});
it('should set list of artifacts', () => {
expect(stateCopy.artifacts).toEqual(artifacts);
});
});
describe('RECEIVE_ARTIFACTS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_ARTIFACTS_ERROR](stateCopy);
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(stateCopy.hasError).toEqual(true);
});
it('should set list of artifacts as empty array', () => {
expect(stateCopy.artifacts).toEqual([]);
});
});
});

View file

@ -1,6 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { mockStore } from '../mock_data';
describe('MrWidgetPipelineContainer', () => {
@ -87,4 +88,10 @@ describe('MrWidgetPipelineContainer', () => {
expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
});
});
describe('with artifacts path', () => {
it('renders the artifacts app', () => {
expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
});
});
});

View file

@ -289,4 +289,5 @@ export const mockStore = {
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
exposedArtifactsPath: 'exposed_artifacts.json',
};

View file

@ -0,0 +1,165 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setEndpoint,
requestArtifacts,
clearEtagPoll,
stopPolling,
fetchArtifacts,
receiveArtifactsSuccess,
receiveArtifactsError,
} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
describe('Artifacts App Store Actions', () => {
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe('setEndpoint', () => {
it('should commit SET_ENDPOINT mutation', done => {
testAction(
setEndpoint,
'endpoint.json',
mockedState,
[{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
[],
done,
);
});
});
describe('requestArtifacts', () => {
it('should commit REQUEST_ARTIFACTS mutation', done => {
testAction(
requestArtifacts,
null,
mockedState,
[{ type: types.REQUEST_ARTIFACTS }],
[],
done,
);
});
});
describe('fetchArtifacts', () => {
let mock;
beforeEach(() => {
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
stopPolling();
clearEtagPoll();
});
describe('success', () => {
it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
{
text: 'result.txt',
url: 'asda',
job_name: 'generate-artifact',
job_path: 'asda',
},
]);
testAction(
fetchArtifacts,
null,
mockedState,
[],
[
{
type: 'requestArtifacts',
},
{
payload: {
data: [
{
text: 'result.txt',
url: 'asda',
job_name: 'generate-artifact',
job_path: 'asda',
},
],
status: 200,
},
type: 'receiveArtifactsSuccess',
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
it('dispatches requestArtifacts and receiveArtifactsError ', done => {
testAction(
fetchArtifacts,
null,
mockedState,
[],
[
{
type: 'requestArtifacts',
},
{
type: 'receiveArtifactsError',
},
],
done,
);
});
});
});
describe('receiveArtifactsSuccess', () => {
it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => {
testAction(
receiveArtifactsSuccess,
{ data: { summary: {} }, status: 200 },
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
[],
done,
);
});
it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => {
testAction(
receiveArtifactsSuccess,
{ data: { summary: {} }, status: 204 },
mockedState,
[],
[],
done,
);
});
});
describe('receiveArtifactsError', () => {
it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => {
testAction(
receiveArtifactsError,
null,
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_ERROR }],
[],
done,
);
});
});
});