Commit 7c38405b authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 1fa79760
......@@ -55,7 +55,7 @@ eslint-report.html
/dump.rdb
/jsconfig.json
/log/*.log*
/node_modules/
/node_modules
/nohup.out
/public/assets/
/public/uploads.*
......
......@@ -6,3 +6,8 @@ export const RUNNING = 'running';
export const SUCCESS = 'success';
export const FAILED = 'failed';
export const CANCELED = 'canceled';
// ACTION STATUSES
export const STOPPING = 'stopping';
export const DEPLOYING = 'deploying';
export const REDEPLOYING = 'redeploying';
<script>
import { __, s__ } from '~/locale';
import DeploymentActions from './deployment_actions.vue';
import DeploymentInfo from './deployment_info.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import DeploymentStopButton from './deployment_stop_button.vue';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED } from './constants';
export default {
// name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Deployment',
components: {
DeploymentActions,
DeploymentInfo,
DeploymentStopButton,
DeploymentViewButton,
},
props: {
deployment: {
......@@ -40,38 +37,14 @@ export default {
},
},
computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY;
},
computedDeploymentStatus() {
if (this.deployment.status === CREATED) {
return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
}
return this.deployment.status;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
isManual() {
return Boolean(
this.deployment.details &&
this.deployment.details.playable_build &&
this.deployment.details.playable_build.play_path,
);
},
isDeployInProgress() {
return this.deployment.status === RUNNING;
return Boolean(this.deployment.details?.playable_build?.play_path);
},
},
};
......@@ -87,22 +60,12 @@ export default {
:deployment="deployment"
:show-metrics="showMetrics"
/>
<div>
<!-- show appropriate version of review app button -->
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
<!-- if it is stoppable, show stop -->
<deployment-stop-button
v-if="deployment.stop_url"
:is-deploy-in-progress="isDeployInProgress"
:stop-url="deployment.stop_url"
/>
</div>
<deployment-actions
:deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
</div>
</div>
</div>
......
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
export default {
name: 'DeploymentActionButton',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
actionsConfiguration: {
type: Object,
required: true,
},
actionInProgress: {
type: String,
required: false,
default: null,
},
buttonTitle: {
type: String,
required: false,
default: '',
},
computedDeploymentStatus: {
type: String,
required: true,
},
containerClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
isActionInProgress() {
return Boolean(this.computedDeploymentStatus === RUNNING || this.actionInProgress);
},
actionInProgressTooltip() {
switch (this.actionInProgress) {
case this.actionsConfiguration.actionName:
return this.actionsConfiguration.busyText;
case null:
return '';
default:
return __('Another action is currently in progress');
}
},
isLoading() {
return this.actionInProgress === this.actionsConfiguration.actionName;
},
},
};
</script>
<template>
<span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0">
<gl-button
v-gl-tooltip
:title="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`btn btn-default btn-sm inline prepend-left-4 ${containerClasses}`"
@click="$emit('click')"
>
<span class="d-inline-flex align-items-baseline">
<slot> </slot>
</span>
</gl-button>
</span>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
import DeploymentActionButton from './deployment_action_button.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants';
export default {
name: 'DeploymentActions',
components: {
DeploymentActionButton,
DeploymentViewButton,
GlIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
computedDeploymentStatus: {
type: String,
required: true,
},
deployment: {
type: Object,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
data() {
return {
actionInProgress: null,
constants: {
STOPPING,
DEPLOYING,
REDEPLOYING,
},
};
},
computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY && Boolean(this.playPath);
},
canBeManuallyRedeployed() {
return this.computedDeploymentStatus === FAILED && Boolean(this.redeployPath);
},
shouldShowManualButtons() {
return this.glFeatures.deployFromFooter;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
playPath() {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
return this.deployment.details?.playable_build?.retry_path;
},
stopUrl() {
return this.deployment.stop_url;
},
},
actionsConfiguration: {
[STOPPING]: {
actionName: STOPPING,
buttonText: s__('MrDeploymentActions|Stop environment'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to stop this environment?'),
errorMessage: __('Something went wrong while stopping this environment. Please try again.'),
},
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: s__('MrDeploymentActions|Deploy'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: s__('MrDeploymentActions|Re-deploy'),
busyText: __('This environment is being re-deployed'),
confirmMessage: __('Are you sure you want to re-deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
},
methods: {
executeAction(endpoint, { actionName, confirmMessage, errorMessage }) {
const isConfirmed = confirm(confirmMessage); //eslint-disable-line
if (isConfirmed) {
this.actionInProgress = actionName;
MRWidgetService.executeInlineAction(endpoint)
.then(resp => {
const redirectUrl = resp?.data?.redirect_url;
if (redirectUrl) {
visitUrl(redirectUrl);
}
})
.catch(() => {
createFlash(errorMessage);
})
.finally(() => {
this.actionInProgress = null;
});
}
},
stopEnvironment() {
this.executeAction(this.stopUrl, this.$options.actionsConfiguration[STOPPING]);
},
deployManually() {
this.executeAction(this.playPath, this.$options.actionsConfiguration[DEPLOYING]);
},
redeploy() {
this.executeAction(this.redeployPath, this.$options.actionsConfiguration[REDEPLOYING]);
},
},
};
</script>
<template>
<div>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyDeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-deploy-action"
@click="deployManually"
>
<gl-icon name="play" />
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyRedeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-redeploy-action"
@click="redeploy"
>
<gl-icon name="repeat" />
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
:action-in-progress="actionInProgress"
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
:button-title="$options.actionsConfiguration[constants.STOPPING].buttonText"
container-classes="js-stop-env"
@click="stopEnvironment"
>
<gl-icon name="stop" />
</deployment-action-button>
</div>
</template>
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'DeploymentStopButton',
components: {
LoadingButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
isDeployInProgress: {
type: Boolean,
required: true,
},
stopUrl: {
type: String,
required: true,
},
},
data() {
return {
isStopping: false,
};
},
computed: {
deployInProgressTooltip() {
return this.isDeployInProgress
? __('Stopping this environment is currently not possible as a deployment is in progress')
: '';
},
},
methods: {
stopEnvironment() {
const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
this.isStopping = true;
MRWidgetService.stopEnvironment(this.stopUrl)
.then(res => res.data)
.then(data => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
this.isStopping = false;
})
.catch(() => {
createFlash(
__('Something went wrong while stopping this environment. Please try again.'),
);
this.isStopping = false;
});
}
},
},
};
</script>
<template>
<span v-gl-tooltip :title="deployInProgressTooltip" class="d-inline-block" tabindex="0">
<loading-button
v-gl-tooltip
:loading="isStopping"
:disabled="isDeployInProgress"
:title="__('Stop environment')"
container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
@click="stopEnvironment"
>
<icon name="stop" />
</loading-button>
</span>
</template>
......@@ -54,7 +54,7 @@ export default class MRWidgetService {
return axios.post(this.endpoints.rebasePath);
}
static stopEnvironment(url) {
static executeInlineAction(url) {
return axios.post(url);
}
......
......@@ -20,6 +20,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true)
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:single_mr_diff_view, @project)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end
......
......@@ -141,3 +141,5 @@ class GitlabSchema < GraphQL::Schema
end
end
end
GitlabSchema.prepend_if_ee('EE::GitlabSchema')
......@@ -43,11 +43,7 @@ module MergeRequests
abort_auto_merge(merge_request, 'target branch was changed')
end
if merge_request.assignees != old_assignees
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
handle_assignees_change(merge_request, old_assignees) if merge_request.assignees != old_assignees
if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch')
......@@ -120,6 +116,12 @@ module MergeRequests
end
end
def handle_assignees_change(merge_request, old_assignees)
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
......
---
title: Add deploy and re-deploy buttons to deployments
merge_request: 25427
author:
type: added
# frozen_string_literal: true
class AddMergeRequestMetricsFirstReassignedAt < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :merge_request_metrics, :first_reassigned_at, :datetime_with_timezone
end
end
def down
with_lock_retries do
remove_column :merge_request_metrics, :first_reassigned_at, :datetime_with_timezone
end
end
end
# frozen_string_literal: true
class AddMergeRequestAssigneeCreatedAt < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :merge_request_assignees, :created_at, :datetime_with_timezone
end
end
def down
with_lock_retries do
remove_column :merge_request_assignees, :created_at
end
end
end
......@@ -2450,6 +2450,7 @@ ActiveRecord::Schema.define(version: 2020_03_06_170531) do
create_table "merge_request_assignees", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "merge_request_id", null: false
t.datetime_with_timezone "created_at"
t.index ["merge_request_id", "user_id"], name: "index_merge_request_assignees_on_merge_request_id_and_user_id", unique: true
t.index ["merge_request_id"], name: "index_merge_request_assignees_on_merge_request_id"
t.index ["user_id"], name: "index_merge_request_assignees_on_user_id"
......@@ -2564,6 +2565,7 @@ ActiveRecord::Schema.define(version: 2020_03_06_170531) do
t.integer "modified_paths_size"
t.integer "commits_count"
t.datetime_with_timezone "first_approved_at"
t.datetime_with_timezone "first_reassigned_at"
t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at"
t.index ["latest_closed_at"], name: "index_merge_request_metrics_on_latest_closed_at", where: "(latest_closed_at IS NOT NULL)"
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id"
......
......@@ -1948,8 +1948,8 @@ type Epic implements Noteable {
descendantCounts: EpicDescendantCount
"""
Total weight of open and closed descendant epic's issues. Available only when
feature flag `unfiltered_epic_aggregates` is enabled.
Total weight of open and closed issues in the epic and its descendants.
Available only when feature flag `unfiltered_epic_aggregates` is enabled.
"""
descendantWeightSum: EpicDescendantWeights
......
......@@ -317,7 +317,7 @@ Represents an epic.
| `closedAt` | Time | Timestamp of the epic's closure |
| `createdAt` | Time | Timestamp of the epic's creation |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed descendant epic's issues. Available only when feature flag `unfiltered_epic_aggregates` is enabled. |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. Available only when feature flag `unfiltered_epic_aggregates` is enabled. |
| `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
......
......@@ -29,6 +29,9 @@ The squashed commit's commit message will be either:
- Taken from the first multi-line commit message in the merge.
- The merge request's title if no multi-line commit message is found.
NOTE: **Note:**
This only takes effect if there are at least 2 commits. As there is nothing to squash, the commit message does not change if there is only 1 commit.
It can be customized before merging a merge request.
![A squash commit message editor](img/squash_mr_message.png)
......
......@@ -200,7 +200,9 @@ module Gitlab
end
def subject_starts_with_lowercase?
first_char = subject[0]
first_char = subject.sub(/\A\[.+\]\s/, '')[0]
first_char_downcased = first_char.downcase
return true unless ('a'..'z').cover?(first_char_downcased)
first_char.downcase == first_char
end
......
......@@ -2038,6 +2038,9 @@ msgstr ""
msgid "Anonymous"
msgstr ""
msgid "Another action is currently in progress"
msgstr ""
msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time"
msgstr ""
......@@ -2283,6 +2286,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
......@@ -2298,6 +2304,9 @@ msgstr ""
msgid "Are you sure you want to permanently delete this license?"
msgstr ""
msgid "Are you sure you want to re-deploy this environment?"
msgstr ""
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr ""
......@@ -12799,6 +12808,15 @@ msgstr ""
msgid "Moves this issue to %{path_to_project}."
msgstr ""
msgid "MrDeploymentActions|Deploy"
msgstr ""
msgid "MrDeploymentActions|Re-deploy"
msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multiple issue boards"
msgstr ""
......@@ -18243,6 +18261,9 @@ msgstr ""
msgid "Something went wrong while deleting your note. Please try again."
msgstr ""
msgid "Something went wrong while deploying this environment. Please try again."
msgstr ""
msgid "Something went wrong while editing your comment. Please try again."
msgstr ""
......@@ -18741,9 +18762,6 @@ msgstr ""
msgid "Stop Terminal"
msgstr ""
msgid "Stop environment"
msgstr ""
msgid "Stop impersonation"
msgstr ""
......@@ -18753,9 +18771,6 @@ msgstr ""
msgid "Stopped"
msgstr ""
msgid "Stopping this environment is currently not possible as a deployment is in progress"
msgstr ""
msgid "Stopping..."
msgstr ""
......@@ -20002,6 +20017,12 @@ msgstr ""
msgid "This environment has no deployments yet."
msgstr ""
msgid "This environment is being deployed"
msgstr ""
msgid "This environment is being re-deployed"
msgstr ""
msgid "This epic already has the maximum number of child epics."
msgstr ""
......
......@@ -81,10 +81,13 @@ class AutomatedCleanup
release = Quality::HelmClient::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace)
releases_to_delete << release
end
elsif environment.state != 'stopped' && deployed_at < stop_threshold
stop_environment(environment, deployment)
else
print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
if deployed_at >= stop_threshold
print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
else
environment_state = fetch_environment(environment)&.state
stop_environment(environment, deployment) if environment_state && environment_state != 'stopped'
end
end
checked_environments << environment.slug
......@@ -116,12 +119,19 @@ class AutomatedCleanup
private
def fetch_environment(environment)
gitlab.environment(project_path, environment.id)
rescue Errno::ETIMEDOUT => ex
puts "Failed to fetch '#{environment.name}' / '#{environment.slug}' (##{environment.id}):\n#{ex.message}"
nil
end
def delete_environment(environment, deployment)
print_release_state(subject: 'Review app', release_name: environment.slug, release_date: deployment.created_at, action: 'deleting')
gitlab.delete_environment(project_path, environment.id)
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.slug}' is forbidden: skipping it"
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
end
def stop_environment(environment, deployment)
......@@ -129,7 +139,7 @@ class AutomatedCleanup
gitlab.stop_environment(project_path, environment.id)
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.slug}' is forbidden: skipping it"
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
end
def helm_releases
......
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState({
notes: [],
targetNoteHash: null,
lastFetchedAt: null,
notesData: {},
userData: {},
noteableData: {},
});
};
import $ from 'jquery';
import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import Api from '~/api';
import actionsModule, * as actions from '~/notes/stores/actions';
import Flash from '~/flash';
import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
......@@ -19,21 +19,20 @@ import {
import axios from '~/lib/utils/axios_utils';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
describe('Actions Notes Store', () => {
let commit;
let dispatch;
let state;
let store;
let flashSpy;
let axiosMock;
beforeEach(() => {
store = createStore();
commit = jasmine.createSpy('commit');
dispatch = jasmine.createSpy('dispatch');
commit = jest.fn();
dispatch = jest.fn();
state = {};
flashSpy = spyOnDependency(actionsModule, 'Flash');
axiosMock = new AxiosMockAdapter(axios);
});
......@@ -244,10 +243,10 @@ describe('Actions Notes Store', () => {
});
describe('poll', () => {
beforeEach(done => {
jasmine.clock().install();
jest.useFakeTimers();
spyOn(axios, 'get').and.callThrough();
beforeEach(done => {
jest.spyOn(axios, 'get');
store
.dispatch('setNotesData', notesDataMock)
......@@ -255,10 +254,6 @@ describe('Actions Notes Store', () => {
.catch(done.fail);
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('calls service with last fetched state', done => {
axiosMock
.onAny()
......@@ -271,7 +266,7 @@ describe('Actions Notes Store', () => {
expect(axios.get).toHaveBeenCalled();
expect(store.state.lastFetchedAt).toBe('123456');
jasmine.clock().tick(1500);
jest.advanceTimersByTime(1500);
})
.then(
() =>
......@@ -280,8 +275,8 @@ describe('Actions Notes Store', () => {
}),
)
.then(() => {
expect(axios.get.calls.count()).toBe(2);
expect(axios.get.calls.mostRecent().args[1].headers).toEqual({
expect(axios.get.mock.calls.length).toBe(2);
expect(axios.get.mock.calls[axios.get.mock.calls.length - 1][1].headers).toEqual({
'X-Last-Fetched-At': '123456',
});
})
......@@ -310,13 +305,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
$('body').attr('data-page', '');
document.body.setAttribute('data-page', '');
});
afterEach(() => {
axiosMock.restore();
$('body').attr('data-page', '');
document.body.setAttribute('data-page', '');
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => {
......@@ -347,7 +342,7 @@ describe('Actions Notes Store', () => {
it('dispatches removeDiscussionsFromDiff on merge request page', done => {
const note = { path: endpoint, id: 1 };
$('body').attr('data-page', 'projects:merge_requests:show');
document.body.setAttribute('data-page', 'projects:merge_requests:show');
testAction(
actions.removeNote,
......@@ -381,13 +376,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
$('body').attr('data-page', '');
document.body.setAttribute('data-page', '');
});
afterEach(() => {
axiosMock.restore();
$('body').attr('data-page', '');
document.body.setAttribute('data-page', '');
});
it('dispatches removeNote', done => {
......@@ -534,7 +529,7 @@ describe('Actions Notes Store', () => {
describe('updateMergeRequestWidget', () => {
it('calls mrWidget checkStatus', () => {
spyOn(mrWidgetEventHub, '$emit');
jest.spyOn(mrWidgetEventHub, '$emit').mockImplementation(() => {});
actions.updateMergeRequestWidget();
......@@ -589,7 +584,7 @@ describe('Actions Notes Store', () => {
actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
expect(commit.calls.allArgs()).toEqual([[mutationTypes.UPDATE_NOTE, note]]);
expect(commit.mock.calls).toEqual([[mutationTypes.UPDATE_NOTE, note]]);
});
it('Creates a new note if none exisits', () => {
......@@ -597,7 +592,7 @@ describe('Actions Notes Store', () => {
const getters = { notesById: {} };
actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]);
expect(commit.mock.calls).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]);
});
describe('Discussion notes', () => {
......@@ -619,7 +614,7 @@ describe('Actions Notes Store', () => {
actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]);
expect(commit.calls.allArgs()).toEqual([
expect(commit.mock.calls).toEqual([
[mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, discussionNote],
]);
});
......@@ -630,7 +625,7 @@ describe('Actions Notes Store', () => {
actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [diffNote]);
expect(dispatch.calls.allArgs()).toEqual([
expect(dispatch.mock.calls).toEqual([
['fetchDiscussions', { path: state.notesData.discussionsPath }],
]);
});
......@@ -645,7 +640,7 @@ describe('Actions Notes Store', () => {
actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]);
expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]);
expect(commit.mock.calls).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]);
});
});
});
......@@ -770,7 +765,7 @@ describe('Actions Notes Store', () => {
.then(() => done.fail('Expected error to be thrown!'))
.catch(err => {
expect(err).toBe(error);
expect(flashSpy).not.toHaveBeenCalled();
expect(Flash).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
......@@ -792,7 +787,7 @@ describe('Actions Notes Store', () => {
)
.then(resp => {
expect(resp.hasFlash).toBe(true);
expect(flashSpy).toHaveBeenCalledWith(
expect(Flash).toHaveBeenCalledWith(
'Your comment could not be submitted because something went wrong',
'alert',
flashContainer,
......@@ -818,7 +813,7 @@ describe('Actions Notes Store', () => {
)
.then(data => {
expect(data).toBe(res);
expect(flashSpy).not.toHaveBeenCalled();
expect(Flash).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
......@@ -833,9 +828,8 @@ describe('Actions Notes Store', () => {
let flashContainer;
beforeEach(() => {
spyOn(Api, 'applySuggestion');
dispatch.and.returnValue(Promise.resolve());
Api.applySuggestion.and.returnValue(Promise.resolve());
jest.spyOn(Api, 'applySuggestion').mockReturnValue(Promise.resolve());
dispatch.mockReturnValue(Promise.resolve());
flashContainer = {};
});
......@@ -852,32 +846,32 @@ describe('Actions Notes Store', () => {
it('when service success, commits and resolves discussion', done => {
testSubmitSuggestion(done, () => {
expect(commit.calls.allArgs()).toEqual([
expect(commit.mock.calls).toEqual([
[mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
]);
expect(dispatch.calls.allArgs()).toEqual([['resolveDiscussion', { discussionId }]]);
expect(flashSpy).not.toHaveBeenCalled();
expect(dispatch.mock.calls).toEqual([['resolveDiscussion', { discussionId }]]);
expect(Flash).not.toHaveBeenCalled();
});
});
it('when service fails, flashes error message', done => {
const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
Api.applySuggestion.and.returnValue(Promise.reject(response));
Api.applySuggestion.mockReturnValue(Promise.reject(response));
testSubmitSuggestion(done, () => {
expect(commit).not.toHaveBeenCalled();
expect(dispatch).not.toHaveBeenCalled();
expect(flashSpy).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer);
expect(Flash).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer);
});
});
it('when resolve discussion fails, fail gracefully', done => {
dispatch.and.returnValue(Promise.reject());
dispatch.mockReturnValue(Promise.reject());
testSubmitSuggestion(done, () => {
expect(flashSpy).not.toHaveBeenCalled();
expect(Flash).not.toHaveBeenCalled();
});
});
});
......@@ -887,13 +881,13 @@ describe('Actions Notes Store', () => {
const filter = 0;
beforeEach(() => {
dispatch.and.returnValue(new Promise(() => {}));
dispatch.mockReturnValue(new Promise(() => {}));
});
it('fetches discussions with filter and persistFilter false', () => {
actions.filterDiscussion({ dispatch }, { path, filter, persistFilter: false });
expect(dispatch.calls.allArgs()).toEqual([
expect(dispatch.mock.calls).toEqual([
['setLoadingState', true],
['fetchDiscussions', { path, filter, persistFilter: false }],
]);
......@@ -902,7 +896,7 @@ describe('Actions Notes Store', () => {
it('fetches discussions with filter and persistFilter true', () => {
actions.filterDiscussion({ dispatch }, { path, filter, persistFilter: true });
expect(dispatch.calls.allArgs()).toEqual([
expect(dispatch.mock.calls).toEqual([
['setLoadingState', true],
['fetchDiscussions', { path, filter, persistFilter: true }],
]);
......
import { mount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlButton } from '@gitlab/ui';
import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
import {
CREATED,
RUNNING,
DEPLOYING,
REDEPLOYING,
} from '~/vue_merge_request_widget/components/deployment/constants';
import { actionButtonMocks } from './deployment_mock_data';
const baseProps = {
actionsConfiguration: actionButtonMocks[DEPLOYING],
actionInProgress: null,
computedDeploymentStatus: CREATED,
};
describe('Deployment action button', () => {
let wrapper;
const factory = (options = {}) => {
wrapper = mount(DeploymentActionButton, {
...options,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when passed only icon', () => {
beforeEach(() => {
factory({
propsData: baseProps,
slots: { default: ['<gl-icon name="stop" />'] },
stubs: {
'gl-icon': GlIcon,
},
});
});
it('renders slot correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
});
});
describe('when passed multiple items', () => {
beforeEach(() => {
factory({
propsData: baseProps,
slots: {
default: ['<gl-icon name="play" />', `<span>${actionButtonMocks[DEPLOYING]}</span>`],
},
stubs: {
'gl-icon': GlIcon,
},
});
});
it('renders slot correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
expect(wrapper.text()).toContain(actionButtonMocks[DEPLOYING]);
});
});
describe('when its action is in progress', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[DEPLOYING].actionName,
},
});
});
it('is disabled and shows the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when another action is in progress', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[REDEPLOYING].actionName,
},
});
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when action status is running', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[REDEPLOYING].actionName,
computedDeploymentStatus: RUNNING,
},
});
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when no action is in progress', () => {
beforeEach(() => {
factory({
propsData: baseProps,
});
});
it('is not disabled nor does it show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
import {
CREATED,
MANUAL_DEPLOY,
FAILED,
DEPLOYING,
REDEPLOYING,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
import {
actionButtonMocks,
deploymentMockData,
playDetails,
retryDetails,
} from './deployment_mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('DeploymentAction component', () => {
let wrapper;
let executeActionSpy;
const factory = (options = {}) => {
// This destroys any wrappers created before a nested call to factory reassigns it
if (wrapper && wrapper.destroy) {
wrapper.destroy();
}
wrapper = mount(DeploymentActions, {
...options,
provide: { glFeatures: { deployFromFooter: true } },
});
};
const findStopButton = () => wrapper.find('.js-stop-env');
const findDeployButton = () => wrapper.find('.js-manual-deploy-action');
const findRedeployButton = () => wrapper.find('.js-manual-redeploy-action');
beforeEach(() => {
executeActionSpy = jest.spyOn(MRWidgetService, 'executeInlineAction');
factory({
propsData: {
computedDeploymentStatus: CREATED,
deployment: deploymentMockData,
showVisualReviewApp: false,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('actions do not appear when conditions are unmet', () => {
describe('when there is no stop_url', () => {
beforeEach(() => {
factory({
propsData: {
computedDeploymentStatus: CREATED,
deployment: {
...deploymentMockData,
stop_url: null,
},
showVisualReviewApp: false,
},
});
});
it('the stop button does not appear', () => {
expect(findStopButton().exists()).toBe(false);
});
});
describe('when there is no play_path in details', () => {
it('the manual deploy button does not appear', () => {
expect(findDeployButton().exists()).toBe(false);
});
});
describe('when there is no retry_path in details', () => {
it('the manual redeploy button does not appear', () => {
expect(findRedeployButton().exists()).toBe(false);
});
});
});
describe('when conditions are met', () => {
describe.each`
configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint
${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url}
${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path}
${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path}
`(
'$configConst action',
({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
describe(`${configConst} action`, () => {
const confirmAction = () => {
jest.spyOn(window, 'confirm').mockReturnValueOnce(true);
finderFn().trigger('click');
};
const rejectAction = () => {
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
finderFn().trigger('click');
};
beforeEach(() => {
factory({
propsData: {
computedDeploymentStatus,
deployment: {
...deploymentMockData,
details: displayConditionChanges,
},
showVisualReviewApp: false,
},
});
});
it('the button is rendered', () => {
expect(finderFn().exists()).toBe(true);
});
describe('when clicked', () => {
describe('should show a confirm dialog but not call executeInlineAction when declined', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
rejectAction();
});
it('should show the confirm dialog', () => {
expect(window.confirm).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
);
});
it('should not execute the action', () => {
expect(MRWidgetService.executeInlineAction).not.toHaveBeenCalled();
});
});
describe('should show a confirm dialog and call executeInlineAction when accepted', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
confirmAction();
});
it('should show the confirm dialog', () => {
expect(window.confirm).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
);
});
it('should execute the action with expected URL', () => {
expect(MRWidgetService.executeInlineAction).toHaveBeenCalled();
expect(MRWidgetService.executeInlineAction).toHaveBeenCalledWith(endpoint);
});
it('should not throw an error', () => {
expect(createFlash).not.toHaveBeenCalled();
});
describe('response includes redirect_url', () => {
const url = '/root/example';
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce({
data: { redirect_url: url },
});
confirmAction();
});
it('calls visit url with the redirect_url', () => {
expect(visitUrl).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(url);
});
});
describe('it should call the executeAction method ', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
confirmAction();
});
it('calls with the expected arguments', () => {
expect(wrapper.vm.executeAction).toHaveBeenCalled();
expect(wrapper.vm.executeAction).toHaveBeenCalledWith(
endpoint,
actionButtonMocks[configConst],
);
});
});
describe('when executeInlineAction errors', () => {
beforeEach(() => {
executeActionSpy.mockRejectedValueOnce();
confirmAction();
});
it('should call createFlash with error message', () => {
expect(createFlash).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith(
actionButtonMocks[configConst].errorMessage,
);
});
});
});
});
});
},
);
});
});
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import {
DEPLOYING,
REDEPLOYING,
SUCCESS,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
const actionButtonMocks = {
[STOPPING]: {
actionName: STOPPING,
buttonText: 'Stop environment',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to stop this environment?',
errorMessage: 'Something went wrong while stopping this environment. Please try again.',
},
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: 'Deploy',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
},
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: 'Re-deploy',
busyText: 'This environment is being re-deployed',
confirmMessage: 'Are you sure you want to re-deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
},
};
const deploymentMockData = {
id: 15,
......@@ -29,4 +58,16 @@ const deploymentMockData = {
],
};
export default deploymentMockData;
const playDetails = {
playable_build: {
play_path: '/root/test-deployments/-/jobs/1131/play',
},
};
const retryDetails = {
playable_build: {
retry_path: '/root/test-deployments/-/jobs/1131/retry',
},
};
export { actionButtonMocks, deploymentMockData, playDetails, retryDetails };
......@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
import {
CREATED,
RUNNING,
......@@ -10,15 +9,7 @@ import {
FAILED,
CANCELED,
} from '~/vue_merge_request_widget/components/deployment/constants';
import deploymentMockData from './deployment_mock_data';
const deployDetail = {
playable_build: {
retry_path: '/root/test-deployments/-/jobs/1131/retry',
play_path: '/root/test-deployments/-/jobs/1131/play',
},
isManual: true,
};
import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data';
describe('Deployment component', () => {
let wrapper;
......@@ -30,6 +21,7 @@ describe('Deployment component', () => {
}
wrapper = mount(DeploymentComponent, {
...options,
provide: { glFeatures: { deployFromFooter: true } },
});
};
......@@ -53,28 +45,39 @@ describe('Deployment component', () => {
describe('status message and buttons', () => {
const noActions = [];
const noDetails = { isManual: false };
const deployGroup = [DeploymentViewButton, DeploymentStopButton];
const deployDetail = {
...playDetails,
isManual: true,
};
const retryDetail = {
...retryDetails,
isManual: true,
};
const defaultGroup = ['.js-deploy-url', '.js-stop-env'];
const manualDeployGroup = ['.js-manual-deploy-action', ...defaultGroup];
const manualRedeployGroup = ['.js-manual-redeploy-action', ...defaultGroup];
describe.each`
status | previous | deploymentDetails | text | actionButtons
${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${deployGroup}
${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup}
${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${manualDeployGroup}
${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${defaultGroup}
${CREATED} | ${false} | ${deployDetail} | ${'Can be manually deployed to'} | ${noActions}
${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions}
${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup}
${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup}
${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${defaultGroup}
${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${defaultGroup}
${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions}
${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions}
${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup}
${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup}
${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions}
${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
${FAILED} | ${true} | ${retryDetail} | ${'Failed to deploy to'} | ${manualRedeployGroup}
${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${defaultGroup}
${FAILED} | ${false} | ${retryDetail} | ${'Failed to deploy to'} | ${noActions}
${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions}
${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${deployGroup}
${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${deployGroup}
${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${defaultGroup}
${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${defaultGroup}
${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deployment to'} | ${noActions}
${CANCELED} | ${false} | ${noDetails} | ${'Canceled deployment to'} | ${noActions}
`(
......@@ -112,7 +115,7 @@ describe('Deployment component', () => {
if (actionButtons.length > 0) {
describe('renders the expected button group', () => {
actionButtons.forEach(button => {
it(`renders ${button.name}`, () => {
it(`renders ${button}`, () => {
expect(wrapper.find(button).exists()).toBe(true);
});
});
......@@ -121,8 +124,8 @@ describe('Deployment component', () => {
if (actionButtons.length === 0) {
describe('does not render the button group', () => {
[DeploymentViewButton, DeploymentStopButton].forEach(button => {
it(`does not render ${button.name}`, () => {
defaultGroup.forEach(button => {
it(`does not render ${button}`, () => {
expect(wrapper.find(button).exists()).toBe(false);
});
});
......@@ -144,10 +147,6 @@ describe('Deployment component', () => {
describe('hasExternalUrls', () => {
describe('when deployment has both external_url_formatted and external_url', () => {
it('should return true', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(true);
});
it('should render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(true);
});
......@@ -163,10 +162,6 @@ describe('Deployment component', () => {
});
});
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
});
......@@ -182,10 +177,6 @@ describe('Deployment component', () => {
});
});
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
});
......
import { mount } from '@vue/test-utils';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
import deploymentMockData from './deployment_mock_data';
import { deploymentMockData } from './deployment_mock_data';
const appButtonText = {
text: 'View app',
......
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState({
notes: [],
targetNoteHash: null,
lastFetchedAt: null,
notesData: {},
userData: {},
noteableData: {},
});
};
export * from '../../frontend/notes/helpers.js';
import Vue from 'vue';
import deploymentStopComponent from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Deployment component', () => {
const Component = Vue.extend(deploymentStopComponent);
let deploymentMockData;
beforeEach(() => {
deploymentMockData = {
id: 15,
name: 'review/diplo',
url: '/root/review-apps/environments/15',
stop_url: '/root/review-apps/environments/15/stop',
metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
external_url: 'http://gitlab.com.',
external_url_formatted: 'gitlab',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
deployment_manual_actions: [],
status: SUCCESS,
changes: [
{
path: 'index.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
},
{
path: 'imgs/gallery.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
},
{
path: 'about/',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
},
],
};
});
let vm;
afterEach(() => {
vm.$destroy();
});
describe('', () => {
beforeEach(() => {
vm = mountComponent(Component, {
stopUrl: deploymentMockData.stop_url,
isDeployInProgress: false,
});
});
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () =>
new Promise(resolve => {
resolve({
data: {
redirect_url: url,
},
});
});
const mockStopEnvironment = () => {
vm.stopEnvironment(deploymentMockData);
return vm;
};
it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
const visitUrl = spyOnDependency(deploymentStopComponent, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
expect(visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
it('should show a confirm dialog but should not work if the dialog is rejected', () => {
spyOn(window, 'confirm').and.returnValue(false);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
});
});
......@@ -195,6 +195,39 @@ describe Gitlab::Danger::CommitLinter do
end
end
[
'[ci skip] A commit message',
'[Ci skip] A commit message',
'[API] A commit message'
].each do |message|
context "when subject is '#{message}'" do
let(:commit_message) { message }
it 'does not add a problem' do
expect(commit_linter).not_to receive(:add_problem)
commit_linter.lint
end
end
end
[
'[ci skip]A commit message',
'[Ci skip] A commit message',
'[ci skip] a commit message',
'! A commit message'
].each do |message|
context "when subject is '#{message}'" do
let(:commit_message) { message }
it 'adds a problem' do
expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION)
commit_linter.lint
end
end
end
context 'when subject ends with a period' do
let(:commit_message) { 'A B C.' }
......
......@@ -273,6 +273,7 @@ MergeRequest::Metrics:
- modified_paths_size
- commits_count
- first_approved_at
- first_reassigned_at
Ci::Pipeline:
- id
- project_id
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment