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';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { sprintf } from '~/locale';
import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
MODAL_LABELS,
} from '../constants';
import eventHub from '../event_hub';
import {
responseMessageFromError,
responseMessageFromSuccess,
} from '../utils/response_message_parser';
import ModalConfetti from './confetti.vue';
import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue';
......@@ -50,6 +52,7 @@ export default {
GlFormCheckboxGroup,
MembersTokenSelect,
GroupSelect,
ModalConfetti,
},
inject: ['newProjectPath'],
props: {
......@@ -129,22 +132,30 @@ export default {
source: 'unknown',
invalidFeedbackMessage: '',
isLoading: false,
mode: 'default',
};
},
computed: {
isCelebration() {
return this.mode === 'celebrate';
},
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
isInviteGroup() {
return this.inviteeType === 'group';
},
modalTitle() {
return this.$options.labels[this.inviteeType].modal[this.mode].title;
},
introText() {
const inviteTo = this.isProject ? 'toProject' : 'toGroup';
return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, {
name: this.name,
});
},
inviteTo() {
return this.isProject ? 'toProject' : 'toGroup';
},
toastOptions() {
return {
onComplete: () => {
......@@ -234,7 +245,8 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
openModal({ inviteeType, source }) {
openModal({ mode = 'default', inviteeType, source }) {
this.mode = mode;
this.inviteeType = inviteeType;
this.source = source;
......@@ -381,60 +393,7 @@ export default {
return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
},
},
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)',
),
},
labels: MODAL_LABELS,
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
......@@ -445,12 +404,15 @@ export default {
size="sm"
data-qa-selector="invite_members_modal_content"
data-testid="invite-members-modal"
:title="$options.labels[inviteeType].modalTitle"
:title="modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
@hidden="resetFields"
@close="resetFields"
@hide="resetFields"
>
<div>
<div class="gl-display-flex">
<div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div>
<div>
<p ref="introText">
<gl-sprintf :message="introText">
......@@ -458,7 +420,12 @@ export default {
<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
:invalid-feedback="invalidFeedbackMessage"
......
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
......@@ -27,3 +27,120 @@ export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button';
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>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
......@@ -22,6 +23,11 @@ export default {
required: true,
type: Object,
},
inviteMembersOpen: {
type: Boolean,
required: false,
default: false,
},
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
......@@ -33,7 +39,15 @@ export default {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
},
},
mounted() {
if (this.inviteMembersOpen) {
this.openInviteMembersModal('celebrate');
}
},
methods: {
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
},
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actions).filter(
......
import Vue from 'vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
......@@ -11,15 +12,17 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
props: { actions, sections },
props: { actions, sections, inviteMembersOpen },
});
},
});
}
initInviteMembersModal();
initLearnGitlab();
......@@ -7,6 +7,20 @@ module LearnGitlabHelper
learn_gitlab_onboarding_available?(project)
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)
attributes = onboarding_progress(project).attributes.symbolize_keys
......@@ -42,13 +56,6 @@ module LearnGitlabHelper
}
end
def learn_gitlab_onboarding_available?(project)
OnboardingProgress.onboarding?(project.namespace) &&
LearnGitlab::Project.new(current_user).available?
end
private
def action_urls
LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
......
- breadcrumb_title _("Learn GitLab")
- page_title _("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
project = ::Project.find(params[:project_id])
return access_denied! unless can?(current_user, :owner_access, project)
session[:confetti_post_signup] = true
render locals: { project: project }
end
......
......@@ -38,6 +38,12 @@ RSpec.describe Registrations::WelcomeController do
end
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
context 'with a non-owner user signed in' do
......
......@@ -41,4 +41,46 @@ RSpec.describe 'User sees new onboarding flow', :js do
expect(page).to have_content('Learn GitLab')
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
......@@ -17,8 +17,10 @@ RSpec.describe LearnGitlabHelper do
OnboardingProgress.onboard(namespace)
end
describe '#onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
describe '#learn_gitlab_data' do
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
before do
......
......@@ -19003,15 +19003,24 @@ msgstr ""
msgid "InviteMembersModal|Configure security features"
msgstr ""
msgid "InviteMembersModal|Congratulations on creating your project, you're almost there!"
msgstr ""
msgid "InviteMembersModal|Contribute to the codebase"
msgstr ""
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
msgstr ""
msgid "InviteMembersModal|GitLab is better with colleagues!"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address"
msgstr ""
msgid "InviteMembersModal|How about inviting a colleague or two to join you?"
msgstr ""
msgid "InviteMembersModal|Invite"
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';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
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 {
INVITE_MEMBERS_IN_COMMENT,
MEMBER_AREAS_OF_FOCUS,
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';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
......@@ -74,6 +82,7 @@ const user4 = {
avatar_url: '',
};
const sharedGroup = { id: '981' };
const GlEmoji = { template: '<img/>' };
const createComponent = (data = {}, props = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
......@@ -104,6 +113,7 @@ const createComponent = (data = {}, props = {}) => {
}),
GlDropdown: true,
GlDropdownItem: true,
GlEmoji,
GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback', 'description'],
......@@ -158,6 +168,7 @@ describe('InviteMembersModal', () => {
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
const findCelebrationEmoji = () => wrapper.findComponent(GlModal).find(GlEmoji);
describe('rendering the modal', () => {
beforeEach(() => {
......@@ -165,15 +176,15 @@ describe('InviteMembersModal', () => {
});
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', () => {
expect(findCancelButton().text()).toBe('Cancel');
expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
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', () => {
......@@ -342,11 +353,40 @@ describe('InviteMembersModal', () => {
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
beforeEach(() => {
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(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', () => {
createInviteMembersToGroupWrapper();
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 { mount } from '@vue/test-utils';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import eventHub from '~/invite_members/event_hub';
import { testActions, testSections } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
let inviteMembersOpen = false;
const createWrapper = () => {
wrapper = mount(LearnGitlab, { propsData: { actions: testActions, sections: testSections } });
wrapper = mount(LearnGitlab, {
propsData: { actions: testActions, sections: testSections, inviteMembersOpen },
});
};
beforeEach(() => {
......@@ -17,6 +21,7 @@ describe('Learn GitLab', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
inviteMembersOpen = false;
});
it('renders correctly', () => {
......@@ -35,4 +40,30 @@ describe('Learn GitLab', () => {
expect(progressBar.attributes('value')).toBe('2');
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();
});
});
});
......@@ -19,12 +19,51 @@ RSpec.describe LearnGitlabHelper do
OnboardingProgress.register(namespace, :git_write)
end
describe '#onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
describe '#learn_gitlab_enabled?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { helper.learn_gitlab_enabled?(project) }
where(:onboarding, :learn_gitlab_available, :result) do
true | true | true
true | false | false
false | true | false
end
with_them do
before do
allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available)
end
shared_examples 'has all actions' do
context 'when signed in' do
before do
sign_in(user)
end
it { is_expected.to eq(result) }
end
end
context 'when not signed in' do
it { is_expected.to eq(false) }
end
end
describe '#learn_gitlab_data' do
subject(:learn_gitlab_data) { helper.learn_gitlab_data(project) }
let(:onboarding_actions_data) { Gitlab::Json.parse(learn_gitlab_data[:actions]).deep_symbolize_keys }
let(:onboarding_sections_data) { Gitlab::Json.parse(learn_gitlab_data[:sections]).deep_symbolize_keys }
shared_examples 'has all data' do
it 'has all actions' do
expect(onboarding_actions_data.keys).to contain_exactly(
expected_keys = [
:issue_created,
:git_write,
:pipeline_created,
......@@ -34,11 +73,18 @@ RSpec.describe LearnGitlabHelper do
:required_mr_approvals_enabled,
:code_owners_enabled,
:security_scan_enabled
)
]
expect(onboarding_actions_data.keys).to contain_exactly(*expected_keys)
end
it 'has all section data', :aggregate_failures do
expect(onboarding_sections_data.keys).to contain_exactly(:deploy, :plan, :workspace)
expect(onboarding_sections_data.values.map { |section| section.keys }).to match_array([[:svg]] * 3)
end
end
it_behaves_like 'has all actions'
it_behaves_like 'has all data'
it 'sets correct paths' do
expect(onboarding_actions_data).to match({
......@@ -91,7 +137,7 @@ RSpec.describe LearnGitlabHelper do
stub_experiments(change_continuous_onboarding_link_urls: :candidate)
end
it_behaves_like 'has all actions'
it_behaves_like 'has all data'
it 'sets mostly new paths' do
expect(onboarding_actions_data).to match({
......@@ -126,51 +172,4 @@ RSpec.describe LearnGitlabHelper do
end
end
end
describe '#learn_gitlab_enabled?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { helper.learn_gitlab_enabled?(project) }
where(:onboarding, :learn_gitlab_available, :result) do
true | true | true
true | false | false
false | true | false
end
with_them do
before do
allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available)
end
context 'when signed in' do
before do
sign_in(user)
end
it { is_expected.to eq(result) }
end
end
context 'when not signed in' do
it { is_expected.to eq(false) }
end
end
describe '#onboarding_sections_data' do
subject(:sections) { helper.onboarding_sections_data }
it 'has the right keys' do
expect(sections.keys).to contain_exactly(:deploy, :plan, :workspace)
end
it 'has the svg' do
expect(sections.values.map { |section| section.keys }).to eq([[:svg]] * 3)
end
end
end
......@@ -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"
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:
version "2.0.0"
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