Commit a603f1ab authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'sidebar-refactor/use-generic-component-for-iterations' into 'master'

Sidebar iteration widget refactor - Use generic component

See merge request gitlab-org/gitlab!62857
parents 93562c77 7ec3d2c2
......@@ -26,8 +26,6 @@ export default {
BoardSidebarMilestoneSelect,
BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarIterationWidget: () =>
import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
SidebarDropdownWidget: () =>
import('ee_component/sidebar/components/sidebar_dropdown_widget.vue'),
},
......@@ -100,13 +98,16 @@ export default {
/>
<div>
<board-sidebar-milestone-select />
<sidebar-iteration-widget
<sidebar-dropdown-widget
v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
issuable-attribute="iteration"
:workspace-path="projectPathForActiveIssue"
:iterations-workspace-path="groupPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
class="gl-mt-5"
data-testid="iteration-edit"
data-qa-selector="iteration_container"
/>
</div>
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
......
<script>
import {
GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlDropdownDivider,
GlLoadingIcon,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
iterationSelectTextMap,
iterationDisplayState,
noIteration,
issuableIterationQueries,
iterationsQueries,
} from '../constants';
export default {
noIteration,
i18n: {
iteration: iterationSelectTextMap.iteration,
noIteration: iterationSelectTextMap.noIteration,
assignIteration: iterationSelectTextMap.assignIteration,
iterationSelectFail: iterationSelectTextMap.iterationSelectFail,
noIterationsFound: iterationSelectTextMap.noIterationsFound,
currentIterationFetchError: iterationSelectTextMap.currentIterationFetchError,
iterationsFetchError: iterationSelectTextMap.iterationsFetchError,
edit: __('Edit'),
none: __('None'),
},
issuableIterationQueries,
iterationsQueries,
tracking: {
label: 'right_sidebar',
property: 'iteration',
event: 'click_edit_button',
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
SidebarEditableItem,
GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownDivider,
GlSearchBoxByType,
GlIcon,
GlLoadingIcon,
},
inject: {
isClassicSidebar: {
default: false,
},
},
props: {
workspacePath: {
required: true,
type: String,
},
iid: {
required: true,
type: String,
},
iterationsWorkspacePath: {
required: true,
type: String,
},
issuableType: {
type: String,
required: true,
validator(value) {
// Add supported IssuableType here along with graphql queries
// as this widget is used for addtional issuable types.
return [IssuableType.Issue].includes(value);
},
},
},
apollo: {
currentIteration: {
query() {
return issuableIterationQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.workspacePath,
iid: this.iid,
};
},
update(data) {
return data?.workspace?.issuable.iteration;
},
error(error) {
createFlash({ message: this.$options.i18n.currentIterationFetchError });
Sentry.captureException(error);
},
},
iterations: {
query() {
return iterationsQueries[this.issuableType].query;
},
skip() {
return !this.editing;
},
debounce: 250,
variables() {
return {
fullPath: this.iterationsWorkspacePath,
title: this.searchTerm,
state: iterationDisplayState,
};
},
update(data) {
return data?.workspace?.iterations.nodes || [];
},
error(error) {
createFlash({ message: this.$options.i18n.iterationsFetchError });
Sentry.captureException(error);
},
},
},
data() {
return {
searchTerm: '',
editing: false,
updating: false,
selectedTitle: null,
currentIteration: null,
iterations: [],
};
},
computed: {
iteration() {
return this.iterations.find(({ id }) => id === this.currentIteration);
},
iterationTitle() {
return this.currentIteration?.title;
},
iterationUrl() {
return this.currentIteration?.webUrl;
},
dropdownText() {
return this.currentIteration ? this.currentIteration?.title : this.$options.i18n.iteration;
},
loading() {
return this.$apollo.queries.currentIteration.loading;
},
noIterations() {
return this.iterations.length === 0;
},
},
methods: {
updateIteration(iterationId) {
if (this.currentIteration === null && iterationId === null) return;
if (iterationId === this.currentIteration?.id) return;
this.updating = true;
const selectedIteration = this.iterations.find((i) => i.id === iterationId);
this.selectedTitle = selectedIteration ? selectedIteration.title : this.$options.i18n.none;
this.$apollo
.mutate({
mutation: issuableIterationQueries[this.issuableType].mutation,
variables: {
fullPath: this.workspacePath,
iterationId,
iid: this.iid,
},
})
.then(({ data }) => {
if (data.issuableSetIteration?.errors?.length) {
createFlash(data.issuableSetIteration.errors[0]);
Sentry.captureException(data.issuableSetIteration.errors[0]);
} else {
this.$emit('iteration-updated', data);
}
})
.catch((error) => {
createFlash(this.$options.i18n.iterationSelectFail);
Sentry.captureException(error);
})
.finally(() => {
this.updating = false;
this.searchTerm = '';
this.selectedTitle = null;
});
},
isIterationChecked(iterationId = undefined) {
return (
iterationId === this.currentIteration?.id || (!this.currentIteration?.id && !iterationId)
);
},
showDropdown() {
this.$refs.newDropdown.show();
},
handleOpen() {
this.editing = true;
this.showDropdown();
},
handleClose() {
this.editing = false;
},
setFocus() {
this.$refs.search.focusInput();
},
},
};
</script>
<template>
<div data-qa-selector="iteration_container">
<sidebar-editable-item
ref="editable"
:title="$options.i18n.iteration"
data-testid="iteration-edit-link"
:tracking="$options.tracking"
:loading="updating || loading"
@open="handleOpen"
@close="handleClose"
>
<template #collapsed>
<div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon">
<gl-icon :size="16" :aria-label="$options.i18n.iteration" name="iteration" />
<span class="collapse-truncated-title">{{ iterationTitle }}</span>
</div>
<div
data-testid="select-iteration"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
<strong v-if="updating">{{ selectedTitle }}</strong>
<span v-else-if="!updating && !currentIteration" class="gl-text-gray-500">{{
$options.i18n.none
}}</span>
<gl-link
v-else
data-qa-selector="iteration_link"
class="gl-text-gray-900! gl-font-weight-bold"
:href="iterationUrl"
><strong>{{ iterationTitle }}</strong></gl-link
>
</div>
</template>
<template #default>
<gl-dropdown
ref="newDropdown"
lazy
:header-text="$options.i18n.assignIteration"
:text="dropdownText"
:loading="loading"
class="gl-w-full"
@shown="setFocus"
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
<gl-dropdown-item
data-testid="no-iteration-item"
:is-check-item="true"
:is-checked="isIterationChecked($options.noIteration)"
@click="updateIteration($options.noIteration)"
>
{{ $options.i18n.noIteration }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-loading-icon
v-if="$apollo.queries.iterations.loading"
class="gl-py-4"
data-testid="loading-icon-dropdown"
/>
<template v-else>
<gl-dropdown-text v-if="noIterations">
{{ $options.i18n.noIterationsFound }}
</gl-dropdown-text>
<gl-dropdown-item
v-for="iterationItem in iterations"
:key="iterationItem.id"
:is-check-item="true"
:is-checked="isIterationChecked(iterationItem.id)"
data-testid="iteration-items"
@click="updateIteration(iterationItem.id)"
>{{ iterationItem.title }}</gl-dropdown-item
>
</template>
</gl-dropdown>
</template>
</sidebar-editable-item>
</div>
</template>
......@@ -7,7 +7,6 @@ import { apolloProvider } from '~/sidebar/graphql';
import * as CEMountSidebar from '~/sidebar/mount_sidebar';
import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue';
import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
import SidebarIterationWidget from './components/sidebar_iteration_widget.vue';
import SidebarStatus from './components/status/sidebar_status.vue';
import SidebarWeight from './components/weight/sidebar_weight.vue';
import { IssuableAttributeType } from './constants';
......@@ -118,19 +117,20 @@ function mountIterationSelect() {
el,
apolloProvider,
components: {
SidebarIterationWidget,
SidebarDropdownWidget,
},
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) =>
createElement('sidebar-iteration-widget', {
createElement('sidebar-dropdown-widget', {
props: {
iterationsWorkspacePath: groupPath,
attrWorkspacePath: groupPath,
workspacePath: projectPath,
iid: issueIid,
issuableType: IssuableType.Issue,
issuableAttribute: IssuableAttributeType.Iteration,
},
}),
});
......
......@@ -3,7 +3,7 @@
query issueIterations($fullPath: ID!, $title: String, $state: IterationState) {
workspace: group(fullPath: $fullPath) {
__typename
iterations(title: $title, state: $state) {
attributes: iterations(title: $title, state: $state) {
nodes {
...IterationFragment
state
......
mutation projectIssueIterationMutation($fullPath: ID!, $iid: String!, $iterationId: ID) {
issuableSetIteration: issueSetIteration(
input: { projectPath: $fullPath, iid: $iid, iterationId: $iterationId }
mutation projectIssueIterationMutation($fullPath: ID!, $iid: String!, $attributeId: ID) {
issuableSetAttribute: issueSetIteration(
input: { projectPath: $fullPath, iid: $iid, iterationId: $attributeId }
) {
__typename
errors
issuable: issue {
__typename
id
iteration {
attribute: iteration {
title
id
state
......
......@@ -6,7 +6,7 @@ query projectIssueIteration($fullPath: ID!, $iid: String!) {
issuable: issue(iid: $iid) {
__typename
id
iteration {
attribute: iteration {
...IterationFragment
}
}
......
......@@ -27,7 +27,7 @@ RSpec.describe 'Boards licensed features', :js do
end
it "hides iteration widget" do
expect(page).not_to have_selector('[data-testid="iteration-edit-link"]')
expect(page).not_to have_selector('[data-testid="iteration-edit"]')
end
end
......
......@@ -206,7 +206,7 @@ RSpec.describe 'Issue Sidebar' do
end
def find_and_click_edit_iteration
page.find('[data-testid="iteration-edit-link"] [data-testid="edit-button"]').click
page.find('[data-testid="iteration-edit"] [data-testid="edit-button"]').click
wait_for_all_requests
end
......
......@@ -24,11 +24,14 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
<div>
<boardsidebarmilestoneselect-stub />
<sidebariterationwidget-stub
<sidebardropdownwidget-stub
attr-workspace-path="gitlab-org"
class="gl-mt-5"
data-qa-selector="iteration_container"
data-testid="iteration-edit"
iid="27"
issuable-attribute="iteration"
issuable-type="issue"
iterations-workspace-path="gitlab-org"
workspace-path="gitlab-org/gitlab-test"
/>
</div>
......
......@@ -63,7 +63,6 @@ describe('ee/BoardContentSidebar', () => {
SidebarSubscriptionsWidget: true,
BoardSidebarMilestoneSelect: true,
BoardSidebarWeightInput: true,
SidebarIterationWidget: true,
SidebarDropdownWidget: true,
},
});
......
import {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlLink,
GlSearchBoxByType,
GlFormInput,
GlLoadingIcon,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import SidebarIterationWidget from 'ee/sidebar/components/sidebar_iteration_widget.vue';
import { iterationSelectTextMap, iterationDisplayState } from 'ee/sidebar/constants';
import groupIterationsQuery from 'ee/sidebar/queries/group_iterations.query.graphql';
import projectIssueIterationMutation from 'ee/sidebar/queries/project_issue_iteration.mutation.graphql';
import projectIssueIterationQuery from 'ee/sidebar/queries/project_issue_iteration.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
mockIssue,
mockGroupIterationsResponse,
mockIteration2,
mockIterationMutationResponse,
emptyGroupIterationsResponse,
noCurrentIterationResponse,
} from '../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
describe('SidebarIterationWidget', () => {
let wrapper;
let mockApollo;
const promiseData = { issuableSetIteration: { issue: { iteration: { id: '123' } } } };
const firstErrorMsg = 'first error';
const promiseWithErrors = {
...promiseData,
issuableSetIteration: { ...promiseData.issuableSetIteration, errors: [firstErrorMsg] },
};
const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
const mutationError = () => jest.fn().mockRejectedValue();
const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors });
const findGlLink = () => wrapper.find(GlLink);
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownText = () => wrapper.find(GlDropdownText);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findDropdownItemWithText = (text) =>
findAllDropdownItems().wrappers.find((x) => x.text() === text);
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
const findEditableLoadingIcon = () => findSidebarEditableItem().find(GlLoadingIcon);
const findIterationItems = () => wrapper.findByTestId('iteration-items');
const findSelectedIteration = () => wrapper.findByTestId('select-iteration');
const findNoIterationItem = () => wrapper.findByTestId('no-iteration-item');
const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
const waitForDropdown = async () => {
// BDropdown first changes its `visible` property
// in a requestAnimationFrame callback.
// It then emits `shown` event in a watcher for `visible`
// Hence we need both of these:
await waitForPromises();
await wrapper.vm.$nextTick();
};
const waitForApollo = async () => {
jest.runOnlyPendingTimers();
await waitForPromises();
};
// Used with createComponentWithApollo which uses 'mount'
const clickEdit = async () => {
await findEditButton().trigger('click');
await waitForDropdown();
// We should wait for iterations list to be fetched.
await waitForApollo();
};
// Used with createComponent which shallow mounts components
const toggleDropdown = async () => {
wrapper.vm.$refs.editable.expand();
await waitForDropdown();
};
const createComponentWithApollo = async ({
requestHandlers = [],
currentIterationSpy = jest.fn().mockResolvedValue(noCurrentIterationResponse),
groupIterationsSpy = jest.fn().mockResolvedValue(mockGroupIterationsResponse),
} = {}) => {
localVue.use(VueApollo);
mockApollo = createMockApollo([
[projectIssueIterationQuery, currentIterationSpy],
[groupIterationsQuery, groupIterationsSpy],
...requestHandlers,
]);
wrapper = extendedWrapper(
mount(SidebarIterationWidget, {
localVue,
provide: { canUpdate: true },
apolloProvider: mockApollo,
propsData: {
workspacePath: mockIssue.projectPath,
iterationsWorkspacePath: mockIssue.groupPath,
iid: mockIssue.iid,
issuableType: IssuableType.Issue,
},
attachTo: document.body,
}),
);
await waitForApollo();
};
const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(SidebarIterationWidget, {
provide: { canUpdate: true },
data() {
return data;
},
propsData: {
workspacePath: '',
iterationsWorkspacePath: '',
iid: '',
issuableType: IssuableType.Issue,
},
mocks: {
$apollo: {
mutate: mutationPromise(),
queries: {
currentIteration: { loading: false },
iterations: { loading: false },
...queries,
},
},
},
stubs: {
SidebarEditableItem,
GlSearchBoxByType,
GlDropdown,
},
}),
);
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when not editing', () => {
beforeEach(() => {
createComponent({
data: {
currentIteration: { id: 'id', title: 'title', webUrl: 'webUrl' },
},
stubs: {
GlDropdown,
SidebarEditableItem,
},
});
});
it('shows the current iteration', () => {
expect(findSelectedIteration().text()).toBe('title');
});
it('links to the current iteration', () => {
expect(findGlLink().attributes().href).toBe('webUrl');
});
it('does not show a loading spinner next to the iteration heading', () => {
expect(findEditableLoadingIcon().exists()).toBe(false);
});
it('shows a loading spinner while fetching the current iteration', () => {
createComponent({
queries: {
currentIteration: { loading: true },
},
});
expect(findEditableLoadingIcon().exists()).toBe(true);
});
it('shows the title of the selected iteration while updating', () => {
createComponent({
data: {
updating: true,
selectedTitle: 'Some iteration title',
},
queries: {
currentIteration: { loading: false },
},
});
expect(findEditableLoadingIcon().exists()).toBe(true);
expect(findSelectedIteration().text()).toBe('Some iteration title');
});
describe('when current iteration does not exist', () => {
it('renders "None" as the selected iteration title', () => {
createComponent();
expect(findSelectedIteration().text()).toBe('None');
});
});
});
describe('when a user can edit', () => {
describe('when user is editing', () => {
describe('when rendering the dropdown', () => {
it('shows a loading spinner while fetching a list of iterations', async () => {
createComponent({
queries: {
iterations: { loading: true },
},
});
await toggleDropdown();
expect(findLoadingIconDropdown().exists()).toBe(true);
});
describe('GlDropdownItem with the right title and id', () => {
const id = 'id';
const title = 'title';
beforeEach(async () => {
createComponent({
data: { iterations: [{ id, title }], currentIteration: { id, title } },
});
await toggleDropdown();
});
it('does not show a loading spinner', () => {
expect(findLoadingIconDropdown().exists()).toBe(false);
});
it('renders title $title', () => {
expect(findDropdownItemWithText(title).text()).toBe(title);
});
it('checks the correct dropdown item', () => {
expect(
findAllDropdownItems()
.filter((w) => w.props('isChecked') === true)
.at(0)
.text(),
).toBe(title);
});
});
describe('when no data is assigned', () => {
beforeEach(async () => {
createComponent();
await toggleDropdown();
});
it('finds GlDropdownItem with "No iteration"', () => {
expect(findNoIterationItem().text()).toBe('No iteration');
});
it('"No iteration" is checked', () => {
expect(findNoIterationItem().props('isChecked')).toBe(true);
});
it('does not render any dropdown item', () => {
expect(findIterationItems().exists()).toBe(false);
});
});
describe('when clicking on dropdown item', () => {
describe('when currentIteration is equal to iteration id', () => {
it('does not call setIssueIteration mutation', async () => {
createComponent({
data: {
iterations: [{ id: 'id', title: 'title' }],
currentIteration: { id: 'id', title: 'title' },
},
});
await toggleDropdown();
findDropdownItemWithText('title').vm.$emit('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
});
});
describe('when currentIteration is not equal to iteration id', () => {
describe('when error', () => {
const bootstrapComponent = (mutationResp) => {
createComponent({
data: {
iterations: [
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
currentIteration: '123',
},
mutationPromise: mutationResp,
});
};
describe.each`
description | mutationResp | expectedMsg
${'top-level error'} | ${mutationError} | ${iterationSelectTextMap.iterationSelectFail}
${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg}
`(`$description`, ({ mutationResp, expectedMsg }) => {
beforeEach(async () => {
bootstrapComponent(mutationResp);
await toggleDropdown();
findDropdownItemWithText('title').vm.$emit('click');
});
it(`calls createFlash with "${expectedMsg}"`, async () => {
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenCalledWith(expectedMsg);
});
});
});
});
});
});
describe('when a user is searching', () => {
describe('when search result is not found', () => {
it('renders "No iterations found"', async () => {
createComponent();
await toggleDropdown();
findSearchBox().vm.$emit('input', 'non existing iterations');
await wrapper.vm.$nextTick();
expect(findDropdownText().text()).toBe('No iterations found');
});
});
});
});
});
describe('with mock apollo', () => {
let error;
beforeEach(() => {
jest.spyOn(Sentry, 'captureException');
error = new Error('mayday');
});
describe("when issuable type is 'issue'", () => {
describe('when dropdown is expanded and user can edit', () => {
let iterationMutationSpy;
beforeEach(async () => {
iterationMutationSpy = jest.fn().mockResolvedValue(mockIterationMutationResponse);
await createComponentWithApollo({
requestHandlers: [[projectIssueIterationMutation, iterationMutationSpy]],
});
await clickEdit();
});
it('renders the dropdown on clicking edit', async () => {
expect(findDropdown().isVisible()).toBe(true);
});
it('focuses on the input when dropdown is shown', async () => {
expect(document.activeElement).toEqual(wrapper.find(GlFormInput).element);
});
describe('when currentIteration is not equal to iteration id', () => {
describe('when update is successful', () => {
beforeEach(() => {
findDropdownItemWithText(mockIteration2.title).vm.$emit('click');
});
it('calls setIssueIteration mutation', () => {
expect(iterationMutationSpy).toHaveBeenCalledWith({
iid: mockIssue.iid,
iterationId: mockIteration2.id,
fullPath: mockIssue.projectPath,
});
});
it('sets the value returned from the mutation to currentIteration', async () => {
expect(findSelectedIteration().text()).toBe(mockIteration2.title);
});
});
});
describe('iterations', () => {
let groupIterationsSpy;
it('should call createFlash and Sentry if iterations query fails', async () => {
await createComponentWithApollo({
groupIterationsSpy: jest.fn().mockRejectedValue(error),
});
await clickEdit();
expect(createFlash).toHaveBeenNthCalledWith(1, {
message: wrapper.vm.$options.i18n.iterationsFetchError,
});
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(error);
});
it('only fetches iterations when dropdown is opened', async () => {
groupIterationsSpy = jest.fn().mockResolvedValueOnce(emptyGroupIterationsResponse);
await createComponentWithApollo({ groupIterationsSpy });
expect(groupIterationsSpy).not.toHaveBeenCalled();
await clickEdit();
expect(groupIterationsSpy).toHaveBeenNthCalledWith(1, {
fullPath: mockIssue.groupPath,
title: '',
state: iterationDisplayState,
});
});
describe('when a user is searching', () => {
const mockSearchTerm = 'foobar';
beforeEach(async () => {
groupIterationsSpy = jest.fn().mockResolvedValueOnce(emptyGroupIterationsResponse);
await createComponentWithApollo({ groupIterationsSpy });
await clickEdit();
});
it('sends a groupIterations query with the entered search term "foo"', async () => {
findSearchBox().vm.$emit('input', mockSearchTerm);
await wrapper.vm.$nextTick();
// Account for debouncing
jest.runAllTimers();
expect(groupIterationsSpy).toHaveBeenNthCalledWith(2, {
fullPath: mockIssue.groupPath,
title: mockSearchTerm,
state: iterationDisplayState,
});
});
});
});
});
describe('currentIterations', () => {
it('should call createFlash and Sentry if currentIterations query fails', async () => {
await createComponentWithApollo({
currentIterationSpy: jest.fn().mockRejectedValue(error),
});
expect(createFlash).toHaveBeenNthCalledWith(1, {
message: wrapper.vm.$options.i18n.currentIterationFetchError,
});
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(error);
});
});
});
});
});
......@@ -72,13 +72,13 @@ RSpec.shared_examples 'issue boards sidebar EE' do
select_iteration(iteration.title)
expect(page.find('[data-testid="iteration-edit-link"]')).to have_content('Iteration 1')
expect(page.find('[data-testid="iteration-edit"]')).to have_content('Iteration 1')
find_and_click_edit_iteration
select_iteration('No iteration')
expect(page.find('[data-testid="iteration-edit-link"]')).to have_content('None')
expect(page.find('[data-testid="iteration-edit"]')).to have_content('None')
end
context 'when iteration feature is not available' do
......@@ -90,15 +90,15 @@ RSpec.shared_examples 'issue boards sidebar EE' do
wait_for_all_requests
end
it 'cannot find the iteration-edit-link' do
expect(page).not_to have_selector('[data-testid="iteration-edit-link"]')
it 'cannot find the iteration-edit' do
expect(page).not_to have_selector('[data-testid="iteration-edit"]')
end
end
end
end
def find_and_click_edit_iteration
page.find('[data-testid="iteration-edit-link"] [data-testid="edit-button"]').click
page.find('[data-testid="iteration-edit"] [data-testid="edit-button"]').click
wait_for_all_requests
end
......
......@@ -16,11 +16,6 @@ module QA
element :edit_link
end
view 'ee/app/assets/javascripts/sidebar/components/sidebar_iteration_widget.vue' do
element :iteration_container
element :iteration_link
end
view 'ee/app/assets/javascripts/sidebar/components/weight/weight.vue' do
element :edit_weight_link
element :remove_weight_link
......
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