Commit 6fd529f6 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '354003-resolve-ce-ee-duplication-in-invite-modal-base' into 'master'

Part 2 - Resolve EE/CE duplication in invite_modal_base

See merge request gitlab-org/gitlab!82138
parents 47dd2466 a628a289
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
GlFormInput, GlFormInput,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import { import {
ACCESS_LEVEL, ACCESS_LEVEL,
ACCESS_EXPIRE_DATE, ACCESS_EXPIRE_DATE,
...@@ -20,6 +21,17 @@ import { ...@@ -20,6 +21,17 @@ import {
HEADER_CLOSE_LABEL, HEADER_CLOSE_LABEL,
} from '../constants'; } from '../constants';
const DEFAULT_SLOT = 'default';
const DEFAULT_SLOTS = [
{
key: DEFAULT_SLOT,
attributes: {
class: 'invite-modal-content',
'data-testid': 'invite-modal-initial-content',
},
},
];
export default { export default {
components: { components: {
GlFormGroup, GlFormGroup,
...@@ -31,6 +43,7 @@ export default { ...@@ -31,6 +43,7 @@ export default {
GlSprintf, GlSprintf,
GlButton, GlButton,
GlFormInput, GlFormInput,
ContentTransition,
}, },
inheritAttrs: false, inheritAttrs: false,
props: { props: {
...@@ -86,6 +99,21 @@ export default { ...@@ -86,6 +99,21 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
submitButtonText: {
type: String,
required: false,
default: INVITE_BUTTON_TEXT,
},
currentSlot: {
type: String,
required: false,
default: DEFAULT_SLOT,
},
extraSlots: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
// Be sure to check out reset! // Be sure to check out reset!
...@@ -110,6 +138,9 @@ export default { ...@@ -110,6 +138,9 @@ export default {
(key) => this.accessLevels[key] === Number(this.selectedAccessLevel), (key) => this.accessLevels[key] === Number(this.selectedAccessLevel),
); );
}, },
contentSlots() {
return [...DEFAULT_SLOTS, ...(this.extraSlots || [])];
},
}, },
watch: { watch: {
selectedAccessLevel: { selectedAccessLevel: {
...@@ -148,6 +179,7 @@ export default { ...@@ -148,6 +179,7 @@ export default {
READ_MORE_TEXT, READ_MORE_TEXT,
INVITE_BUTTON_TEXT, INVITE_BUTTON_TEXT,
CANCEL_BUTTON_TEXT, CANCEL_BUTTON_TEXT,
DEFAULT_SLOT,
}; };
</script> </script>
...@@ -164,6 +196,13 @@ export default { ...@@ -164,6 +196,13 @@ export default {
@close="reset" @close="reset"
@hide="reset" @hide="reset"
> >
<content-transition
class="gl-display-grid"
transition-name="invite-modal-transition"
:slots="contentSlots"
:current-slot="currentSlot"
>
<template #[$options.DEFAULT_SLOT]>
<div class="gl-display-flex" data-testid="modal-base-intro-text"> <div class="gl-display-flex" data-testid="modal-base-intro-text">
<slot name="intro-text-before"></slot> <slot name="intro-text-before"></slot>
<p> <p>
...@@ -227,16 +266,26 @@ export default { ...@@ -227,16 +266,26 @@ export default {
:target="null" :target="null"
> >
<template #default="{ formattedDate }"> <template #default="{ formattedDate }">
<gl-form-input class="gl-w-full" :value="formattedDate" :placeholder="__(`YYYY-MM-DD`)" /> <gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</template> </template>
</gl-datepicker> </gl-datepicker>
</div> </div>
<slot name="form-after"></slot> <slot name="form-after"></slot>
</template>
<template v-for="{ key } in extraSlots" #[key]>
<slot :name="key"></slot>
</template>
</content-transition>
<template #modal-footer> <template #modal-footer>
<slot name="cancel-button">
<gl-button data-testid="cancel-button" @click="closeModal"> <gl-button data-testid="cancel-button" @click="closeModal">
{{ $options.CANCEL_BUTTON_TEXT }} {{ $options.CANCEL_BUTTON_TEXT }}
</gl-button> </gl-button>
</slot>
<gl-button <gl-button
:disabled="submitDisabled" :disabled="submitDisabled"
:loading="isLoading" :loading="isLoading"
...@@ -245,7 +294,7 @@ export default { ...@@ -245,7 +294,7 @@ export default {
data-testid="invite-button" data-testid="invite-button"
@click="submit" @click="submit"
> >
{{ $options.INVITE_BUTTON_TEXT }} {{ submitButtonText }}
</gl-button> </gl-button>
</template> </template>
</gl-modal> </gl-modal>
......
<script>
export default {
props: {
currentSlot: {
type: String,
required: true,
},
slots: {
type: Array,
required: true,
},
transitionName: {
type: String,
required: true,
},
},
methods: {
shouldShow(key) {
return this.currentSlot === key;
},
},
};
</script>
<template>
<div>
<transition v-for="{ key, attributes } in slots" :key="key" :name="transitionName">
<div v-show="shouldShow(key)" v-bind="attributes">
<slot :name="key"></slot>
</div>
</transition>
</div>
</template>
<script> <script>
import { import { GlLink, GlButton } from '@gitlab/ui';
GlFormGroup,
GlModal,
GlDropdown,
GlDropdownItem,
GlDatepicker,
GlLink,
GlSprintf,
GlButton,
GlFormInput,
} from '@gitlab/ui';
import { sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
ACCESS_LEVEL,
ACCESS_EXPIRE_DATE,
READ_MORE_TEXT,
INVITE_BUTTON_TEXT,
CANCEL_BUTTON_TEXT,
HEADER_CLOSE_LABEL,
} from '~/invite_members/constants';
import { import {
OVERAGE_MODAL_LINK, OVERAGE_MODAL_LINK,
OVERAGE_MODAL_TITLE, OVERAGE_MODAL_TITLE,
...@@ -30,73 +12,34 @@ import { ...@@ -30,73 +12,34 @@ import {
overageModalInfoWarning, overageModalInfoWarning,
} from '../constants'; } from '../constants';
const OVERAGE_CONTENT_SLOT = 'overage-content';
const EXTRA_SLOTS = [
{
key: OVERAGE_CONTENT_SLOT,
attributes: {
class: 'invite-modal-content',
'data-testid': 'invite-modal-overage-content',
},
},
];
export default { export default {
components: { components: {
GlFormGroup,
GlDatepicker,
GlLink, GlLink,
GlModal,
GlDropdown,
GlDropdownItem,
GlSprintf,
GlButton, GlButton,
GlFormInput, InviteModalBase,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
inheritAttrs: false, inheritAttrs: false,
props: { props: {
modalTitle: {
type: String,
required: true,
},
modalId: {
type: String,
required: true,
},
name: { name: {
type: String, type: String,
required: true, required: true,
}, },
accessLevels: { modalTitle: {
type: Object,
required: true,
},
defaultAccessLevel: {
type: Number,
required: true,
},
helpLink: {
type: String,
required: true,
},
labelIntroText: {
type: String,
required: true,
},
labelSearchField: {
type: String, type: String,
required: true, required: true,
}, },
formGroupDescription: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
invalidFeedbackMessage: {
type: String,
required: false,
default: '',
},
subscriptionSeats: { subscriptionSeats: {
type: Number, type: Number,
required: false, required: false,
...@@ -104,29 +47,19 @@ export default { ...@@ -104,29 +47,19 @@ export default {
}, },
}, },
data() { data() {
// Be sure to check out reset!
return { return {
selectedAccessLevel: this.defaultAccessLevel,
selectedDate: undefined,
minDate: new Date(),
hasOverage: false, hasOverage: false,
totalUserCount: null, totalUserCount: null,
}; };
}, },
computed: { computed: {
introText() { currentSlot() {
return sprintf(this.labelIntroText, { name: this.name }); if (this.showOverageModal) {
}, return OVERAGE_CONTENT_SLOT;
validationState() { }
return this.invalidFeedbackMessage ? false : null;
}, // Use CE default
selectLabelId() { return undefined;
return `${this.modalId}_select`;
},
selectedRoleName() {
return Object.keys(this.accessLevels).find(
(key) => this.accessLevels[key] === Number(this.selectedAccessLevel),
);
}, },
showOverageModal() { showOverageModal() {
return this.hasOverage && this.enabledOverageCheck; return this.hasOverage && this.enabledOverageCheck;
...@@ -136,220 +69,91 @@ export default { ...@@ -136,220 +69,91 @@ export default {
}, },
modalInfo() { modalInfo() {
if (this.totalUserCount) { if (this.totalUserCount) {
const infoText = this.$options.i18n.infoText(this.subscriptionSeats); const infoText = overageModalInfoText(this.subscriptionSeats);
const infoWarning = this.$options.i18n.infoWarning(this.totalUserCount, this.name); const infoWarning = overageModalInfoWarning(this.totalUserCount, this.name);
return `${infoText} ${infoWarning}`; return `${infoText} ${infoWarning}`;
} }
return ''; return '';
}, },
modalTitleLabel() { modalTitleOverride() {
return this.showOverageModal ? this.$options.i18n.OVERAGE_MODAL_TITLE : this.modalTitle; return this.showOverageModal ? OVERAGE_MODAL_TITLE : this.modalTitle;
},
},
watch: {
selectedAccessLevel: {
immediate: true,
handler(val) {
this.$emit('access-level', val);
}, },
submitButtonText() {
if (this.showOverageModal) {
return OVERAGE_MODAL_CONTINUE_BUTTON;
}
// Use CE default
return undefined;
}, },
}, },
methods: { methods: {
reset() { getPassthroughListeners() {
// This component isn't necessarily disposed, // This gets the listeners we don't manually handle here
// so we might need to reset it's state. // so we can pass them through to the CE invite_modal_base.vue
this.selectedAccessLevel = this.defaultAccessLevel; const { reset, submit, ...listeners } = this.$listeners;
this.selectedDate = undefined;
return listeners;
},
onReset(...args) {
// don't reopen the overage modal // don't reopen the overage modal
this.hasOverage = false; this.hasOverage = false;
this.$emit('reset'); this.$emit('reset', ...args);
},
closeModal() {
this.reset();
this.$refs.modal.hide();
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
}, },
submit() { onSubmit(...args) {
this.$emit('submit', { if (this.enabledOverageCheck && !this.hasOverage) {
accessLevel: this.selectedAccessLevel,
expiresAt: this.selectedDate,
});
},
checkOverage() {
// add a more complex check in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78287
// totalUserCount should be calculated there
if (this.enabledOverageCheck) {
this.totalUserCount = 1; this.totalUserCount = 1;
this.hasOverage = true; this.hasOverage = true;
} else { } else {
this.submit(); this.$emit('submit', ...args);
} }
}, },
handleBack() { handleBack() {
this.hasOverage = false; this.hasOverage = false;
}, },
passthroughSlotNames() {
return Object.keys(this.$scopedSlots || {});
},
}, },
i18n: { i18n: {
HEADER_CLOSE_LABEL,
ACCESS_EXPIRE_DATE,
ACCESS_LEVEL,
READ_MORE_TEXT,
INVITE_BUTTON_TEXT,
CANCEL_BUTTON_TEXT,
OVERAGE_MODAL_TITLE, OVERAGE_MODAL_TITLE,
OVERAGE_MODAL_LINK, OVERAGE_MODAL_LINK,
OVERAGE_MODAL_BACK_BUTTON, OVERAGE_MODAL_BACK_BUTTON,
OVERAGE_MODAL_CONTINUE_BUTTON, OVERAGE_MODAL_CONTINUE_BUTTON,
OVERAGE_MODAL_LINK_TEXT, OVERAGE_MODAL_LINK_TEXT,
infoText: overageModalInfoText,
infoWarning: overageModalInfoWarning,
}, },
OVERAGE_CONTENT_SLOT,
EXTRA_SLOTS,
}; };
</script> </script>
<template> <template>
<gl-modal <invite-modal-base
ref="modal"
:modal-id="modalId"
data-qa-selector="invite_members_modal_content"
data-testid="invite-modal"
size="sm"
:title="modalTitleLabel"
:header-close-label="$options.i18n.HEADER_CLOSE_LABEL"
@hidden="reset"
@close="reset"
@hide="reset"
>
<div class="gl-display-grid">
<transition name="invite-modal-transition">
<div
v-show="!showOverageModal"
class="invite-modal-content"
data-testid="invite-modal-initial-content"
>
<div class="gl-display-flex" data-testid="modal-base-intro-text">
<slot name="intro-text-before"></slot>
<p>
<gl-sprintf :message="introText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<slot name="intro-text-after"></slot>
</div>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
:description="formGroupDescription"
data-testid="members-form-group"
>
<label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
<slot name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
</gl-form-group>
<label class="gl-font-weight-bold">{{ $options.i18n.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs" v-bind="$attrs"
:text="selectedRoleName" :name="name"
> :submit-button-text="submitButtonText"
<template v-for="(key, item) in accessLevels"> :modal-title="modalTitleOverride"
<gl-dropdown-item :current-slot="currentSlot"
:key="key" :extra-slots="$options.EXTRA_SLOTS"
active-class="is-active" @reset="onReset"
is-check-item @submit="onSubmit"
:is-checked="key === selectedAccessLevel" v-on="getPassthroughListeners()"
@click="changeSelectedItem(key)"
>
<div>{{ item }}</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.i18n.READ_MORE_TEXT">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
<label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.i18n.ACCESS_EXPIRE_DATE
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker
v-model="selectedDate"
class="gl-display-inline!"
:min-date="minDate"
:target="null"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</template>
</gl-datepicker>
</div>
<slot name="form-after"></slot>
</div>
</transition>
<transition name="invite-modal-transition">
<div
v-show="showOverageModal"
class="invite-modal-content"
data-testid="invite-modal-overage-content"
> >
<template #[$options.OVERAGE_CONTENT_SLOT]>
{{ modalInfo }} {{ modalInfo }}
<gl-link :href="$options.i18n.OVERAGE_MODAL_LINK" target="_blank">{{ <gl-link :href="$options.i18n.OVERAGE_MODAL_LINK" target="_blank">{{
$options.i18n.OVERAGE_MODAL_LINK_TEXT $options.i18n.OVERAGE_MODAL_LINK_TEXT
}}</gl-link> }}</gl-link>
</div>
</transition>
</div>
<template #modal-footer>
<template v-if="!showOverageModal">
<gl-button data-testid="cancel-button" @click="closeModal">
{{ $options.i18n.CANCEL_BUTTON_TEXT }}
</gl-button>
<gl-button
:disabled="submitDisabled"
:loading="isLoading"
variant="confirm"
data-qa-selector="invite_button"
data-testid="invite-button"
@click="checkOverage"
>
{{ $options.i18n.INVITE_BUTTON_TEXT }}
</gl-button>
</template> </template>
<template v-else> <template v-if="enabledOverageCheck && hasOverage" #cancel-button>
<gl-button data-testid="overage-back-button" @click="handleBack"> <gl-button data-testid="overage-back-button" @click="handleBack">
{{ $options.i18n.OVERAGE_MODAL_BACK_BUTTON }} {{ $options.i18n.OVERAGE_MODAL_BACK_BUTTON }}
</gl-button> </gl-button>
<gl-button
:disabled="submitDisabled"
:loading="isLoading"
variant="confirm"
data-qa-selector="invite_with_overage_button"
data-testid="invite-with-overage-button"
@click="submit"
>
{{ $options.i18n.OVERAGE_MODAL_CONTINUE_BUTTON }}
</gl-button>
</template> </template>
<template v-for="(_, slot) of $scopedSlots" #[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template> </template>
</gl-modal> </invite-modal-base>
</template> </template>
import { import { GlModal, GlSprintf } from '@gitlab/ui';
GlDropdown, import { nextTick } from 'vue';
GlDropdownItem,
GlDatepicker,
GlFormGroup,
GlSprintf,
GlLink,
GlModal,
} from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue'; import ContentTransition from '~/vue_shared/components/content_transition.vue';
import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants'; import CEInviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import EEInviteModalBase from 'ee/invite_members/components/invite_modal_base.vue';
import { import {
OVERAGE_MODAL_TITLE, OVERAGE_MODAL_TITLE,
OVERAGE_MODAL_CONTINUE_BUTTON, OVERAGE_MODAL_CONTINUE_BUTTON,
...@@ -18,11 +12,12 @@ import { ...@@ -18,11 +12,12 @@ import {
} from 'ee/invite_members/constants'; } from 'ee/invite_members/constants';
import { propsData } from 'jest/invite_members/mock_data/modal_base'; import { propsData } from 'jest/invite_members/mock_data/modal_base';
describe('InviteModalBase', () => { describe('EEInviteModalBase', () => {
let wrapper; let wrapper;
let listenerSpy;
const createComponent = (props = {}, glFeatures = {}) => { const createComponent = (props = {}, glFeatures = {}) => {
wrapper = shallowMountExtended(InviteModalBase, { wrapper = shallowMountExtended(EEInviteModalBase, {
propsData: { propsData: {
...propsData, ...propsData,
...props, ...props,
...@@ -31,125 +26,103 @@ describe('InviteModalBase', () => { ...@@ -31,125 +26,103 @@ describe('InviteModalBase', () => {
...glFeatures, ...glFeatures,
}, },
stubs: { stubs: {
GlSprintf,
InviteModalBase: CEInviteModalBase,
ContentTransition,
GlModal: stubComponent(GlModal, { GlModal: stubComponent(GlModal, {
template: template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}), }),
GlDropdown: true, },
GlDropdownItem: true, listeners: {
GlSprintf, submit: (...args) => listenerSpy('submit', ...args),
GlFormGroup: stubComponent(GlFormGroup, { reset: (...args) => listenerSpy('reset', ...args),
props: ['state', 'invalidFeedback', 'description'], foo: (...args) => listenerSpy('foo', ...args),
}),
}, },
}); });
}; };
beforeEach(() => {
listenerSpy = jest.fn();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
const findDropdown = () => wrapper.findComponent(GlDropdown); const findCEBase = () => wrapper.findComponent(CEInviteModalBase);
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findInviteButton = () => wrapper.findByTestId('invite-button'); const findInviteButton = () => wrapper.findByTestId('invite-button');
const findBackButton = () => wrapper.findByTestId('overage-back-button'); const findBackButton = () => wrapper.findByTestId('overage-back-button');
const findOverageInviteButton = () => wrapper.findByTestId('invite-with-overage-button');
const findInitialModalContent = () => wrapper.findByTestId('invite-modal-initial-content'); const findInitialModalContent = () => wrapper.findByTestId('invite-modal-initial-content');
const findOverageModalContent = () => wrapper.findByTestId('invite-modal-overage-content'); const findOverageModalContent = () => wrapper.findByTestId('invite-modal-overage-content');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const findModalTitle = () => wrapper.findComponent(GlModal).props('title');
const clickInviteButton = () => findInviteButton().vm.$emit('click'); const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickBackButton = () => findBackButton().vm.$emit('click'); const clickBackButton = () => findBackButton().vm.$emit('click');
describe('rendering the modal', () => { describe('default', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
it('renders the modal with the correct title', () => { it('passes attrs to CE base', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe(propsData.modalTitle); expect(findCEBase().props()).toMatchObject({
}); ...propsData,
currentSlot: 'default',
it('displays the introText', () => { extraSlots: EEInviteModalBase.EXTRA_SLOTS,
expect(findIntroText()).toBe(propsData.labelIntroText);
});
it('renders the Cancel button text correctly', () => {
expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
}); });
it('renders the Invite button text correctly', () => {
expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
}); });
it('renders the Invite button modal without isLoading', () => { it("doesn't show the overage content", () => {
expect(findInviteButton().props('loading')).toBe(false); expect(findOverageModalContent().isVisible()).toBe(false);
});
describe('rendering the access levels dropdown', () => {
it('sets the default dropdown text to the default access level name', () => {
expect(findDropdown().attributes('text')).toBe('Guest');
}); });
it('renders dropdown items for each accessLevel', () => { it('when reset is emitted on base, emits reset', () => {
expect(findDropdownItems()).toHaveLength(5); expect(wrapper.emitted('reset')).toBeUndefined();
});
});
it('renders the correct link', () => { findCEBase().vm.$emit('reset');
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
it('renders the datepicker', () => { expect(wrapper.emitted('reset')).toHaveLength(1);
expect(findDatepicker().exists()).toBe(true);
}); });
it("doesn't show the overage content", () => { describe('(integration) when invite is clicked', () => {
expect(findOverageModalContent().isVisible()).toBe(false); beforeEach(async () => {
clickInviteButton();
await nextTick();
}); });
it('renders the members form group', () => { it('does not change title', () => {
expect(findMembersFormGroup().props()).toEqual({ expect(findModalTitle()).toBe(propsData.modalTitle);
description: propsData.formGroupDescription,
invalidFeedback: '',
state: null,
});
});
}); });
it('with isLoading, shows loading for invite button', () => { it('does not show back button', () => {
createComponent({ expect(findBackButton().exists()).toBe(false);
isLoading: true,
}); });
expect(findInviteButton().props('loading')).toBe(true); it('shows initial modal content', () => {
expect(findInitialModalContent().isVisible()).toBe(true);
}); });
it('with invalidFeedbackMessage, set members form group validation state', () => { it('emits submit', () => {
createComponent({ expect(wrapper.emitted('submit')).toEqual([[{ accessLevel: 10, expiresAt: undefined }]]);
invalidFeedbackMessage: 'invalid message!',
}); });
expect(findMembersFormGroup().props()).toEqual({
description: propsData.formGroupDescription,
invalidFeedback: 'invalid message!',
state: false,
}); });
}); });
describe('displays overage modal', () => { describe('with overageMembersModal feature flag, and invite is clicked ', () => {
beforeEach(() => { beforeEach(async () => {
createComponent({}, { glFeatures: { overageMembersModal: true } }); createComponent({}, { glFeatures: { overageMembersModal: true } });
clickInviteButton(); clickInviteButton();
await nextTick();
});
it('does not emit submit', () => {
expect(wrapper.emitted().submit).toBeUndefined();
}); });
it('renders the modal with the correct title', () => { it('renders the modal with the correct title', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe(OVERAGE_MODAL_TITLE); expect(findModalTitle()).toBe(OVERAGE_MODAL_TITLE);
}); });
it('renders the Back button text correctly', () => { it('renders the Back button text correctly', () => {
...@@ -157,7 +130,7 @@ describe('InviteModalBase', () => { ...@@ -157,7 +130,7 @@ describe('InviteModalBase', () => {
}); });
it('renders the Continue button text correctly', () => { it('renders the Continue button text correctly', () => {
expect(findOverageInviteButton().text()).toBe(OVERAGE_MODAL_CONTINUE_BUTTON); expect(findInviteButton().text()).toBe(OVERAGE_MODAL_CONTINUE_BUTTON);
}); });
it('shows the info text', () => { it('shows the info text', () => {
...@@ -174,7 +147,7 @@ describe('InviteModalBase', () => { ...@@ -174,7 +147,7 @@ describe('InviteModalBase', () => {
beforeEach(() => clickBackButton()); beforeEach(() => clickBackButton());
it('shows the initial modal', () => { it('shows the initial modal', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe('_modal_title_'); expect(wrapper.findComponent(GlModal).props('title')).toBe(propsData.modalTitle);
expect(findInitialModalContent().isVisible()).toBe(true); expect(findInitialModalContent().isVisible()).toBe(true);
}); });
......
...@@ -4,6 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; ...@@ -4,6 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Api from '~/api'; import Api from '~/api';
import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.vue'; import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.vue';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue'; import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import GroupSelect from '~/invite_members/components/group_select.vue'; import GroupSelect from '~/invite_members/components/group_select.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { propsData, sharedGroup } from '../mock_data/group_modal'; import { propsData, sharedGroup } from '../mock_data/group_modal';
...@@ -19,6 +20,7 @@ describe('InviteGroupsModal', () => { ...@@ -19,6 +20,7 @@ describe('InviteGroupsModal', () => {
}, },
stubs: { stubs: {
InviteModalBase, InviteModalBase,
ContentTransition,
GlSprintf, GlSprintf,
GlModal: stubComponent(GlModal, { GlModal: stubComponent(GlModal, {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>', template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
LEARN_GITLAB, LEARN_GITLAB,
} from '~/invite_members/constants'; } from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub'; import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
...@@ -55,6 +56,7 @@ describe('InviteMembersModal', () => { ...@@ -55,6 +56,7 @@ describe('InviteMembersModal', () => {
}, },
stubs: { stubs: {
InviteModalBase, InviteModalBase,
ContentTransition,
GlSprintf, GlSprintf,
GlModal: stubComponent(GlModal, { GlModal: stubComponent(GlModal, {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>', template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue'; import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants'; import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants';
import { propsData } from '../mock_data/modal_base'; import { propsData } from '../mock_data/modal_base';
...@@ -23,6 +24,7 @@ describe('InviteModalBase', () => { ...@@ -23,6 +24,7 @@ describe('InviteModalBase', () => {
...props, ...props,
}, },
stubs: { stubs: {
ContentTransition,
GlModal: stubComponent(GlModal, { GlModal: stubComponent(GlModal, {
template: template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_shared/components/content_transition.vue default shows all transitions and only default is visible 1`] = `
<div>
<transition-stub
name="test_transition_name"
>
<div
data-testval="default"
>
<p>
Default
</p>
</div>
</transition-stub>
<transition-stub
name="test_transition_name"
>
<div
data-testval="foo"
style="display: none;"
>
<p>
Foo
</p>
</div>
</transition-stub>
<transition-stub
name="test_transition_name"
>
<div
data-testval="bar"
style="display: none;"
>
<p>
Bar
</p>
</div>
</transition-stub>
</div>
`;
import { groupBy, mapValues } from 'lodash';
import { shallowMount } from '@vue/test-utils';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
const TEST_CURRENT_SLOT = 'default';
const TEST_TRANSITION_NAME = 'test_transition_name';
const TEST_SLOTS = [
{ key: 'default', attributes: { 'data-testval': 'default' } },
{ key: 'foo', attributes: { 'data-testval': 'foo' } },
{ key: 'bar', attributes: { 'data-testval': 'bar' } },
];
describe('~/vue_shared/components/content_transition.vue', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(ContentTransition, {
propsData: {
transitionName: TEST_TRANSITION_NAME,
currentSlot: TEST_CURRENT_SLOT,
slots: TEST_SLOTS,
...props,
},
slots: {
default: '<p>Default</p>',
foo: '<p>Foo</p>',
bar: '<p>Bar</p>',
dne: '<p>DOES NOT EXIST</p>',
...slots,
},
});
};
const findTransitionsData = () =>
wrapper.findAll('transition-stub').wrappers.map((transition) => {
const child = transition.find('[data-testval]');
const { style, ...attributes } = child.attributes();
return {
transitionName: transition.attributes('name'),
isVisible: child.isVisible(),
attributes,
text: transition.text(),
};
});
const findVisibleData = () => {
const group = groupBy(findTransitionsData(), (x) => x.attributes['data-testval']);
return mapValues(group, (x) => x[0].isVisible);
};
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('shows all transitions and only default is visible', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('render transitions for each slot', () => {
expect(findTransitionsData()).toEqual([
{
attributes: {
'data-testval': 'default',
},
isVisible: true,
text: 'Default',
transitionName: 'test_transition_name',
},
{
attributes: {
'data-testval': 'foo',
},
isVisible: false,
text: 'Foo',
transitionName: 'test_transition_name',
},
{
attributes: {
'data-testval': 'bar',
},
isVisible: false,
text: 'Bar',
transitionName: 'test_transition_name',
},
]);
});
});
describe('with currentSlot=foo', () => {
beforeEach(() => {
createComponent({ currentSlot: 'foo' });
});
it('should only show the foo slot', () => {
expect(findVisibleData()).toEqual({
default: false,
foo: true,
bar: 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