Commit eaf85b81 authored by Jackie Fraser's avatar Jackie Fraser Committed by Nicolò Maria Mezzopera

Experiment: Add modal with celebration to the end of user signup

parent 8da8bc98
<script>
import confetti from 'canvas-confetti';
export default {
mounted() {
confetti.create(this.$refs.canvas, {
resize: true,
useWorker: true,
disableForReducedMotion: true,
});
this.basicCannon();
},
methods: {
basicCannon() {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.2 },
scalar: 2,
shapes: ['square'],
colors: ['#FC6D26', '#6B4FBB', '#FDB997'],
zIndex: 1045,
gravity: 1.5,
});
},
},
};
</script>
<template>
<canvas ref="canvas" width="0" height="0"></canvas>
</template>
...@@ -18,19 +18,21 @@ import ExperimentTracking from '~/experimentation/experiment_tracking'; ...@@ -18,19 +18,21 @@ import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale'; import { sprintf } from '~/locale';
import { import {
INVITE_MEMBERS_IN_COMMENT, INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS, GROUP_FILTERS,
USERS_FILTER_ALL, USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS, MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK, INVITE_MEMBERS_FOR_TASK,
MODAL_LABELS,
} from '../constants'; } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { import {
responseMessageFromError, responseMessageFromError,
responseMessageFromSuccess, responseMessageFromSuccess,
} from '../utils/response_message_parser'; } from '../utils/response_message_parser';
import ModalConfetti from './confetti.vue';
import GroupSelect from './group_select.vue'; import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue'; import MembersTokenSelect from './members_token_select.vue';
...@@ -50,6 +52,7 @@ export default { ...@@ -50,6 +52,7 @@ export default {
GlFormCheckboxGroup, GlFormCheckboxGroup,
MembersTokenSelect, MembersTokenSelect,
GroupSelect, GroupSelect,
ModalConfetti,
}, },
inject: ['newProjectPath'], inject: ['newProjectPath'],
props: { props: {
...@@ -129,22 +132,30 @@ export default { ...@@ -129,22 +132,30 @@ export default {
source: 'unknown', source: 'unknown',
invalidFeedbackMessage: '', invalidFeedbackMessage: '',
isLoading: false, isLoading: false,
mode: 'default',
}; };
}, },
computed: { computed: {
isCelebration() {
return this.mode === 'celebrate';
},
validationState() { validationState() {
return this.invalidFeedbackMessage === '' ? null : false; return this.invalidFeedbackMessage === '' ? null : false;
}, },
isInviteGroup() { isInviteGroup() {
return this.inviteeType === 'group'; return this.inviteeType === 'group';
}, },
modalTitle() {
return this.$options.labels[this.inviteeType].modal[this.mode].title;
},
introText() { introText() {
const inviteTo = this.isProject ? 'toProject' : 'toGroup'; return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, {
return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
name: this.name, name: this.name,
}); });
}, },
inviteTo() {
return this.isProject ? 'toProject' : 'toGroup';
},
toastOptions() { toastOptions() {
return { return {
onComplete: () => { onComplete: () => {
...@@ -234,7 +245,8 @@ export default { ...@@ -234,7 +245,8 @@ export default {
usersToAddById.map((user) => user.id).join(','), usersToAddById.map((user) => user.id).join(','),
]; ];
}, },
openModal({ inviteeType, source }) { openModal({ mode = 'default', inviteeType, source }) {
this.mode = mode;
this.inviteeType = inviteeType; this.inviteeType = inviteeType;
this.source = source; this.source = source;
...@@ -381,60 +393,7 @@ export default { ...@@ -381,60 +393,7 @@ export default {
return unescape(sanitize(message, { ALLOWED_TAGS: [] })); return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
}, },
}, },
labels: { labels: MODAL_LABELS,
members: {
modalTitle: s__('InviteMembersModal|Invite members'),
searchField: s__('InviteMembersModal|GitLab member or email address'),
placeHolder: s__('InviteMembersModal|Select members or type email addresses'),
toGroup: {
introText: s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
),
},
toProject: {
introText: s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
),
},
tasksToBeDone: {
title: s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
),
noProjects: s__(
'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
),
},
tasksProject: {
title: s__('InviteMembersModal|Choose a project for the issues'),
},
},
group: {
modalTitle: s__('InviteMembersModal|Invite a group'),
searchField: s__('InviteMembersModal|Select a group to invite'),
placeHolder: s__('InviteMembersModal|Search for a group to invite'),
toGroup: {
introText: s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
),
},
toProject: {
introText: s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
),
},
},
accessLevel: s__('InviteMembersModal|Select a role'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
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'),
areasOfFocusLabel: s__(
'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
),
},
membersTokenSelectLabelId: 'invite-members-input', membersTokenSelectLabelId: 'invite-members-input',
}; };
</script> </script>
...@@ -445,20 +404,28 @@ export default { ...@@ -445,20 +404,28 @@ export default {
size="sm" size="sm"
data-qa-selector="invite_members_modal_content" data-qa-selector="invite_members_modal_content"
data-testid="invite-members-modal" data-testid="invite-members-modal"
:title="$options.labels[inviteeType].modalTitle" :title="modalTitle"
:header-close-label="$options.labels.headerCloseLabel" :header-close-label="$options.labels.headerCloseLabel"
@hidden="resetFields" @hidden="resetFields"
@close="resetFields" @close="resetFields"
@hide="resetFields" @hide="resetFields"
> >
<div> <div>
<p ref="introText"> <div class="gl-display-flex">
<gl-sprintf :message="introText"> <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div>
<template #strong="{ content }"> <div>
<strong>{{ content }}</strong> <p ref="introText">
</template> <gl-sprintf :message="introText">
</gl-sprintf> <template #strong="{ content }">
</p> <strong>{{ content }}</strong>
</template>
</gl-sprintf>
<br />
<span v-if="isCelebration">{{ $options.labels.members.modal.celebrate.intro }} </span>
<modal-confetti v-if="isCelebration" />
</p>
</div>
</div>
<gl-form-group <gl-form-group
:invalid-feedback="invalidFeedbackMessage" :invalid-feedback="invalidFeedbackMessage"
......
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200; export const SEARCH_DELAY = 200;
...@@ -27,3 +27,120 @@ export const USERS_FILTER_ALL = 'all'; ...@@ -27,3 +27,120 @@ export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id'; export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button'; export const TRIGGER_ELEMENT_BUTTON = 'button';
export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav'; export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
export const MEMBERS_MODAL_CELEBRATE_TITLE = s__(
'InviteMembersModal|GitLab is better with colleagues!',
);
export const MEMBERS_MODAL_CELEBRATE_INTRO = s__(
'InviteMembersModal|How about inviting a colleague or two to join you?',
);
export const MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
);
export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
);
export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
"InviteMembersModal|Congratulations on creating your project, you're almost there!",
);
export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
);
export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
);
export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
'InviteMembersModal|Choose a project for the issues',
);
export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
);
export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
export const ACCESS_LEVEL = s__('InviteMembersModal|Select a role');
export const ACCESS_EXPIRE_DATE = s__('InviteMembersModal|Access expiration date (optional)');
export const TOAST_MESSAGE_SUCCESSFUL = s__('InviteMembersModal|Members were successfully added');
export const INVALID_FEEDBACK_MESSAGE_DEFAULT = s__('InviteMembersModal|Something went wrong');
export const READ_MORE_TEXT = s__(
`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
);
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
export const AREAS_OF_FOCUS_LABEL = s__(
'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
);
export const MODAL_LABELS = {
members: {
modal: {
default: {
title: MEMBERS_MODAL_DEFAULT_TITLE,
},
celebrate: {
title: MEMBERS_MODAL_CELEBRATE_TITLE,
intro: MEMBERS_MODAL_CELEBRATE_INTRO,
},
},
toGroup: {
default: {
introText: MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT,
},
},
toProject: {
default: {
introText: MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT,
},
celebrate: {
introText: MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
},
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
tasksToBeDone: {
title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
},
tasksProject: {
title: MEMBERS_TASKS_PROJECTS_TITLE,
},
},
group: {
modal: {
default: {
title: GROUP_MODAL_DEFAULT_TITLE,
},
},
toGroup: {
default: {
introText: GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT,
},
},
toProject: {
default: {
introText: GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT,
},
},
searchField: GROUP_SEARCH_FIELD,
placeHolder: GROUP_PLACEHOLDER,
},
accessLevel: ACCESS_LEVEL,
accessExpireDate: ACCESS_EXPIRE_DATE,
toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
invalidFeedbackMessageDefault: INVALID_FEEDBACK_MESSAGE_DEFAULT,
readMoreText: READ_MORE_TEXT,
inviteButtonText: INVITE_BUTTON_TEXT,
cancelButtonText: CANCEL_BUTTON_TEXT,
headerCloseLabel: HEADER_CLOSE_LABEL,
areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
};
<script> <script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui'; import { GlProgressBar, GlSprintf } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants'; import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue'; import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
...@@ -22,6 +23,11 @@ export default { ...@@ -22,6 +23,11 @@ export default {
required: true, required: true,
type: Object, type: Object,
}, },
inviteMembersOpen: {
type: Boolean,
required: false,
default: false,
},
}, },
maxValue: Object.keys(ACTION_LABELS).length, maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS), actionSections: Object.keys(ACTION_SECTIONS),
...@@ -33,7 +39,15 @@ export default { ...@@ -33,7 +39,15 @@ export default {
return Math.round((this.progressValue / this.$options.maxValue) * 100); return Math.round((this.progressValue / this.$options.maxValue) * 100);
}, },
}, },
mounted() {
if (this.inviteMembersOpen) {
this.openInviteMembersModal('celebrate');
}
},
methods: { methods: {
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
},
actionsFor(section) { actionsFor(section) {
const actions = Object.fromEntries( const actions = Object.fromEntries(
Object.entries(this.actions).filter( Object.entries(this.actions).filter(
......
import Vue from 'vue'; import Vue from 'vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue'; import LearnGitlab from '../components/learn_gitlab.vue';
...@@ -11,15 +12,17 @@ function initLearnGitlab() { ...@@ -11,15 +12,17 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections)); const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const { inviteMembersOpen } = el.dataset;
return new Vue({ return new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(LearnGitlab, { return createElement(LearnGitlab, {
props: { actions, sections }, props: { actions, sections, inviteMembersOpen },
}); });
}, },
}); });
} }
initInviteMembersModal();
initLearnGitlab(); initLearnGitlab();
...@@ -7,6 +7,20 @@ module LearnGitlabHelper ...@@ -7,6 +7,20 @@ module LearnGitlabHelper
learn_gitlab_onboarding_available?(project) learn_gitlab_onboarding_available?(project)
end end
def learn_gitlab_data(project)
{
actions: onboarding_actions_data(project).to_json,
sections: onboarding_sections_data.to_json
}
end
def learn_gitlab_onboarding_available?(project)
OnboardingProgress.onboarding?(project.namespace) &&
LearnGitlab::Project.new(current_user).available?
end
private
def onboarding_actions_data(project) def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys attributes = onboarding_progress(project).attributes.symbolize_keys
...@@ -42,13 +56,6 @@ module LearnGitlabHelper ...@@ -42,13 +56,6 @@ module LearnGitlabHelper
} }
end end
def learn_gitlab_onboarding_available?(project)
OnboardingProgress.onboarding?(project.namespace) &&
LearnGitlab::Project.new(current_user).available?
end
private
def action_urls def action_urls
LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) } LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS) .merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
......
- breadcrumb_title _("Learn GitLab") - breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab") - page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab' - add_page_specific_style 'page_bundles/learn_gitlab'
- data = learn_gitlab_data(@project)
- invite_members_open = session.delete(:confetti_post_signup)
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } } - experiment(:confetti_post_signup, actor: current_user) do |e|
- e.control do
#js-learn-gitlab-app{ data: data }
- e.candidate do
= render 'projects/invite_members_modal', project: @project
#js-learn-gitlab-app{ data: data.merge(invite_members_open: invite_members_open) }
---
name: confetti_post_signup
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70011
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339890
milestone: '14.5'
type: experiment
group: group::expansion
default_enabled: false
...@@ -37,6 +37,8 @@ module EE ...@@ -37,6 +37,8 @@ module EE
project = ::Project.find(params[:project_id]) project = ::Project.find(params[:project_id])
return access_denied! unless can?(current_user, :owner_access, project) return access_denied! unless can?(current_user, :owner_access, project)
session[:confetti_post_signup] = true
render locals: { project: project } render locals: { project: project }
end end
......
...@@ -38,6 +38,12 @@ RSpec.describe Registrations::WelcomeController do ...@@ -38,6 +38,12 @@ RSpec.describe Registrations::WelcomeController do
end end
it { is_expected.to render_template(:continuous_onboarding_getting_started) } it { is_expected.to render_template(:continuous_onboarding_getting_started) }
it 'sets the correct session key' do
continuous_onboarding_getting_started
expect(session[:confetti_post_signup]).to eq(true)
end
end end
context 'with a non-owner user signed in' do context 'with a non-owner user signed in' do
......
...@@ -41,4 +41,46 @@ RSpec.describe 'User sees new onboarding flow', :js do ...@@ -41,4 +41,46 @@ RSpec.describe 'User sees new onboarding flow', :js do
expect(page).to have_content('Learn GitLab') expect(page).to have_content('Learn GitLab')
end end
context 'with confetti_post_signup experiment candidate experience', :experiment do
before do
stub_experiments(change_continuous_onboarding_link_urls: :control,
combined_registration: :control,
confetti_post_signup: :candidate)
end
it 'shows continuous onboarding flow pages with celebration invite modal' do
sign_in(create(:user))
visit users_sign_up_welcome_path
expect(page).to have_content('Welcome to GitLab')
choose 'Just me'
click_on 'Continue'
expect(page).to have_content('Create your group')
fill_in 'group_name', with: 'test'
expect(page).to have_field('group_path', with: 'test')
click_on 'Create group'
expect(page).to have_content('Create/import your first project')
fill_in 'project_name', with: 'test'
expect(page).to have_field('project_path', with: 'test')
click_on 'Create project'
expect(page).to have_content('Get started with GitLab')
Sidekiq::Worker.drain_all
click_on "Ok, let's go"
expect(page).to have_content('Learn GitLab')
expect(page).to have_content('GitLab is better with colleagues!')
end
end
end end
...@@ -17,8 +17,10 @@ RSpec.describe LearnGitlabHelper do ...@@ -17,8 +17,10 @@ RSpec.describe LearnGitlabHelper do
OnboardingProgress.onboard(namespace) OnboardingProgress.onboard(namespace)
end end
describe '#onboarding_actions_data' do describe '#learn_gitlab_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) } subject(:onboarding_actions_data) do
Gitlab::Json.parse(helper.learn_gitlab_data(project)[:actions]).deep_symbolize_keys
end
context 'when in the new action URLs experiment' do context 'when in the new action URLs experiment' do
before do before do
......
...@@ -19003,15 +19003,24 @@ msgstr "" ...@@ -19003,15 +19003,24 @@ msgstr ""
msgid "InviteMembersModal|Configure security features" msgid "InviteMembersModal|Configure security features"
msgstr "" msgstr ""
msgid "InviteMembersModal|Congratulations on creating your project, you're almost there!"
msgstr ""
msgid "InviteMembersModal|Contribute to the codebase" msgid "InviteMembersModal|Contribute to the codebase"
msgstr "" msgstr ""
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)" msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
msgstr "" msgstr ""
msgid "InviteMembersModal|GitLab is better with colleagues!"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address" msgid "InviteMembersModal|GitLab member or email address"
msgstr "" msgstr ""
msgid "InviteMembersModal|How about inviting a colleague or two to join you?"
msgstr ""
msgid "InviteMembersModal|Invite" msgid "InviteMembersModal|Invite"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import confetti from 'canvas-confetti';
import Confetti from '~/invite_members/components/confetti.vue';
jest.mock('canvas-confetti', () => ({
create: jest.fn(),
}));
let wrapper;
const createComponent = () => {
wrapper = shallowMount(Confetti);
};
afterEach(() => {
wrapper.destroy();
});
describe('Confetti', () => {
it('initiates confetti', () => {
const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {});
createComponent();
expect(confetti.create).toHaveBeenCalled();
expect(basicCannon).toHaveBeenCalled();
});
});
...@@ -15,11 +15,19 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -15,11 +15,19 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking'; import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { import {
INVITE_MEMBERS_IN_COMMENT, INVITE_MEMBERS_IN_COMMENT,
MEMBER_AREAS_OF_FOCUS, MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK, INVITE_MEMBERS_FOR_TASK,
CANCEL_BUTTON_TEXT,
INVITE_BUTTON_TEXT,
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_MODAL_DEFAULT_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
} from '~/invite_members/constants'; } from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub'; import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -74,6 +82,7 @@ const user4 = { ...@@ -74,6 +82,7 @@ const user4 = {
avatar_url: '', avatar_url: '',
}; };
const sharedGroup = { id: '981' }; const sharedGroup = { id: '981' };
const GlEmoji = { template: '<img/>' };
const createComponent = (data = {}, props = {}) => { const createComponent = (data = {}, props = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, { wrapper = shallowMountExtended(InviteMembersModal, {
...@@ -104,6 +113,7 @@ const createComponent = (data = {}, props = {}) => { ...@@ -104,6 +113,7 @@ const createComponent = (data = {}, props = {}) => {
}), }),
GlDropdown: true, GlDropdown: true,
GlDropdownItem: true, GlDropdownItem: true,
GlEmoji,
GlSprintf, GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, { GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback', 'description'], props: ['state', 'invalidFeedback', 'description'],
...@@ -158,6 +168,7 @@ describe('InviteMembersModal', () => { ...@@ -158,6 +168,7 @@ describe('InviteMembersModal', () => {
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks'); const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select'); const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert'); const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
const findCelebrationEmoji = () => wrapper.findComponent(GlModal).find(GlEmoji);
describe('rendering the modal', () => { describe('rendering the modal', () => {
beforeEach(() => { beforeEach(() => {
...@@ -165,15 +176,15 @@ describe('InviteMembersModal', () => { ...@@ -165,15 +176,15 @@ describe('InviteMembersModal', () => {
}); });
it('renders the modal with the correct title', () => { it('renders the modal with the correct title', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe('Invite members'); expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_DEFAULT_TITLE);
}); });
it('renders the Cancel button text correctly', () => { it('renders the Cancel button text correctly', () => {
expect(findCancelButton().text()).toBe('Cancel'); expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
}); });
it('renders the Invite button text correctly', () => { it('renders the Invite button text correctly', () => {
expect(findInviteButton().text()).toBe('Invite'); expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
}); });
it('renders the Invite button modal without isLoading', () => { it('renders the Invite button modal without isLoading', () => {
...@@ -342,11 +353,40 @@ describe('InviteMembersModal', () => { ...@@ -342,11 +353,40 @@ describe('InviteMembersModal', () => {
describe('displaying the correct introText and form group description', () => { describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => { describe('when inviting to a project', () => {
describe('when inviting members', () => { describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => { beforeEach(() => {
createInviteMembersToProjectWrapper(); createInviteMembersToProjectWrapper();
});
it('renders the modal without confetti', () => {
expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false);
});
it('includes the correct invitee, type, and formatted name', () => {
expect(findIntroText()).toBe("You're inviting members to the test name project."); expect(findIntroText()).toBe("You're inviting members to the test name project.");
expect(membersFormGroupDescription()).toBe('Select members or type email addresses'); expect(findCelebrationEmoji().exists()).toBe(false);
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
describe('when inviting members with celebration', () => {
beforeEach(() => {
createComponent({ mode: 'celebrate', inviteeType: 'members' }, { isProject: true });
});
it('renders the modal with confetti', () => {
expect(wrapper.findComponent(ModalConfetti).exists()).toBe(true);
});
it('renders the modal with the correct title', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
});
it('includes the correct celebration text and emoji', () => {
expect(findIntroText()).toBe(
`${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`,
);
expect(findCelebrationEmoji().exists()).toBe(true);
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
}); });
}); });
...@@ -366,7 +406,7 @@ describe('InviteMembersModal', () => { ...@@ -366,7 +406,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper(); createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group."); expect(findIntroText()).toBe("You're inviting members to the test name group.");
expect(membersFormGroupDescription()).toBe('Select members or type email addresses'); expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
}); });
}); });
......
import { GlProgressBar } from '@gitlab/ui'; import { GlProgressBar } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue'; import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import eventHub from '~/invite_members/event_hub';
import { testActions, testSections } from './mock_data'; import { testActions, testSections } from './mock_data';
describe('Learn GitLab', () => { describe('Learn GitLab', () => {
let wrapper; let wrapper;
let inviteMembersOpen = false;
const createWrapper = () => { const createWrapper = () => {
wrapper = mount(LearnGitlab, { propsData: { actions: testActions, sections: testSections } }); wrapper = mount(LearnGitlab, {
propsData: { actions: testActions, sections: testSections, inviteMembersOpen },
});
}; };
beforeEach(() => { beforeEach(() => {
...@@ -17,6 +21,7 @@ describe('Learn GitLab', () => { ...@@ -17,6 +21,7 @@ describe('Learn GitLab', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
inviteMembersOpen = false;
}); });
it('renders correctly', () => { it('renders correctly', () => {
...@@ -35,4 +40,30 @@ describe('Learn GitLab', () => { ...@@ -35,4 +40,30 @@ describe('Learn GitLab', () => {
expect(progressBar.attributes('value')).toBe('2'); expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('9'); expect(progressBar.attributes('max')).toBe('9');
}); });
describe('Invite Members Modal', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
});
it('emits openModal', () => {
inviteMembersOpen = true;
createWrapper();
expect(spy).toHaveBeenCalledWith('openModal', {
mode: 'celebrate',
inviteeType: 'members',
source: 'learn-gitlab',
});
});
it('does not emit openModal', () => {
createWrapper();
expect(spy).not.toHaveBeenCalled();
});
});
}); });
This diff is collapsed.
...@@ -3276,6 +3276,11 @@ caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001259: ...@@ -3276,6 +3276,11 @@ caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001259:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001261.tgz#96d89813c076ea061209a4e040d8dcf0c66a1d01" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001261.tgz#96d89813c076ea061209a4e040d8dcf0c66a1d01"
integrity sha512-vM8D9Uvp7bHIN0fZ2KQ4wnmYFpJo/Etb4Vwsuc+ka0tfGDHvOPrFm6S/7CCNLSOkAUjenT2HnUPESdOIL91FaA== integrity sha512-vM8D9Uvp7bHIN0fZ2KQ4wnmYFpJo/Etb4Vwsuc+ka0tfGDHvOPrFm6S/7CCNLSOkAUjenT2HnUPESdOIL91FaA==
canvas-confetti@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.4.0.tgz#840f6db4a566f8f32abe28c00dcd82acf39c92bd"
integrity sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ==
capture-exit@^2.0.0: capture-exit@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
......
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