Commit f1bdac24 authored by Markus Koller's avatar Markus Koller

Merge branch 'move-timebox-reports-graphql' into 'master'

Generalize burnup chart service for other metrics

See merge request gitlab-org/gitlab!45121
parents 8c801481 47c780c0
......@@ -6,13 +6,13 @@ import {
GlDatepicker,
GlLink,
GlSprintf,
GlSearchBoxByType,
GlButton,
GlFormInput,
} from '@gitlab/ui';
import eventHub from '../event_hub';
import { s__, sprintf } from '~/locale';
import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
export default {
name: 'InviteMembersModal',
......@@ -23,9 +23,9 @@ export default {
GlDropdown,
GlDropdownItem,
GlSprintf,
GlSearchBoxByType,
GlButton,
GlFormInput,
MembersTokenSelect,
},
props: {
groupId: {
......@@ -129,44 +129,45 @@ export default {
},
labels: {
modalTitle: s__('InviteMembersModal|Invite team members'),
userToInvite: s__('InviteMembersModal|GitLab member or Email address'),
newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'),
userPlaceholder: s__('InviteMembersModal|Search for members to invite'),
accessLevel: s__('InviteMembersModal|Choose a role permission'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Users were succesfully added'),
toastMessageUnsuccessful: s__('InviteMembersModal|User not invited. Feature coming soon!'),
toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
toastMessageUnsuccessful: s__('InviteMembersModal|Some of the members could not be added'),
readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
inviteButtonText: s__('InviteMembersModal|Invite'),
cancelButtonText: s__('InviteMembersModal|Cancel'),
headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
},
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
<template>
<gl-modal :modal-id="modalId" size="sm" :title="$options.labels.modalTitle">
<gl-modal
:modal-id="modalId"
size="sm"
:title="$options.labels.modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
>
<div class="gl-ml-5 gl-mr-5">
<div>{{ introText }}</div>
<label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.userToInvite }}</label>
<label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{
$options.labels.newUsersToInvite
}}</label>
<div class="gl-mt-2">
<gl-search-box-by-type
<members-token-select
v-model="newUsersToInvite"
:label="$options.labels.newUsersToInvite"
:aria-labelledby="$options.membersTokenSelectLabelId"
:placeholder="$options.labels.userPlaceholder"
type="text"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</div>
<label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
menu-class="dropdown-menu-selectable"
class="gl-shadow-none gl-w-full"
v-bind="$attrs"
:text="selectedRoleName"
>
<gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName">
<template v-for="(key, item) in accessLevels">
<gl-dropdown-item
:key="key"
......@@ -215,9 +216,13 @@ export default {
{{ $options.labels.cancelButtonText }}
</gl-button>
<div class="gl-mr-3"></div>
<gl-button ref="inviteButton" variant="success" @click="sendInvite">{{
$options.labels.inviteButtonText
}}</gl-button>
<gl-button
ref="inviteButton"
:disabled="!newUsersToInvite"
variant="success"
@click="sendInvite"
>{{ $options.labels.inviteButtonText }}</gl-button
>
</div>
</template>
</gl-modal>
......
<script>
import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled } from '@gitlab/ui';
import { USER_SEARCH_DELAY } from '../constants';
import Api from '~/api';
export default {
components: {
GlTokenSelector,
GlAvatar,
GlAvatarLabeled,
},
props: {
placeholder: {
type: String,
required: false,
default: '',
},
ariaLabelledby: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
query: '',
users: [],
selectedTokens: [],
hasBeenFocused: false,
hideDropdownWithNoItems: true,
};
},
computed: {
newUsersToInvite() {
return this.selectedTokens
.map(obj => {
return obj.id;
})
.join(',');
},
placeholderText() {
if (this.selectedTokens.length === 0) {
return this.placeholder;
}
return '';
},
},
methods: {
handleTextInput(query) {
this.hideDropdownWithNoItems = false;
this.query = query;
this.loading = true;
this.retrieveUsers(query);
},
retrieveUsers: debounce(function debouncedRetrieveUsers() {
return Api.users(this.query, this.$options.queryOptions)
.then(response => {
this.users = response.data.map(token => ({
id: token.id,
name: token.name,
username: token.username,
avatar_url: token.avatar_url,
}));
this.loading = false;
})
.catch(() => {
this.loading = false;
});
}, USER_SEARCH_DELAY),
handleInput() {
this.$emit('input', this.newUsersToInvite);
},
handleBlur() {
this.hideDropdownWithNoItems = false;
},
handleFocus() {
// The modal auto-focuses on the input when opened.
// This prevents the dropdown from opening when the modal opens.
if (this.hasBeenFocused) {
this.loading = true;
this.retrieveUsers();
}
this.hasBeenFocused = true;
},
},
queryOptions: { exclude_internal: true, active: true },
};
</script>
<template>
<gl-token-selector
v-model="selectedTokens"
:dropdown-items="users"
:loading="loading"
:allow-user-defined-tokens="false"
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
@focus="handleFocus"
>
<template #token-content="{ token }">
<gl-avatar v-if="token.avatar_url" :src="token.avatar_url" :size="16" />
{{ token.name }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<gl-avatar-labeled
:src="dropdownItem.avatar_url"
:size="32"
:label="dropdownItem.name"
:sub-label="dropdownItem.username"
/>
</template>
</gl-token-selector>
</template>
export const USER_SEARCH_DELAY = 200;
......@@ -115,14 +115,10 @@ code {
background-color: $gray-50;
border-radius: $border-radius-default;
.code > & {
background-color: inherit;
padding: unset;
}
.code > &,
.build-trace & {
background-color: inherit;
padding: inherit;
padding: unset;
}
}
......
---
title: Fix code lines being cut-off on failed job tab
merge_request: 46885
author:
type: fixed
......@@ -85,31 +85,29 @@ describe('IterationSelect', () => {
});
describe('when a user can edit', () => {
it('opens the dropdown on click of the edit button', () => {
it('opens the dropdown on click of the edit button', async () => {
createComponent({ props: { canEdit: true } });
expect(wrapper.find(GlDropdown).isVisible()).toBe(false);
toggleDropdown();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlDropdown).isVisible()).toBe(true);
});
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).isVisible()).toBe(true);
});
it('focuses on the input', () => {
it('focuses on the input', async () => {
createComponent({ props: { canEdit: true } });
const spy = jest.spyOn(wrapper.vm.$refs.search, 'focusInput');
toggleDropdown();
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalled();
});
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalled();
});
it('stops propagation of the click event to avoid opening milestone dropdown', () => {
it('stops propagation of the click event to avoid opening milestone dropdown', async () => {
const spy = jest.fn();
createComponent({ props: { canEdit: true } });
......@@ -117,9 +115,8 @@ describe('IterationSelect', () => {
toggleDropdown(spy);
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1);
});
describe('when user is editing', () => {
......@@ -214,10 +211,9 @@ describe('IterationSelect', () => {
});
});
it('sets the value returned from the mutation to currentIteration', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentIteration).toBe('123');
});
it('sets the value returned from the mutation to currentIteration', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.vm.currentIteration).toBe('123');
});
});
......@@ -247,10 +243,9 @@ describe('IterationSelect', () => {
.vm.$emit('click');
});
it('calls createFlash with $expectedMsg', () => {
return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalledWith(expectedMsg);
});
it('calls createFlash with $expectedMsg', async () => {
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenCalledWith(expectedMsg);
});
});
});
......@@ -263,33 +258,31 @@ describe('IterationSelect', () => {
createComponent({});
});
it('sets the search term', () => {
it('sets the search term', async () => {
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'testing');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.searchTerm).toBe('testing');
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.searchTerm).toBe('testing');
});
});
describe('when the user off clicks', () => {
describe('when the dropdown is open', () => {
beforeEach(() => {
beforeEach(async () => {
createComponent({});
toggleDropdown();
return wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
});
it('closes the dropdown', () => {
it('closes the dropdown', async () => {
expect(wrapper.find(GlDropdown).isVisible()).toBe(true);
toggleDropdown();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlDropdown).isVisible()).toBe(false);
});
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).isVisible()).toBe(false);
});
});
});
......
......@@ -29,71 +29,67 @@ describe('SidebarItemEpicsSelect', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('methods', () => {
describe('getInitialEpicLoading', () => {
it('should return `false` when `initialEpic` prop is provided', () => {
it('should return `false` when `initialEpic` prop is provided', async () => {
wrapper.setProps({
initialEpic: mockEpic1,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
it('should return value of `sidebarStore.isFetching.epic` when `initialEpic` prop is null and `isFetching` is available', () => {
it('should return value of `sidebarStore.isFetching.epic` when `initialEpic` prop is null and `isFetching` is available', async () => {
wrapper.setProps({
sidebarStore: { isFetching: { epic: true } },
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getInitialEpicLoading()).toBe(true);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(true);
});
it('should return `false` when both `initialEpic` and `sidebarStore.isFetching` are unavailable', () => {
it('should return `false` when both `initialEpic` and `sidebarStore.isFetching` are unavailable', async () => {
wrapper.setProps({
initialEpic: null,
sidebarStore: { isFetching: null },
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
});
describe('getEpic', () => {
it('should return value of `initialEpic` as it is when it is available', () => {
it('should return value of `initialEpic` as it is when it is available', async () => {
wrapper.setProps({
initialEpic: mockEpic1,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getEpic()).toBe(mockEpic1);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.getEpic()).toBe(mockEpic1);
});
it('should return value of `sidebarStore.epic` as it is when it is available', () => {
expect(wrapper.vm.getEpic()).toBe(mockEpic1);
});
it('should return No Epic object as it is when both `initialEpic` & `sidebarStore.epic` are unavailable', () => {
it('should return No Epic object as it is when both `initialEpic` & `sidebarStore.epic` are unavailable', async () => {
wrapper.setProps({
initialEpic: null,
sidebarStore: { epic: null },
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getEpic()).toEqual(
expect.objectContaining({
id: 0,
title: 'No Epic',
}),
);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.getEpic()).toEqual(
expect.objectContaining({
id: 0,
title: 'No Epic',
}),
);
});
});
});
......
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Vue from 'vue';
import Status from 'ee/sidebar/components/status/status.vue';
import { healthStatus, healthStatusTextMap } from 'ee/sidebar/constants';
......@@ -46,6 +45,7 @@ describe('Status', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the text "Status"', () => {
......@@ -106,7 +106,7 @@ describe('Status', () => {
});
describe('remove status dropdown item', () => {
it('is displayed when there is a status', () => {
it('is displayed when there is a status', async () => {
const props = {
isEditable: true,
status: healthStatus.AT_RISK,
......@@ -116,9 +116,8 @@ describe('Status', () => {
wrapper.vm.isDropdownShowing = true;
wrapper.vm.$nextTick(() => {
expect(getRemoveStatusItem(wrapper).exists()).toBe(true);
});
await wrapper.vm.$nextTick();
expect(getRemoveStatusItem(wrapper).exists()).toBe(true);
});
it('emits an onDropdownClick event with argument null when clicked', () => {
......@@ -201,12 +200,11 @@ describe('Status', () => {
mountStatus(props);
});
it('shows the dropdown when the Edit button is clicked', () => {
it('shows the dropdown when the Edit button is clicked', async () => {
getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => {
expect(getDropdownClasses(wrapper)).toContain('show');
});
await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('show');
});
});
......@@ -231,22 +229,20 @@ describe('Status', () => {
).toContain(message);
});
it('hides form when the `edit` button is clicked', () => {
it('hides form when the `edit` button is clicked', async () => {
getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => {
expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
it('hides form when a dropdown item is clicked', () => {
it('hides form when a dropdown item is clicked', async () => {
const dropdownItem = wrapper.findAll(GlDropdownItem).at(1);
dropdownItem.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
});
......@@ -285,15 +281,14 @@ describe('Status', () => {
// Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted
it.each(getIterableArray(Object.values(healthStatus)))(
'emits onFormSubmit event with argument "%s" when user selects the option and submits form',
(status, index) => {
async (status, index) => {
wrapper
.findAll(GlDropdownItem)
.at(index + 1)
.vm.$emit('click', { preventDefault: () => null });
return Vue.nextTick().then(() => {
expect(wrapper.emitted().onDropdownClick[0]).toEqual([status]);
});
await wrapper.vm.$nextTick();
expect(wrapper.emitted().onDropdownClick[0]).toEqual([status]);
},
);
});
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { escape } from 'lodash';
import ancestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { GlLoadingIcon } from '@gitlab/ui';
import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
describe('AncestorsTreeContainer', () => {
let vm;
let wrapper;
const ancestors = [
{ id: 1, url: '', title: 'A', state: 'open' },
{ id: 2, url: '', title: 'B', state: 'open' },
];
beforeEach(() => {
const AncestorsTreeContainer = Vue.extend(ancestorsTree);
vm = mountComponent(AncestorsTreeContainer, { ancestors, isFetching: false });
});
const defaultProps = {
ancestors,
isFetching: false,
};
const createComponent = (props = {}) => {
wrapper = shallowMount(AncestorsTree, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
vm.$destroy();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findTooltip = () => wrapper.find('.collapse-truncated-title');
const containsTimeline = () => wrapper.contains('.vertical-timeline');
const containsValue = () => wrapper.contains('.value');
it('renders all ancestors rows', () => {
expect(vm.$el.querySelectorAll('.vertical-timeline-row')).toHaveLength(ancestors.length);
createComponent();
expect(wrapper.findAll('.vertical-timeline-row')).toHaveLength(ancestors.length);
});
it('renders tooltip with the immediate parent', () => {
expect(vm.$el.querySelector('.collapse-truncated-title').innerText.trim()).toBe(
ancestors.slice(-1)[0].title,
);
createComponent();
expect(findTooltip().text()).toBe(ancestors.slice(-1)[0].title);
});
it('does not render timeline when fetching', () => {
vm.$props.isFetching = true;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.vertical-timeline')).toBeNull();
expect(vm.$el.querySelector('.value')).toBeNull();
createComponent({
isFetching: true,
});
expect(containsTimeline()).toBe(false);
expect(containsValue()).toBe(false);
});
it('render `None` when ancestors is an empty array', () => {
vm.$props.ancestors = [];
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.vertical-timeline')).toBeNull();
expect(vm.$el.querySelector('.value')).not.toBeNull();
createComponent({
ancestors: [],
});
expect(containsTimeline()).toBe(false);
expect(containsValue()).not.toBe(false);
});
it('render loading icon when isFetching is true', () => {
vm.$props.isFetching = true;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
createComponent({
isFetching: true,
});
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
});
it('escapes html in the tooltip', () => {
const title = '<script>alert(1);</script>';
const escapedTitle = escape(title);
vm.$props.ancestors = [{ id: 1, url: '', title, state: 'open' }];
return vm.$nextTick().then(() => {
const tooltip = vm.$el.querySelector('.collapse-truncated-title');
expect(tooltip.innerText).toBe(escapedTitle);
createComponent({
ancestors: [{ id: 1, url: '', title, state: 'open' }],
});
expect(findTooltip().text()).toBe(escapedTitle);
});
});
import Vue from 'vue';
import sidebarWeight from 'ee/sidebar/components/weight/sidebar_weight.vue';
import { shallowMount } from '@vue/test-utils';
import SidebarWeight from 'ee/sidebar/components/weight/sidebar_weight.vue';
import SidebarMediator from 'ee/sidebar/sidebar_mediator';
import SidebarStore from 'ee/sidebar/stores/sidebar_store';
import mountComponent from 'helpers/vue_mount_component_helper';
import SidebarService from '~/sidebar/services/sidebar_service';
import eventHub from '~/sidebar/event_hub';
import Mock from './ee_mock_data';
describe('Sidebar Weight', () => {
let vm;
let sidebarMediator;
let SidebarWeight;
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(SidebarWeight, {
propsData: { ...props },
});
};
beforeEach(() => {
SidebarWeight = Vue.extend(sidebarWeight);
// Set up the stores, services, etc
sidebarMediator = new SidebarMediator(Mock.mediator);
});
afterEach(() => {
vm.$destroy();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
......@@ -27,7 +33,8 @@ describe('Sidebar Weight', () => {
it('calls the mediator updateWeight on event', () => {
jest.spyOn(SidebarMediator.prototype, 'updateWeight').mockReturnValue(Promise.resolve());
vm = mountComponent(SidebarWeight, {
createComponent({
mediator: sidebarMediator,
});
......
import Vue from 'vue';
import weight from 'ee/sidebar/components/weight/weight.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import Weight from 'ee/sidebar/components/weight/weight.vue';
import eventHub from '~/sidebar/event_hub';
import { ENTER_KEY_CODE } from '~/lib/utils/keycodes';
const DEFAULT_PROPS = {
weightNoneValue: 'None',
};
describe('Weight', () => {
let vm;
let Weight;
let wrapper;
beforeEach(() => {
Weight = Vue.extend(weight);
});
const defaultProps = {
weightNoneValue: 'None',
};
const createComponent = (props = {}) => {
wrapper = shallowMount(Weight, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
vm.$destroy();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const containsCollapsedLoadingIcon = () => wrapper.contains('.js-weight-collapsed-loading-icon');
const containsLoadingIcon = () => wrapper.contains('.js-weight-loading-icon');
const findCollapsedLabel = () => wrapper.find('.js-weight-collapsed-weight-label');
const findLabelValue = () => wrapper.find('.js-weight-weight-label-value');
const findLabelNoValue = () => wrapper.find('.js-weight-weight-label .no-value');
const findCollapsedBlock = () => wrapper.find('.js-weight-collapsed-block');
const findEditLink = () => wrapper.find('.js-weight-edit-link');
const findRemoveLink = () => wrapper.find('.js-weight-remove-link');
const containsEditableField = () => wrapper.contains({ ref: 'editableField' });
const containsInputError = () => wrapper.contains('.gl-field-error');
it('shows loading spinner when fetching', () => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
createComponent({
fetching: true,
});
expect(vm.$el.querySelector('.js-weight-collapsed-loading-icon')).not.toBeNull();
expect(vm.$el.querySelector('.js-weight-loading-icon')).not.toBeNull();
expect(containsCollapsedLoadingIcon()).toBe(true);
expect(containsLoadingIcon()).toBe(true);
});
it('shows loading spinner when loading', () => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
createComponent({
fetching: false,
loading: true,
});
// We show the value in the collapsed view instead of the loading icon
expect(vm.$el.querySelector('.js-weight-collapsed-loading-icon')).toBeNull();
expect(vm.$el.querySelector('.js-weight-loading-icon')).not.toBeNull();
expect(containsCollapsedLoadingIcon()).toBe(false);
expect(containsLoadingIcon()).toBe(true);
});
it('shows weight value', () => {
const WEIGHT = 3;
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
const expectedWeight = 3;
createComponent({
fetching: false,
weight: WEIGHT,
weight: expectedWeight,
});
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toBe(
`${WEIGHT}`,
);
expect(vm.$el.querySelector('.js-weight-weight-label-value').textContent.trim()).toBe(
`${WEIGHT}`,
);
expect(findCollapsedLabel().text()).toBe(`${expectedWeight}`);
expect(findLabelValue().text()).toBe(`${expectedWeight}`);
});
it('shows weight no-value', () => {
const WEIGHT = null;
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
const expectedWeight = null;
createComponent({
fetching: false,
weight: WEIGHT,
weight: expectedWeight,
});
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toBe(
'None',
);
expect(vm.$el.querySelector('.js-weight-weight-label .no-value').textContent.trim()).toBe(
'None',
);
expect(findCollapsedLabel().text()).toBe(defaultProps.weightNoneValue);
expect(findLabelNoValue().text()).toBe(defaultProps.weightNoneValue);
});
it('adds `collapse-after-update` class when clicking the collapsed block', () => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
});
it('adds `collapse-after-update` class when clicking the collapsed block', async () => {
createComponent();
vm.$el.querySelector('.js-weight-collapsed-block').click();
findCollapsedBlock().trigger('click');
return vm.$nextTick().then(() => {
expect(vm.$el.classList.contains('collapse-after-update')).toBe(true);
});
await wrapper.vm.$nextTick;
expect(wrapper.classes()).toContain('collapse-after-update');
});
it('shows dropdown on "Edit" link click', () => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
it('shows dropdown on "Edit" link click', async () => {
createComponent({
editable: true,
});
expect(vm.shouldShowEditField).toBe(false);
expect(containsEditableField()).toBe(false);
vm.$el.querySelector('.js-weight-edit-link').click();
findEditLink().trigger('click');
return vm.$nextTick().then(() => {
expect(vm.shouldShowEditField).toBe(true);
});
await wrapper.vm.$nextTick;
expect(containsEditableField()).toBe(true);
});
it('emits event on input submission', () => {
const ID = 123;
it('emits event on input submission', async () => {
const mockId = 123;
const expectedWeightValue = '3';
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
createComponent({
editable: true,
id: ID,
id: mockId,
});
vm.$el.querySelector('.js-weight-edit-link').click();
findEditLink().trigger('click');
return vm.$nextTick(() => {
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
await wrapper.vm.$nextTick;
vm.$refs.editableField.click();
vm.$refs.editableField.value = expectedWeightValue;
vm.$refs.editableField.dispatchEvent(event);
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
expect(vm.hasValidInput).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', expectedWeightValue, ID);
});
const { editableField } = wrapper.vm.$refs;
editableField.click();
editableField.value = expectedWeightValue;
editableField.dispatchEvent(event);
await wrapper.vm.$nextTick;
expect(containsInputError()).toBe(false);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', expectedWeightValue, mockId);
});
it('emits event on remove weight link click', () => {
const ID = 123;
it('emits event on remove weight link click', async () => {
const mockId = 234;
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
createComponent({
editable: true,
weight: 3,
id: ID,
id: mockId,
});
vm.$el.querySelector('.js-weight-remove-link').click();
findRemoveLink().trigger('click');
return vm.$nextTick(() => {
expect(vm.hasValidInput).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', '', ID);
});
await wrapper.vm.$nextTick;
expect(containsInputError()).toBe(false);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', '', mockId);
});
it('triggers error on invalid negative integer value', () => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
it('triggers error on invalid negative integer value', async () => {
createComponent({
editable: true,
});
vm.$el.querySelector('.js-weight-edit-link').click();
findEditLink().trigger('click');
return vm.$nextTick(() => {
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
await wrapper.vm.$nextTick;
vm.$refs.editableField.click();
vm.$refs.editableField.value = -9001;
vm.$refs.editableField.dispatchEvent(event);
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
expect(vm.hasValidInput).toBe(false);
});
const { editableField } = wrapper.vm.$refs;
editableField.click();
editableField.value = -9001;
editableField.dispatchEvent(event);
await wrapper.vm.$nextTick;
expect(containsInputError()).toBe(true);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
createComponent({
editable: true,
});
trackingSpy = mockTracking('_category_', vm.$el, (obj, what) =>
trackingSpy = mockTracking('_category_', wrapper.element, (obj, what) =>
jest.spyOn(obj, what).mockImplementation(() => {}),
);
});
......@@ -184,12 +190,12 @@ describe('Weight', () => {
unmockTracking();
});
it('calls trackEvent when "Edit" is clicked', () => {
triggerEvent(vm.$el.querySelector('.js-weight-edit-link'));
it('calls trackEvent when "Edit" is clicked', async () => {
triggerEvent(findEditLink().element);
return vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalled();
});
await wrapper.vm.$nextTick;
expect(trackingSpy).toHaveBeenCalled();
});
});
});
......@@ -14732,6 +14732,9 @@ msgstr ""
msgid "InviteMembersModal|Choose a role permission"
msgstr ""
msgid "InviteMembersModal|Close invite team members"
msgstr ""
msgid "InviteMembersModal|GitLab member or Email address"
msgstr ""
......@@ -14741,13 +14744,13 @@ msgstr ""
msgid "InviteMembersModal|Invite team members"
msgstr ""
msgid "InviteMembersModal|Search for members to invite"
msgid "InviteMembersModal|Members were successfully added"
msgstr ""
msgid "InviteMembersModal|User not invited. Feature coming soon!"
msgid "InviteMembersModal|Search for members to invite"
msgstr ""
msgid "InviteMembersModal|Users were succesfully added"
msgid "InviteMembersModal|Some of the members could not be added"
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{group_name} group"
......
......@@ -2,11 +2,8 @@
module QA
RSpec.describe 'Verify' do
describe 'Run pipeline', :requires_admin, :skip_live_env do
# [TODO]: Developer to remove :requires_admin and :skip_live_env once FF is removed in https://gitlab.com/gitlab-org/gitlab/-/issues/229632
describe 'Run pipeline', only: { subdomain: :staging } do
context 'with web only rule' do
let(:feature_flag) { :new_pipeline_form }
let(:job_name) { 'test_job' }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
......@@ -20,33 +17,29 @@ module QA
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
#{job_name}:
tags:
- #{project.name}
script: echo 'OK'
only:
- web
YAML
}
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
#{job_name}:
tags:
- #{project.name}
script: echo 'OK'
only:
- web
YAML
}
]
)
end
end
before do
Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
Flow::Login.sign_in
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
end
after do
Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
end
it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/946' do
Page::Project::Pipeline::Index.perform do |index|
expect(index).not_to have_pipeline # should not auto trigger pipeline
......
......@@ -9,7 +9,7 @@ const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, O
const defaultAccessLevel = '10';
const helpLink = 'https://example.com';
const createComponent = () => {
const createComponent = (data = {}) => {
return shallowMount(InviteMembersModal, {
propsData: {
groupId,
......@@ -18,9 +18,14 @@ const createComponent = () => {
defaultAccessLevel,
helpLink,
},
data() {
return data;
},
stubs: {
GlSprintf,
'gl-modal': '<div><slot name="modal-footer"></slot><slot></slot></div>',
'gl-dropdown': true,
'gl-dropdown-item': true,
GlSprintf,
},
});
};
......@@ -34,7 +39,7 @@ describe('InviteMembersModal', () => {
});
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findDropdownItems = () => findDropdown().findAll(GlDropdownItem);
const findDatepicker = () => wrapper.find(GlDatepicker);
const findLink = () => wrapper.find(GlLink);
const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
......@@ -88,25 +93,69 @@ describe('InviteMembersModal', () => {
format: 'json',
};
beforeEach(() => {
wrapper = createComponent();
describe('when the invite was sent successfully', () => {
beforeEach(() => {
wrapper = createComponent();
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMember').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'inviteGroupMember').mockResolvedValue({ data: postData });
wrapper.vm.$toast = { show: jest.fn() };
wrapper.vm.submitForm(postData);
});
wrapper.vm.submitForm(postData);
it('displays the successful toastMessage', () => {
const toastMessageSuccessful = 'Members were successfully added';
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
toastMessageSuccessful,
wrapper.vm.toastOptions,
);
});
it('calls Api inviteGroupMember with the correct params', () => {
expect(Api.inviteGroupMember).toHaveBeenCalledWith(groupId, postData);
});
});
it('calls Api inviteGroupMember with the correct params', () => {
expect(Api.inviteGroupMember).toHaveBeenCalledWith(groupId, postData);
describe('when sending the invite for a single member returned an api error', () => {
const apiErrorMessage = 'Members already exists';
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: '123' });
wrapper.vm.$toast = { show: jest.fn() };
jest
.spyOn(Api, 'inviteGroupMember')
.mockRejectedValue({ response: { data: { message: apiErrorMessage } } });
findInviteButton().vm.$emit('click');
});
it('displays the api error message for the toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
apiErrorMessage,
wrapper.vm.toastOptions,
);
});
});
describe('when the invite was sent successfully', () => {
const toastMessageSuccessful = 'Users were succesfully added';
describe('when sending the invite for multiple members returned any error', () => {
const genericErrorMessage = 'Some of the members could not be added';
it('displays the successful toastMessage', () => {
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: '123' });
wrapper.vm.$toast = { show: jest.fn() };
jest
.spyOn(Api, 'inviteGroupMember')
.mockRejectedValue({ response: { data: { success: false } } });
findInviteButton().vm.$emit('click');
});
it('displays the expected toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
toastMessageSuccessful,
genericErrorMessage,
wrapper.vm.toastOptions,
);
});
......
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlTokenSelector } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
const label = 'testgroup';
const placeholder = 'Search for a member';
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'two_2', avatar_url: '' };
const allUsers = [user1, user2];
const createComponent = () => {
return shallowMount(MembersTokenSelect, {
propsData: {
ariaLabelledby: label,
placeholder,
},
});
};
describe('MembersTokenSelect', () => {
let wrapper;
beforeEach(() => {
jest.spyOn(Api, 'users').mockResolvedValue({ data: allUsers });
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTokenSelector = () => wrapper.find(GlTokenSelector);
describe('rendering the token-selector component', () => {
it('renders with the correct props', () => {
const expectedProps = {
ariaLabelledby: label,
placeholder,
};
expect(findTokenSelector().props()).toEqual(expect.objectContaining(expectedProps));
});
});
describe('users', () => {
describe('when input is focused for the first time (modal auto-focus)', () => {
it('does not call the API', async () => {
findTokenSelector().vm.$emit('focus');
await waitForPromises();
expect(Api.users).not.toHaveBeenCalled();
});
});
describe('when input is manually focused', () => {
it('calls the API and sets dropdown items as request result', async () => {
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('focus');
tokenSelector.vm.$emit('blur');
tokenSelector.vm.$emit('focus');
await waitForPromises();
expect(tokenSelector.props('dropdownItems')).toMatchObject(allUsers);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
describe('when text input is typed in', () => {
it('calls the API with search parameter', async () => {
const searchParam = 'One';
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('text-input', searchParam);
await waitForPromises();
expect(Api.users).toHaveBeenCalledWith(searchParam, wrapper.vm.$options.queryOptions);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
describe('when user is selected', () => {
it('emits `input` event with selected users', () => {
findTokenSelector().vm.$emit('input', [
{ id: 1, name: 'John Smith' },
{ id: 2, name: 'Jane Doe' },
]);
expect(wrapper.emitted().input[0][0]).toBe('1,2');
});
});
});
describe('when text input is blurred', () => {
it('clears text input', async () => {
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('blur');
await nextTick();
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
});
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