Commit cac4831e authored by Kushal Pandya's avatar Kushal Pandya

Add Test Case Create Form

Adds `test_case_create` app to show Create Test Case
form UI in `/new` route.
parent 4b59a5aa
...@@ -53,7 +53,12 @@ export default { ...@@ -53,7 +53,12 @@ export default {
<div data-testid="issuable-title" class="form-group row"> <div data-testid="issuable-title" class="form-group row">
<label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label> <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<gl-form-input id="issuable-title" v-model="issuableTitle" :placeholder="__('Title')" /> <gl-form-input
id="issuable-title"
v-model="issuableTitle"
:autofocus="true"
:placeholder="__('Title')"
/>
</div> </div>
</div> </div>
<div data-testid="issuable-description" class="form-group row"> <div data-testid="issuable-description" class="form-group row">
......
import { initTestCaseCreate } from 'ee/test_case_create/test_case_create_bundle';
document.addEventListener('DOMContentLoaded', () => {
initTestCaseCreate({
mountPointSelector: '#js-create-test-case',
});
});
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import IssuableCreate from '~/issuable_create/components/issuable_create_root.vue';
import createTestCase from '../queries/create_test_case.mutation.graphql';
export default {
components: {
GlButton,
IssuableCreate,
},
inject: [
'projectFullPath',
'projectTestCasesPath',
'descriptionPreviewPath',
'descriptionHelpPath',
'labelsFetchPath',
'labelsManagePath',
],
data() {
return {
createTestCaseRequestActive: false,
};
},
methods: {
handleTestCaseSubmitClick({ issuableTitle, issuableDescription, selectedLabels }) {
this.createTestCaseRequestActive = true;
return this.$apollo
.mutate({
mutation: createTestCase,
variables: {
createTestCaseInput: {
projectPath: this.projectFullPath,
title: issuableTitle,
description: issuableDescription,
labelIds: selectedLabels.map(label => label.id),
},
},
})
.then(({ data = {} }) => {
const errors = data.createTestCase?.errors;
if (errors?.length) {
throw new Error(`Error creating a test case. Error message: ${errors[0].message}`);
}
redirectTo(this.projectTestCasesPath);
})
.catch(error => {
createFlash({
message: __('Something went wrong while creating a test case.'),
captureError: true,
error,
});
})
.finally(() => {
this.createTestCaseRequestActive = false;
});
},
},
};
</script>
<template>
<issuable-create
:description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath"
:labels-fetch-path="labelsFetchPath"
:labels-manage-path="labelsManagePath"
>
<template #title>
<h3 class="page-title">{{ __('New Test Case') }}</h3>
</template>
<template #actions="issuableMeta">
<div class="gl-flex-grow-1">
<gl-button
data-testid="submit-test-case"
category="primary"
variant="success"
:loading="createTestCaseRequestActive"
:disabled="!issuableMeta.issuableTitle.length"
@click="handleTestCaseSubmitClick(issuableMeta)"
>{{ __('Submit test case') }}</gl-button
>
</div>
<gl-button
data-testid="cancel-test-case"
:disabled="createTestCaseRequestActive"
:href="projectTestCasesPath"
>{{ __('Cancel') }}</gl-button
>
</template>
</issuable-create>
</template>
mutation createTestCase($createTestCaseInput: CreateTestCaseInput!) {
createTestCase(input: $createTestCaseInput) {
clientMutationId
errors
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TestCaseCreateApp from './components/test_case_create_root.vue';
Vue.use(VueApollo);
export function initTestCaseCreate({ mountPointSelector }) {
const mountPointEl = document.querySelector(mountPointSelector);
if (!mountPointEl) {
return null;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el: mountPointEl,
apolloProvider,
provide: {
...mountPointEl.dataset,
},
render: createElement => createElement(TestCaseCreateApp),
});
}
- breadcrumb_title _('Test Cases')
- page_title _('Test Cases') - page_title _('Test Cases')
- breadcrumb_title _("Test Cases") - @content_class = 'project-test-cases'
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Test Cases', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:label1) { create(:label, project: project, title: 'bug') }
let_it_be(:label2) { create(:label, project: project, title: 'enhancement') }
let_it_be(:label3) { create(:label, project: project, title: 'documentation') }
before do
project.add_developer(user)
stub_licensed_features(quality_management: true)
sign_in(user)
end
context 'test case create form' do
before do
visit new_project_quality_test_case_path(project)
wait_for_requests
end
it 'shows page title, title, description and label input fields' do
page.within('.issuable-create-container') do
expect(page.find('.page-title')).to have_content('New Test Case')
end
page.within('.issuable-create-container form') do
form_fields = page.find_all('.form-group.row')
expect(form_fields[0].find('label')).to have_content('Title')
expect(form_fields[0]).to have_selector('input#issuable-title')
expect(form_fields[1].find('label')).to have_content('Description')
expect(form_fields[1]).to have_selector('.js-vue-markdown-field')
expect(form_fields[2].find('label')).to have_content('Labels')
expect(form_fields[2]).to have_selector('.labels-select-wrapper')
end
end
it 'shows labels and footer actions within labels dropdown' do
page.within('.issuable-create-container form .labels-select-wrapper') do
page.find('.js-dropdown-button').click
wait_for_requests
expect(page.find('.js-labels-list .dropdown-content')).to have_selector('li', count: 3)
expect(page.find('.js-labels-list .dropdown-footer')).to have_selector('li', count: 2)
end
end
it 'shows page actions' do
page.within('.issuable-create-container .footer-block') do
expect(page.find('button')).to have_content('Submit test case')
expect(page.find('a')).to have_content('Cancel')
end
end
it 'creates a test case on saving form' do
title = 'Sample title'
description = 'Sample _test case_ description.'
page.within('.issuable-create-container form') do
form_fields = page.find_all('.form-group.row')
form_fields[0].find('input#issuable-title').native.send_keys title
form_fields[1].find('textarea#issuable-description').native.send_keys description
form_fields[2].find('.js-dropdown-button').click
wait_for_requests
form_fields[2].find_all('.js-labels-list .dropdown-content li')[0].click
end
click_button 'Submit test case'
wait_for_requests
expect(page).to have_selector('.content-wrapper .project-test-cases')
end
end
end
import { mount } from '@vue/test-utils';
import TestCaseCreateRoot from 'ee/test_case_create/components/test_case_create_root.vue';
import createTestCase from 'ee/test_case_create/queries/create_test_case.mutation.graphql';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import IssuableCreate from '~/issuable_create/components/issuable_create_root.vue';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
const mockProvide = {
projectFullPath: 'gitlab-org/gitlab-test',
projectTestCasesPath: '/gitlab-org/gitlab-test/-/quality/test_cases',
descriptionPreviewPath: '/gitlab-org/gitlab-test/preview_markdown',
descriptionHelpPath: '/help/user/markdown',
labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json',
labelsManagePath: '/gitlab-org/gitlab-shell/-/labels',
};
const createComponent = () =>
mount(TestCaseCreateRoot, {
provide: mockProvide,
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
});
describe('TestCaseCreateRoot', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('methods', () => {
describe('handleTestCaseSubmitClick', () => {
const issuableTitle = 'Sample title';
const issuableDescription = 'Sample _description_.';
const selectedLabels = [
{
id: 1,
set: true,
color: '#BADA55',
text_color: '#FFFFFF',
title: 'Bug',
},
];
const mockCreateMutationResult = {
data: {
createTestCase: {
errors: [],
},
},
};
it('sets `createTestCaseRequestActive` prop to true', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockCreateMutationResult);
wrapper.vm.handleTestCaseSubmitClick({
issuableTitle,
issuableDescription,
selectedLabels,
});
expect(wrapper.vm.createTestCaseRequestActive).toBe(true);
});
it('calls `$apollo.mutate` with `createTestCase` mutation and input variables containing projectPath, title, description and labelIds', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockCreateMutationResult);
wrapper.vm.handleTestCaseSubmitClick({
issuableTitle,
issuableDescription,
selectedLabels,
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createTestCase,
variables: {
createTestCaseInput: {
projectPath: 'gitlab-org/gitlab-test',
title: issuableTitle,
description: issuableDescription,
labelIds: selectedLabels.map(label => label.id),
},
},
}),
);
});
it('calls `redirectTo` with projectTestCasesPath when mutation is successful', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockCreateMutationResult);
return wrapper.vm
.handleTestCaseSubmitClick({
issuableTitle,
issuableDescription,
selectedLabels,
})
.then(() => {
expect(redirectTo).toHaveBeenCalledWith(mockProvide.projectTestCasesPath);
})
.finally(() => {
expect(wrapper.vm.createTestCaseRequestActive).toBe(false);
});
});
it('calls `createFlash` with message and error captured when mutation fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
return wrapper.vm
.handleTestCaseSubmitClick({
issuableTitle,
issuableDescription,
selectedLabels,
})
.then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while creating a test case.',
captureError: true,
error: expect.any(Object),
});
})
.finally(() => {
expect(wrapper.vm.createTestCaseRequestActive).toBe(false);
});
});
});
});
describe('template', () => {
it('renders issuable-create as a root component', () => {
const {
descriptionPreviewPath,
descriptionHelpPath,
labelsFetchPath,
labelsManagePath,
} = mockProvide;
expect(wrapper.find(IssuableCreate).exists()).toBe(true);
expect(wrapper.find(IssuableCreate).props()).toMatchObject({
descriptionPreviewPath,
descriptionHelpPath,
labelsFetchPath,
labelsManagePath,
});
});
it('renders page title', () => {
expect(wrapper.find('h3').text()).toBe('New Test Case');
});
it('renders page actions', () => {
const submitEl = wrapper.find('[data-testid="submit-test-case"]');
const cancelEl = wrapper.find('[data-testid="cancel-test-case"]');
expect(submitEl.text()).toBe('Submit test case');
expect(submitEl.props()).toMatchObject({
loading: false,
disabled: true,
});
expect(cancelEl.text()).toBe('Cancel');
expect(cancelEl.props('disabled')).toBe(false);
expect(cancelEl.attributes('href')).toBe(mockProvide.projectTestCasesPath);
});
it('submit button shows loading animation when `createTestCaseRequestActive` is true', async () => {
wrapper.setData({
createTestCaseRequestActive: true,
});
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="submit-test-case"]').props('loading')).toBe(true);
});
it('cancel button is disabled when `createTestCaseRequestActive` is true', async () => {
wrapper.setData({
createTestCaseRequestActive: true,
});
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="cancel-test-case"]').props('disabled')).toBe(true);
});
describe('events', () => {
it('submit button click calls `handleTestCaseSubmitClick` method', () => {
jest.spyOn(wrapper.vm, 'handleTestCaseSubmitClick').mockImplementation(jest.fn);
const submitButton = wrapper.find('[data-testid="submit-test-case"]');
submitButton.vm.$emit('click');
expect(wrapper.vm.handleTestCaseSubmitClick).toHaveBeenCalledWith({
issuableTitle: '',
issuableDescription: '',
selectedLabels: [],
});
});
});
});
});
...@@ -16843,6 +16843,9 @@ msgstr "" ...@@ -16843,6 +16843,9 @@ msgstr ""
msgid "New Snippet" msgid "New Snippet"
msgstr "" msgstr ""
msgid "New Test Case"
msgstr ""
msgid "New User" msgid "New User"
msgstr "" msgstr ""
...@@ -23622,6 +23625,9 @@ msgstr "" ...@@ -23622,6 +23625,9 @@ msgstr ""
msgid "Something went wrong while creating a requirement." msgid "Something went wrong while creating a requirement."
msgstr "" msgstr ""
msgid "Something went wrong while creating a test case."
msgstr ""
msgid "Something went wrong while deleting description changes. Please try again." msgid "Something went wrong while deleting description changes. Please try again."
msgstr "" msgstr ""
...@@ -24360,6 +24366,9 @@ msgstr "" ...@@ -24360,6 +24366,9 @@ msgstr ""
msgid "Submit search" msgid "Submit search"
msgstr "" msgstr ""
msgid "Submit test case"
msgstr ""
msgid "Submit the current review." msgid "Submit the current review."
msgstr "" msgstr ""
......
...@@ -65,6 +65,7 @@ describe('IssuableForm', () => { ...@@ -65,6 +65,7 @@ describe('IssuableForm', () => {
expect(titleFieldEl.find('label').text()).toBe('Title'); expect(titleFieldEl.find('label').text()).toBe('Title');
expect(titleFieldEl.find(GlFormInput).exists()).toBe(true); expect(titleFieldEl.find(GlFormInput).exists()).toBe(true);
expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title'); expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title');
expect(titleFieldEl.find(GlFormInput).attributes('autofocus')).toBe('true');
}); });
it('renders issuable description input field', () => { it('renders issuable description input field', () => {
......
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