Commit 04f6f0eb authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'psi-iteration-cad-form' into 'master'

Allow creating and editing iterations within a cadence

See merge request gitlab-org/gitlab!63601
parents 45e92f65 147d2f6d
...@@ -123,6 +123,14 @@ export default { ...@@ -123,6 +123,14 @@ export default {
}, },
}; };
}, },
newIteration() {
return {
name: 'newIteration',
params: {
cadenceId: getIdFromGraphQLId(this.cadenceId),
},
};
},
}, },
methods: { methods: {
fetchMore() { fetchMore() {
...@@ -204,6 +212,10 @@ export default { ...@@ -204,6 +212,10 @@ export default {
text-sr-only text-sr-only
no-caret no-caret
> >
<gl-dropdown-item v-if="!durationInWeeks" :to="newIteration">
{{ s__('Iterations|Add iteration') }}
</gl-dropdown-item>
<gl-dropdown-item :to="editCadence"> <gl-dropdown-item :to="editCadence">
{{ s__('Iterations|Edit cadence') }} {{ s__('Iterations|Edit cadence') }}
</gl-dropdown-item> </gl-dropdown-item>
......
<script> <script>
import { GlButton, GlForm, GlFormInput } from '@gitlab/ui'; import { GlAlert, GlButton, GlForm, GlFormInput } from '@gitlab/ui';
import initDatePicker from '~/behaviors/date_picker'; import initDatePicker from '~/behaviors/date_picker';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import createIteration from '../queries/create_iteration.mutation.graphql'; import readIteration from '../queries/iteration.query.graphql';
import createIteration from '../queries/iteration_create.mutation.graphql';
import updateIteration from '../queries/update_iteration.mutation.graphql'; import updateIteration from '../queries/update_iteration.mutation.graphql';
export default { export default {
cadencesList: {
name: 'index',
},
components: { components: {
GlAlert,
GlButton, GlButton,
GlForm, GlForm,
GlFormInput, GlFormInput,
MarkdownField, MarkdownField,
}, },
props: { apollo: {
groupPath: { group: {
type: String, query: readIteration,
required: true, skip() {
}, return !this.iterationId;
previewMarkdownPath: { },
type: String, /* eslint-disable @gitlab/require-i18n-strings */
required: false, variables() {
default: '', return {
}, fullPath: this.fullPath,
iterationsListPath: { id: convertToGraphQLId('Iteration', this.iterationId),
type: String, isGroup: true,
required: false, };
default: '', },
}, /* eslint-enable @gitlab/require-i18n-strings */
isEditing: { result({ data }) {
type: Boolean, const iteration = data.group.iterations?.nodes[0];
required: false,
default: false, if (!iteration) {
}, this.error = s__('Iterations|Unable to find iteration.');
iteration: { return;
type: Object, }
required: false,
default: () => ({}), this.title = iteration.title;
this.description = iteration.description;
this.startDate = iteration.startDate;
this.dueDate = iteration.dueDate;
},
error(err) {
this.error = err.message;
},
}, },
}, },
inject: ['fullPath', 'previewMarkdownPath'],
data() { data() {
return { return {
iterations: [],
loading: false, loading: false,
title: this.iteration.title, error: '',
description: this.iteration.description, group: { iteration: {} },
startDate: this.iteration.startDate, title: '',
dueDate: this.iteration.dueDate, description: '',
startDate: '',
dueDate: '',
}; };
}, },
computed: { computed: {
cadenceId() {
return this.$router.currentRoute.params.cadenceId;
},
iterationId() {
return this.$router.currentRoute.params.iterationId;
},
isEditing() {
return Boolean(this.iterationId);
},
variables() { variables() {
return { return {
input: { groupPath: this.fullPath,
groupPath: this.groupPath, title: this.title,
title: this.title, description: this.description,
description: this.description, startDate: this.startDate,
startDate: this.startDate, dueDate: this.dueDate,
dueDate: this.dueDate,
},
}; };
}, },
}, },
...@@ -73,21 +94,18 @@ export default { ...@@ -73,21 +94,18 @@ export default {
this.loading = true; this.loading = true;
return this.isEditing ? this.updateIteration() : this.createIteration(); return this.isEditing ? this.updateIteration() : this.createIteration();
}, },
cancel() {
if (this.iterationsListPath) {
visitUrl(this.iterationsListPath);
} else {
this.$emit('cancel');
}
},
createIteration() { createIteration() {
return this.$apollo return this.$apollo
.mutate({ .mutate({
mutation: createIteration, mutation: createIteration,
variables: this.variables, variables: {
...this.variables,
iterationsCadenceId: convertToGraphQLId('Iterations::Cadence', this.cadenceId),
},
}) })
.then(({ data }) => { .then(({ data }) => {
const { errors, iteration } = data.createIteration; const { iteration, errors } = data.iterationCreate;
if (errors.length > 0) { if (errors.length > 0) {
this.loading = false; this.loading = false;
createFlash({ createFlash({
...@@ -96,7 +114,13 @@ export default { ...@@ -96,7 +114,13 @@ export default {
return; return;
} }
visitUrl(iteration.webUrl); this.$router.push({
name: 'iteration',
params: {
cadenceId: this.cadenceId,
iterationId: getIdFromGraphQLId(iteration.id),
},
});
}) })
.catch(() => { .catch(() => {
this.loading = false; this.loading = false;
...@@ -111,8 +135,8 @@ export default { ...@@ -111,8 +135,8 @@ export default {
mutation: updateIteration, mutation: updateIteration,
variables: { variables: {
input: { input: {
...this.variables.input, ...this.variables,
id: this.iteration.id, id: this.iterationId,
}, },
}, },
}) })
...@@ -125,7 +149,13 @@ export default { ...@@ -125,7 +149,13 @@ export default {
return; return;
} }
this.$emit('updated'); this.$router.push({
name: 'iteration',
params: {
cadenceId: this.cadenceId,
iterationId: this.iterationId,
},
});
}) })
.catch(() => { .catch(() => {
createFlash({ createFlash({
...@@ -154,6 +184,10 @@ export default { ...@@ -154,6 +184,10 @@ export default {
</h3> </h3>
</div> </div>
<hr class="gl-mt-0" /> <hr class="gl-mt-0" />
<gl-alert v-if="error" class="gl-mb-5" variant="danger" @dismiss="error = ''">{{
error
}}</gl-alert>
<gl-form class="row common-note-form"> <gl-form class="row common-note-form">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group row"> <div class="form-group row">
...@@ -248,13 +282,13 @@ export default { ...@@ -248,13 +282,13 @@ export default {
<gl-button <gl-button
:loading="loading" :loading="loading"
data-testid="save-iteration" data-testid="save-iteration"
variant="success" variant="confirm"
data-qa-selector="save_iteration_button" data-qa-selector="save_iteration_button"
@click="save" @click="save"
> >
{{ isEditing ? __('Update iteration') : __('Create iteration') }} {{ isEditing ? __('Update iteration') : __('Create iteration') }}
</gl-button> </gl-button>
<gl-button class="ml-auto" data-testid="cancel-iteration" @click="cancel"> <gl-button class="gl-ml-3" data-testid="cancel-iteration" :to="$options.cadencesList">
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-button> </gl-button>
</div> </div>
......
...@@ -107,13 +107,22 @@ export function initCadenceApp({ namespaceType }) { ...@@ -107,13 +107,22 @@ export function initCadenceApp({ namespaceType }) {
cadencesListPath, cadencesListPath,
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
canCreateIteration,
canEditIteration, canEditIteration,
hasScopedLabelsFeature, hasScopedLabelsFeature,
labelsFetchPath, labelsFetchPath,
previewMarkdownPath, previewMarkdownPath,
noIssuesSvgPath, noIssuesSvgPath,
} = el.dataset; } = el.dataset;
const router = createRouter({ base: cadencesListPath }); const router = createRouter({
base: cadencesListPath,
permissions: {
canCreateCadence: parseBoolean(canCreateCadence),
canEditCadence: parseBoolean(canEditCadence),
canCreateIteration: parseBoolean(canCreateIteration),
canEditIteration: parseBoolean(canEditIteration),
},
});
return new Vue({ return new Vue({
el, el,
...@@ -126,6 +135,7 @@ export function initCadenceApp({ namespaceType }) { ...@@ -126,6 +135,7 @@ export function initCadenceApp({ namespaceType }) {
canCreateCadence: parseBoolean(canCreateCadence), canCreateCadence: parseBoolean(canCreateCadence),
canEditCadence: parseBoolean(canEditCadence), canEditCadence: parseBoolean(canEditCadence),
namespaceType, namespaceType,
canCreateIteration: parseBoolean(canCreateIteration),
canEditIteration: parseBoolean(canEditIteration), canEditIteration: parseBoolean(canEditIteration),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature), hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
labelsFetchPath, labelsFetchPath,
......
mutation createIteration($input: iterationCreateInput!) {
iterationCreate(input: $input) {
iteration {
title
description
descriptionHtml
startDate
dueDate
}
errors
}
}
...@@ -2,34 +2,63 @@ import Vue from 'vue'; ...@@ -2,34 +2,63 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import IterationCadenceForm from './components/iteration_cadence_form.vue'; import IterationCadenceForm from './components/iteration_cadence_form.vue';
import IterationCadenceList from './components/iteration_cadences_list.vue'; import IterationCadenceList from './components/iteration_cadences_list.vue';
import IterationForm from './components/iteration_form.vue';
import IterationReport from './components/iteration_report.vue'; import IterationReport from './components/iteration_report.vue';
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ function checkPermission(permission) {
{ return (to, from, next) => {
name: 'new', if (permission) {
path: '/new', next();
component: IterationCadenceForm, } else {
}, next({ path: '/' });
{ }
name: 'edit', };
path: '/:cadenceId/edit', }
component: IterationCadenceForm,
}, export default function createRouter({ base, permissions = {} }) {
{ const routes = [
name: 'index', {
path: '/', name: 'index',
component: IterationCadenceList, path: '/',
}, component: IterationCadenceList,
{ },
name: 'iteration', {
path: '/:cadenceId/iterations/:iterationId', name: 'new',
component: IterationReport, path: '/new',
}, component: IterationCadenceForm,
]; beforeEnter: checkPermission(permissions.canCreateCadence),
},
{
name: 'edit',
path: '/:cadenceId/edit',
component: IterationCadenceForm,
beforeEnter: checkPermission(permissions.canEditCadence),
},
{
name: 'newIteration',
path: '/:cadenceId/iterations/new',
component: IterationForm,
beforeEnter: checkPermission(permissions.canCreateIteration),
},
{
name: 'iteration',
path: '/:cadenceId/iterations/:iterationId',
component: IterationReport,
},
{
name: 'editIteration',
path: '/:cadenceId/iterations/:iterationId/edit',
component: IterationForm,
beforeEnter: checkPermission(permissions.canEditIteration),
},
{
path: '*',
redirect: '/',
},
];
export default function createRouter({ base }) {
const router = new VueRouter({ const router = new VueRouter({
base, base,
mode: 'history', mode: 'history',
......
...@@ -3,15 +3,9 @@ ...@@ -3,15 +3,9 @@
class Groups::IterationCadencesController < Groups::ApplicationController class Groups::IterationCadencesController < Groups::ApplicationController
before_action :check_cadences_available! before_action :check_cadences_available!
before_action :authorize_show_cadence!, only: [:index] before_action :authorize_show_cadence!, only: [:index]
before_action :authorize_admin_cadence!, only: [:edit]
before_action :authorize_create_cadence!, only: [:new]
feature_category :issue_tracking feature_category :issue_tracking
def new; end
def edit; end
def index; end def index; end
private private
...@@ -20,14 +14,6 @@ class Groups::IterationCadencesController < Groups::ApplicationController ...@@ -20,14 +14,6 @@ class Groups::IterationCadencesController < Groups::ApplicationController
render_404 unless group.iteration_cadences_feature_flag_enabled? render_404 unless group.iteration_cadences_feature_flag_enabled?
end end
def authorize_admin_cadence!
render_404 unless can?(current_user, :admin_iteration_cadence, group)
end
def authorize_create_cadence!
render_404 unless can?(current_user, :create_iteration_cadence, group)
end
def authorize_show_cadence! def authorize_show_cadence!
render_404 unless can?(current_user, :read_iteration_cadence, group) render_404 unless can?(current_user, :read_iteration_cadence, group)
end end
......
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s,
can_edit_iteration: can?(current_user, :admin_iteration, @group).to_s,
has_scoped_labels_feature: @group.licensed_feature_available?(:scoped_labels).to_s,
labels_fetch_path: group_labels_path(@group, format: :json, include_ancestor_groups: true),
preview_markdown_path: preview_markdown_path(@group),
no_issues_svg_path: image_path('illustrations/issues.svg') } }
- add_to_breadcrumbs _('Iteration cadences'), group_iteration_cadences_path(@group)
- breadcrumb_title params[:id]
- page_title _('Edit iteration cadence')
= render 'js_app'
- page_title _('Iteration cadences') - page_title _('Iteration cadences')
= render 'js_app' .js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s,
can_create_iteration: can?(current_user, :create_iteration, @group).to_s,
can_edit_iteration: can?(current_user, :admin_iteration, @group).to_s,
has_scoped_labels_feature: @group.licensed_feature_available?(:scoped_labels).to_s,
labels_fetch_path: group_labels_path(@group, format: :json, include_ancestor_groups: true),
preview_markdown_path: preview_markdown_path(@group),
no_issues_svg_path: image_path('illustrations/issues.svg') } }
- add_to_breadcrumbs _('Iteration cadences'), group_iteration_cadences_path(@group)
- breadcrumb_title _('New')
- page_title _('New iteration cadence')
= render 'js_app'
...@@ -120,7 +120,9 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -120,7 +120,9 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ } resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }
resources :iteration_cadences, only: [:index, :new, :edit], path: 'cadences', constraints: { id: /\d+/ } resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do
resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index
end
resources :issues, only: [] do resources :issues, only: [] do
collection do collection do
......
...@@ -15,28 +15,13 @@ RSpec.describe Groups::IterationCadencesController do ...@@ -15,28 +15,13 @@ RSpec.describe Groups::IterationCadencesController do
sign_in(user) sign_in(user)
end end
describe 'new' do describe 'index' do
subject { get :new, params: { group_id: group } } subject { get :index, params: { group_id: group } }
where(:feature_flag_available, :role, :status) do where(:feature_flag_available, :role, :status) do
false | :developer | :not_found false | :developer | :not_found
true | :none | :not_found true | :none | :not_found
true | :guest | :not_found true | :guest | :success
true | :developer | :success
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end
describe 'edit' do
subject { get :edit, params: { group_id: group, id: cadence } }
where(:feature_flag_available, :role, :status) do
false | :developer | :not_found
true | :none | :not_found
true | :guest | :not_found
true | :developer | :success true | :developer | :success
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User edits iteration cadence', :js do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user }
let_it_be(:cadence) { create(:iterations_cadence, group: group, description: 'an example iteration cadence', duration_in_weeks: 3, iterations_in_advance: 2) }
dropdown_selector = '[data-testid="actions-dropdown"]'
context 'with license' do
before do
stub_licensed_features(iterations: true)
end
context 'as authorized user' do
before do
sign_in(user)
end
it 'prefills fields and allows updating values' do
visit edit_group_iteration_cadence_path(cadence.group, id: cadence.id)
wait_for_requests
aggregate_failures do
expect(title_input.value).to eq(cadence.title)
expect(description_input.value).to eq(cadence.description)
expect(start_date_input.value).to have_content(cadence.start_date)
end
updated_title = 'Updated cadence title'
fill_in('Title', with: updated_title)
click_button('Save cadence')
expect(page).to have_content(updated_title)
end
end
context 'as guest user' do
before do
sign_in(guest_user)
end
it 'does not show edit dropdown' do
visit group_iteration_cadences_path(cadence.group)
expect(page).to have_content(cadence.title)
expect(page).not_to have_selector(dropdown_selector)
end
it 'redirects to list page when loading edit cadence page' do
visit edit_group_iteration_cadence_path(cadence.group, id: cadence.id)
# vue-router has trailing slash but _path helper doesn't
expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end
it 'redirects to list page when loading new cadence page' do
visit new_group_iteration_cadence_path(cadence.group)
# vue-router has trailing slash but _path helper doesn't
expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end
end
def title_input
page.find('#cadence-title')
end
def description_input
page.find('#cadence-description')
end
def start_date_input
page.find('#cadence-start-date')
end
end
end
...@@ -2,16 +2,19 @@ ...@@ -2,16 +2,19 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'User views iteration' do RSpec.describe 'User edits iteration' do
let_it_be(:now) { Time.now } let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user } let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user } let_it_be(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, group: group, title: 'Correct Iteration', description: 'Iteration description', start_date: now - 1.day, due_date: now) } let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, group: group, title: 'Correct Iteration', description: 'Iteration description', start_date: now - 1.day, due_date: now, iterations_cadence: cadence) }
dropdown_selector = '[data-testid="actions-dropdown"]' dropdown_selector = '[data-testid="actions-dropdown"]'
context 'with license' do context 'with license' do
using RSpec::Parameterized::TableSyntax
before do before do
stub_licensed_features(iterations: true) stub_licensed_features(iterations: true)
end end
...@@ -21,55 +24,64 @@ RSpec.describe 'User views iteration' do ...@@ -21,55 +24,64 @@ RSpec.describe 'User views iteration' do
sign_in(user) sign_in(user)
end end
context 'load edit page directly', :js do where(using_cadences: [true, false])
before do
visit edit_group_iteration_path(group, iteration.id)
end
it 'prefills fields and allows updating all values' do with_them do
aggregate_failures do let(:iteration_page) { using_cadences ? group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) : group_iteration_path(iteration.group, iteration.id) }
expect(title_input.value).to eq(iteration.title) let(:edit_iteration_page) { using_cadences ? edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) : edit_group_iteration_path(iteration.group, iteration.id) }
expect(description_input.value).to eq(iteration.description)
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date)
end
updated_title = 'Updated iteration title' context 'load edit page directly', :js do
updated_desc = 'Updated iteration desc' before do
updated_start_date = now + 4.days visit edit_iteration_page
updated_due_date = now + 5.days
wait_for_requests
fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc)
fill_in('Start date', with: updated_start_date.strftime('%Y-%m-%d'))
fill_in('Due date', with: updated_due_date.strftime('%Y-%m-%d'))
click_button('Update iteration')
aggregate_failures do
expect(page).to have_content(updated_title)
expect(page).to have_content(updated_desc)
expect(page).to have_content(updated_start_date.strftime('%b %-d, %Y'))
expect(page).to have_content(updated_due_date.strftime('%b %-d, %Y'))
expect(page).to have_current_path(group_iteration_path(group, iteration.id))
end end
end
end
context 'load edit page from report', :js do it 'prefills fields and allows updating all values' do
before do aggregate_failures do
visit group_iteration_path(iteration.group, iteration.id) expect(title_input.value).to eq(iteration.title)
expect(description_input.value).to eq(iteration.description)
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date)
end
updated_title = 'Updated iteration title'
updated_desc = 'Updated iteration desc'
updated_start_date = now + 4.days
updated_due_date = now + 5.days
fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc)
fill_in('Start date', with: updated_start_date.strftime('%Y-%m-%d'))
fill_in('Due date', with: updated_due_date.strftime('%Y-%m-%d'))
click_button('Update iteration')
aggregate_failures do
expect(page).to have_content(updated_title)
expect(page).to have_content(updated_desc)
expect(page).to have_content(updated_start_date.strftime('%b %-d, %Y'))
expect(page).to have_content(updated_due_date.strftime('%b %-d, %Y'))
expect(page).to have_current_path(iteration_page)
end
end
end end
it 'prefills fields and updates URL' do context 'load edit page from report', :js do
find(dropdown_selector).click before do
click_button('Edit iteration') visit iteration_page
end
aggregate_failures do it 'prefills fields and updates URL' do
expect(title_input.value).to eq(iteration.title) find(dropdown_selector).click
expect(description_input.value).to eq(iteration.description) click_link_or_button('Edit iteration')
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date) aggregate_failures do
expect(page).to have_current_path(edit_group_iteration_path(iteration.group, iteration.id)) expect(title_input.value).to eq(iteration.title)
expect(description_input.value).to eq(iteration.description)
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date)
expect(page).to have_current_path(edit_iteration_page)
end
end end
end end
end end
...@@ -80,17 +92,35 @@ RSpec.describe 'User views iteration' do ...@@ -80,17 +92,35 @@ RSpec.describe 'User views iteration' do
sign_in(guest_user) sign_in(guest_user)
end end
it 'does not show edit dropdown', :js do context 'with cadences', :js do
visit group_iteration_path(iteration.group, iteration.id) it 'does not show edit dropdown' do
visit group_iteration_cadence_iteration_path(iteration.group, iteration_cadence_id: cadence.id, id: iteration.id)
expect(page).to have_content(iteration.title) expect(page).to have_content(iteration.title)
expect(page).not_to have_selector(dropdown_selector) expect(page).not_to have_selector(dropdown_selector)
end
it 'redirects to cadence list page when loading edit page directly' do
visit edit_group_iteration_cadence_iteration_path(iteration.group, iteration_cadence_id: cadence.id, id: iteration.id)
expect(page).to have_content(cadence.title)
expect(page).to have_current_path("#{group_iteration_cadences_path(group)}/")
end
end end
it '404s when loading edit page directly' do context 'without cadences' do
visit edit_group_iteration_path(iteration.group, iteration.id) it 'does not show edit dropdown', :js do
visit group_iteration_path(iteration.group, iteration.id)
expect(page).to have_gitlab_http_status(:not_found) expect(page).to have_content(iteration.title)
expect(page).not_to have_selector(dropdown_selector)
end
it '404s when loading edit page directly' do
visit edit_group_iteration_path(iteration.group, iteration.id)
expect(page).to have_gitlab_http_status(:not_found)
end
end end
end end
......
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import IterationForm from 'ee/iterations/components/iteration_form.vue';
import readIteration from 'ee/iterations/queries/iteration.query.graphql';
import createIteration from 'ee/iterations/queries/iteration_create.mutation.graphql';
import updateIteration from 'ee/iterations/queries/update_iteration.mutation.graphql';
import createRouter from 'ee/iterations/router';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { convertToGraphQLId } from '~/graphql_shared/utils';
const baseUrl = '/cadences/';
function createMockApolloProvider(requestHandlers) {
Vue.use(VueApollo);
return createMockApollo(requestHandlers);
}
describe('Iteration Form', () => {
let wrapper;
let router;
const groupPath = 'gitlab-org';
const iterationId = 72;
const cadenceId = 2;
const iteration = {
id: `gid://gitlab/Iteration/${iterationId}`,
iid: 70,
title: 'An iteration',
state: 'opened',
webPath: '/test',
description: 'The words',
descriptionHtml: '<p>The words</p>',
startDate: '2020-06-28',
dueDate: '2020-07-05',
};
const readMutationSuccess = {
data: { group: { iterations: { nodes: [iteration] }, errors: [] } },
};
const createMutationSuccess = { data: { iterationCreate: { iteration, errors: [] } } };
const createMutationFailure = {
data: { iterationCreate: { iteration, errors: ['alas, your data is unchanged'] } },
};
const updateMutationSuccess = { data: { updateIteration: { iteration, errors: [] } } };
function createComponent({
mutationQuery = createIteration,
mutationResult = createMutationSuccess,
query = readIteration,
result = readMutationSuccess,
resolverMock = jest.fn().mockResolvedValue(mutationResult),
} = {}) {
const apolloProvider = createMockApolloProvider([
[query, jest.fn().mockResolvedValue(result)],
[mutationQuery, resolverMock],
]);
wrapper = extendedWrapper(
mount(IterationForm, {
provide: {
fullPath: groupPath,
previewMarkdownPath: '',
},
apolloProvider,
router,
}),
);
}
beforeEach(() => {
router = createRouter({
base: baseUrl,
permissions: { canCreateIteration: true, canEditIteration: true },
});
});
afterEach(() => {
wrapper.destroy();
});
const findPageTitle = () => wrapper.find({ ref: 'pageTitle' });
const findTitle = () => wrapper.find('#iteration-title');
const findDescription = () => wrapper.find('#iteration-description');
const findStartDate = () => wrapper.find('#iteration-start-date');
const findDueDate = () => wrapper.find('#iteration-due-date');
const findSaveButton = () => wrapper.findByTestId('save-iteration');
const findCancelButton = () => wrapper.findByTestId('cancel-iteration');
const clickSave = () => findSaveButton().trigger('click');
describe('New iteration', () => {
const resolverMock = jest.fn().mockResolvedValue(createMutationSuccess);
beforeEach(() => {
router.replace({ name: 'newIteration', params: { cadenceId, iterationId: undefined } });
createComponent({ resolverMock });
});
afterEach(() => {
router.replace({ name: 'index' });
});
it('cancel button links to list page', () => {
expect(findCancelButton().attributes('href')).toBe(baseUrl);
});
describe('save', () => {
it('triggers mutation with form data', async () => {
const title = 'Iteration 5';
const description = 'The fifth iteration';
const startDate = '2020-05-05';
const dueDate = '2020-05-25';
findTitle().vm.$emit('input', title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
await clickSave();
expect(resolverMock).toHaveBeenCalledWith({
groupPath,
title,
description,
iterationsCadenceId: convertToGraphQLId('Iterations::Cadence', cadenceId),
startDate,
dueDate,
});
});
it('redirects to Iteration page on success', async () => {
createComponent();
await clickSave();
expect(findSaveButton().props('loading')).toBe(true);
await waitForPromises();
expect(router.currentRoute.name).toBe('iteration');
expect(router.currentRoute.params).toEqual({
cadenceId,
iterationId,
});
});
it('loading=false on error', () => {
createComponent({ mutationResult: createMutationFailure });
clickSave();
return waitForPromises().then(() => {
expect(findSaveButton().props('loading')).toBe(false);
});
});
});
});
describe('Edit iteration', () => {
beforeEach(() => {
router.replace({ name: 'editIteration', params: { cadenceId, iterationId } });
});
afterEach(() => {
router.replace({ name: 'index' });
});
it('shows update text title', () => {
createComponent();
expect(findPageTitle().text()).toBe('Edit iteration');
});
it('prefills form fields', async () => {
createComponent();
await waitForPromises();
expect(findTitle().element.value).toBe(iteration.title);
expect(findDescription().element.value).toBe(iteration.description);
expect(findStartDate().element.value).toBe(iteration.startDate);
expect(findDueDate().element.value).toBe(iteration.dueDate);
});
it('shows update text on submit button', () => {
createComponent();
expect(findSaveButton().text()).toBe('Update iteration');
});
it('triggers mutation with form data', () => {
const resolverMock = jest.fn().mockResolvedValue(updateMutationSuccess);
createComponent({ mutationQuery: updateIteration, resolverMock });
const title = 'Updated title';
const description = 'Updated description';
const startDate = '2020-05-06';
const dueDate = '2020-05-26';
findTitle().vm.$emit('input', title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
clickSave();
expect(resolverMock).toHaveBeenCalledWith({
input: {
groupPath,
id: iterationId,
title,
description,
startDate,
dueDate,
},
});
});
it('calls update mutation', async () => {
const resolverMock = jest.fn().mockResolvedValue(updateMutationSuccess);
createComponent({
mutationQuery: updateIteration,
resolverMock,
});
clickSave();
await nextTick();
expect(findSaveButton().props('loading')).toBe(true);
expect(resolverMock).toHaveBeenCalledWith({
input: {
groupPath,
id: iterationId,
startDate: '',
dueDate: '',
title: '',
description: '',
},
});
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'cadences routing' do
let_it_be(:group_path) { 'group.abc123' }
let_it_be(:group) { create(:group, path: group_path) }
let(:cadence) do
create(:iterations_cadence, group: group, title: 'test cadence')
end
it "routes to show cadences list" do
expect(get("/groups/#{group_path}/-/cadences")).to route_to('groups/iteration_cadences#index', group_id: group_path)
end
it "routes to new cadence" do
expect(get("/groups/#{group_path}/-/cadences/new")).to route_to('groups/iteration_cadences#index', vueroute: "new", group_id: group_path)
end
it "routes to edit cadence" do
expect(get("/groups/#{group_path}/-/cadences/1/edit")).to route_to('groups/iteration_cadences#index', group_id: group_path, vueroute: "1/edit")
end
it "routes to list iterations within cadence" do
expect(get("/groups/#{group_path}/-/cadences/1/iterations")).to route_to('groups/iteration_cadences#index', group_id: group_path, iteration_cadence_id: "1")
end
it "routes to show iteration within cadence" do
expect(get("/groups/#{group_path}/-/cadences/1/iterations/2")).to route_to('groups/iteration_cadences#index', group_id: group_path, iteration_cadence_id: "1", id: "2")
end
it "routes to edit iteration within cadence" do
expect(get("/groups/#{group_path}/-/cadences/1/iterations/2/edit")).to route_to('groups/iteration_cadences#index', group_id: group_path, iteration_cadence_id: "1", id: "2")
end
it "routes to new iteration within cadence" do
expect(get("/groups/#{group_path}/-/cadences/1/iterations/new")).to route_to('groups/iteration_cadences#index', group_id: group_path, iteration_cadence_id: "1")
end
end
...@@ -11716,9 +11716,6 @@ msgstr "" ...@@ -11716,9 +11716,6 @@ msgstr ""
msgid "Edit iteration" msgid "Edit iteration"
msgstr "" msgstr ""
msgid "Edit iteration cadence"
msgstr ""
msgid "Edit public deploy key" msgid "Edit public deploy key"
msgstr "" msgstr ""
...@@ -18205,6 +18202,9 @@ msgstr "" ...@@ -18205,6 +18202,9 @@ msgstr ""
msgid "Iterations" msgid "Iterations"
msgstr "" msgstr ""
msgid "Iterations|Add iteration"
msgstr ""
msgid "Iterations|Automated scheduling" msgid "Iterations|Automated scheduling"
msgstr "" msgstr ""
...@@ -18286,6 +18286,9 @@ msgstr "" ...@@ -18286,6 +18286,9 @@ msgstr ""
msgid "Iterations|Title" msgid "Iterations|Title"
msgstr "" msgstr ""
msgid "Iterations|Unable to find iteration."
msgstr ""
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group" msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
msgstr "" msgstr ""
...@@ -21796,9 +21799,6 @@ msgstr "" ...@@ -21796,9 +21799,6 @@ msgstr ""
msgid "New iteration" msgid "New iteration"
msgstr "" msgstr ""
msgid "New iteration cadence"
msgstr ""
msgid "New iteration created" msgid "New iteration created"
msgstr "" msgstr ""
......
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