Commit d62e2dcd authored by Terri Chu's avatar Terri Chu

Merge branch 'invite-for-help-continuous-onboarding' into 'master'

Open invite members for task modal in Learn GitLab project

See merge request gitlab-org/gitlab!73846
parents 0df08101 c97b99a9
......@@ -30,7 +30,7 @@ export function getAllExperimentContexts() {
return Object.values(getExperimentsData()).map(createGitlabExperimentContext);
}
export function isExperimentVariant(experimentName, variantName) {
export function isExperimentVariant(experimentName, variantName = CANDIDATE_VARIANT) {
return getExperimentData(experimentName)?.variant === variantName;
}
......
......@@ -26,6 +26,7 @@ import {
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
MODAL_LABELS,
LEARN_GITLAB,
} from '../constants';
import eventHub from '../event_hub';
import {
......@@ -200,7 +201,8 @@ export default {
},
tasksToBeDoneEnabled() {
return (
getParameterValues('open_modal')[0] === 'invite_members_for_task' &&
(getParameterValues('open_modal')[0] === 'invite_members_for_task' ||
this.isOnLearnGitlab) &&
this.tasksToBeDoneOptions.length
);
},
......@@ -221,11 +223,18 @@ export default {
? this.selectedTaskProject.id
: '';
},
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
if (this.isOnLearnGitlab) {
this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, this.source);
} else {
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
}
});
if (this.tasksToBeDoneEnabled) {
......@@ -303,7 +312,7 @@ export default {
: Api.groupShareWithGroup.bind(Api);
apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id))
.then(this.showToastMessageSuccess)
.then(this.showSuccessMessage)
.catch(this.showInvalidFeedbackMessage);
},
submitInviteMembers() {
......@@ -332,7 +341,7 @@ export default {
this.trackinviteMembersForTask();
Promise.all(promises)
.then(this.conditionallyShowToastSuccess)
.then(this.conditionallyShowSuccessMessage)
.catch(this.showInvalidFeedbackMessage);
},
inviteByEmailPostData(usersToInviteByEmail) {
......@@ -364,11 +373,11 @@ export default {
group_access: this.selectedAccessLevel,
};
},
conditionallyShowToastSuccess(response) {
conditionallyShowSuccessMessage(response) {
const message = this.unescapeMsg(responseMessageFromSuccess(response));
if (message === '') {
this.showToastMessageSuccess();
this.showSuccessMessage();
return;
}
......@@ -376,8 +385,12 @@ export default {
this.invalidFeedbackMessage = message;
this.isLoading = false;
},
showToastMessageSuccess() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
showSuccessMessage() {
if (this.isOnLearnGitlab) {
eventHub.$emit('showSuccessfulInvitationsAlert');
} else {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
}
this.closeModal();
},
showInvalidFeedbackMessage(response) {
......
......@@ -144,3 +144,5 @@ export const MODAL_LABELS = {
headerCloseLabel: HEADER_CLOSE_LABEL,
areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
};
export const LEARN_GITLAB = 'learn_gitlab';
<script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
import { GlProgressBar, GlSprintf, GlAlert } 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';
export default {
components: { GlProgressBar, GlSprintf, LearnGitlabSectionCard },
components: { GlProgressBar, GlSprintf, GlAlert, LearnGitlabSectionCard },
i18n: {
title: s__('LearnGitLab|Learn GitLab'),
description: s__(
'LearnGitLab|Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.',
),
percentageCompleted: s__(`LearnGitLab|%{percentage}%{percentSymbol} completed`),
successfulInvitations: s__(
"LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project.",
),
},
props: {
actions: {
......@@ -28,12 +31,22 @@ export default {
required: false,
default: false,
},
project: {
required: true,
type: Object,
},
},
data() {
return {
showSuccessfulInvitationsAlert: false,
actionsData: this.actions,
};
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
computed: {
progressValue() {
return Object.values(this.actions).filter((a) => a.completed).length;
return Object.values(this.actionsData).filter((a) => a.completed).length;
},
progressPercentage() {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
......@@ -43,14 +56,23 @@ export default {
if (this.inviteMembersOpen) {
this.openInviteMembersModal('celebrate');
}
eventHub.$on('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
beforeDestroy() {
eventHub.$off('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
methods: {
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
},
handleShowSuccessfulInvitationsAlert() {
this.showSuccessfulInvitationsAlert = true;
this.markActionAsCompleted('userAdded');
},
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actions).filter(
Object.entries(this.actionsData).filter(
([action]) => ACTION_LABELS[action].section === section,
),
);
......@@ -59,11 +81,34 @@ export default {
svgFor(section) {
return this.sections[section].svg;
},
markActionAsCompleted(completedAction) {
Object.keys(this.actionsData).forEach((action) => {
if (action === completedAction) {
this.actionsData[action].completed = true;
this.modifySidebarPercentage();
}
});
},
modifySidebarPercentage() {
const el = document.querySelector('.sidebar-top-level-items .active .count');
el.textContent = `${this.progressPercentage}%`;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showSuccessfulInvitationsAlert"
class="gl-mt-5"
@dismiss="showSuccessfulInvitationsAlert = false"
>
<gl-sprintf :message="$options.i18n.successfulInvitations">
<template #projectName>
<strong>{{ project.name }}</strong>
</template>
</gl-sprintf>
</gl-alert>
<div class="row">
<div class="gl-mb-7 gl-ml-5">
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
......
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { isExperimentVariant } from '~/experimentation/utils';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS } from '../constants';
......@@ -24,6 +26,20 @@ export default {
trialOnly() {
return ACTION_LABELS[this.action].trialRequired;
},
showInviteModalLink() {
return (
this.action === 'userAdded' && isExperimentVariant('invite_for_help_continuous_onboarding')
);
},
},
methods: {
openModal() {
eventHub.$emit('openModal', {
inviteeType: 'members',
source: 'learn_gitlab',
tasksToBeDoneEnabled: true,
});
},
},
};
</script>
......@@ -33,18 +49,27 @@ export default {
<gl-icon name="check-circle-filled" :size="16" data-testid="completed-icon" />
{{ $options.i18n.ACTION_LABELS[action].title }}
</span>
<span v-else>
<gl-link
target="_blank"
:href="value.url"
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
data-track-experiment="change_continuous_onboarding_link_urls"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
</span>
<gl-link
v-else-if="showInviteModalLink"
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
data-track-property="Growth::Activation::Experiment::InviteForHelpContinuousOnboarding"
data-testid="invite-for-help-continuous-onboarding-experiment-link"
@click="openModal"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
<gl-link
v-else
target="_blank"
:href="value.url"
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
data-track-experiment="change_continuous_onboarding_link_urls"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
<span v-if="trialOnly" class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
- {{ $options.i18n.trialOnly }}
</span>
......
......@@ -12,17 +12,18 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project));
const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
props: { actions, sections, inviteMembersOpen },
props: { actions, sections, project, inviteMembersOpen },
});
},
});
}
initInviteMembersModal();
initLearnGitlab();
initInviteMembersModal();
......@@ -3,6 +3,7 @@
class Projects::LearnGitlabController < Projects::ApplicationController
before_action :authenticate_user!
before_action :check_experiment_enabled?
before_action :enable_invite_for_help_continuous_onboarding_experiment
feature_category :users
......@@ -14,4 +15,13 @@ class Projects::LearnGitlabController < Projects::ApplicationController
def check_experiment_enabled?
return access_denied! unless helpers.learn_gitlab_enabled?(project)
end
def enable_invite_for_help_continuous_onboarding_experiment
return unless current_user.can?(:admin_group_member, project.namespace)
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
e.candidate {}
e.record!
end
end
end
......@@ -42,7 +42,7 @@ module InviteMembersHelper
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
end
if show_invite_members_for_task?
if show_invite_members_for_task?(source)
dataset.merge!(
tasks_to_be_done_options: tasks_to_be_done_options.to_json,
projects: projects_for_source(source).to_json,
......@@ -80,10 +80,12 @@ module InviteMembersHelper
{}
end
def show_invite_members_for_task?
return unless current_user && experiment(:invite_members_for_task).enabled?
def show_invite_members_for_task?(source)
return unless current_user
params[:open_modal] == 'invite_members_for_task'
invite_members_for_task_experiment = experiment(:invite_members_for_task).enabled? && params[:open_modal] == 'invite_members_for_task'
invite_for_help_continuous_onboarding = source.is_a?(Project) && experiment(:invite_for_help_continuous_onboarding, namespace: source.namespace).variant.name == 'candidate'
invite_members_for_task_experiment || invite_for_help_continuous_onboarding
end
def tasks_to_be_done_options
......
......@@ -10,7 +10,8 @@ module LearnGitlabHelper
def learn_gitlab_data(project)
{
actions: onboarding_actions_data(project).to_json,
sections: onboarding_sections_data.to_json
sections: onboarding_sections_data.to_json,
project: onboarding_project_data(project).to_json
}
end
......@@ -56,6 +57,10 @@ module LearnGitlabHelper
}
end
def onboarding_project_data(project)
{ name: project.name }
end
def action_urls
LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
......
......@@ -4,9 +4,10 @@
- data = learn_gitlab_data(@project)
- invite_members_open = session.delete(:confetti_post_signup)
= render 'projects/invite_members_modal', project: @project
- 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: invite_for_help_continuous_onboarding
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73846
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345708
milestone: '14.5'
type: experiment
group: group::activation
default_enabled: false
......@@ -20583,6 +20583,9 @@ msgstr ""
msgid "LearnGitLab|Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:"
msgstr ""
msgid "LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project."
msgstr ""
msgid "LearnGitlab|Creating your onboarding experience..."
msgstr ""
......
......@@ -5,14 +5,15 @@ require 'spec_helper'
RSpec.describe Projects::LearnGitlabController do
describe 'GET #index' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:project) { create(:project, namespace: create(:group)) }
let(:learn_gitlab_enabled) { true }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { get :index, params: params }
subject(:action) { get :index, params: params }
before do
project.namespace.add_owner(user)
allow(controller.helpers).to receive(:learn_gitlab_enabled?).and_return(learn_gitlab_enabled)
end
......@@ -32,6 +33,10 @@ RSpec.describe Projects::LearnGitlabController do
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do
subject { project.namespace }
end
end
end
end
......@@ -99,12 +99,13 @@ describe('experiment Utilities', () => {
describe('isExperimentVariant', () => {
describe.each`
experiment | variant | input | output
${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
experiment | variant | input | output
${ABC_KEY} | ${CANDIDATE_VARIANT} | ${[ABC_KEY]} | ${true}
${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
`(
'with input=$input, experiment=$experiment, variant=$variant',
({ experiment, variant, input, output }) => {
......
......@@ -28,6 +28,7 @@ import {
MEMBERS_MODAL_DEFAULT_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
......@@ -268,6 +269,14 @@ describe('InviteMembersModal', () => {
expect(findTasksToBeDone().exists()).toBe(false);
});
describe('when opened from the Learn GitLab page', () => {
it('does render the tasks to be done', () => {
setupComponent({ source: LEARN_GITLAB }, {}, []);
expect(findTasksToBeDone().exists()).toBe(true);
});
});
});
describe('rendering the tasks', () => {
......@@ -465,7 +474,6 @@ describe('InviteMembersModal', () => {
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
});
it('includes the non-default selected areas of focus', () => {
......@@ -492,7 +500,23 @@ describe('InviteMembersModal', () => {
});
it('displays the successful toastMessage', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
});
describe('when opened from a Learn GitLab page', () => {
it('emits the `showSuccessfulInvitationsAlert` event', async () => {
eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
jest.spyOn(eventHub, '$emit').mockImplementation();
clickInviteButton();
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('showSuccessfulInvitationsAlert');
});
});
});
......@@ -649,7 +673,6 @@ describe('InviteMembersModal', () => {
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
});
it('includes the non-default selected areas of focus', () => {
......@@ -672,7 +695,9 @@ describe('InviteMembersModal', () => {
});
it('displays the successful toastMessage', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
});
});
......@@ -711,13 +736,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toast message when email has already been invited', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
clickInviteButton();
await waitForPromises();
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
expect(findMembersSelect().props('validationState')).toBe(null);
});
......@@ -782,7 +808,6 @@ describe('InviteMembersModal', () => {
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
jest.spyOn(wrapper.vm, 'trackInvite');
});
......@@ -800,7 +825,9 @@ describe('InviteMembersModal', () => {
});
it('displays the successful toastMessage', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
});
......@@ -855,7 +882,6 @@ describe('InviteMembersModal', () => {
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
clickInviteButton();
});
......@@ -865,7 +891,9 @@ describe('InviteMembersModal', () => {
});
it('displays the successful toastMessage', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
});
......@@ -930,6 +958,13 @@ describe('InviteMembersModal', () => {
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view);
});
it('tracks the view for learn_gitlab source', () => {
eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
});
it('tracks the invite for areas_of_focus', () => {
eventHub.$emit('openModal', { inviteeType: 'members' });
......
......@@ -2,6 +2,8 @@
exports[`Learn GitLab renders correctly 1`] = `
<div>
<!---->
<div
class="row"
>
......@@ -131,66 +133,60 @@ exports[`Learn GitLab renders correctly 1`] = `
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Set up CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Set up CI/CD
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Set up CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Set up CI/CD
</a>
<!---->
</div>
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Start a free Ultimate trial"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Start a free Ultimate trial
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Start a free Ultimate trial"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Start a free Ultimate trial
</a>
<!---->
</div>
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add code owners"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Add code owners
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add code owners"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Add code owners
</a>
<span
class="gl-font-style-italic gl-text-gray-500"
......@@ -204,22 +200,20 @@ exports[`Learn GitLab renders correctly 1`] = `
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add merge request approval"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Add merge request approval
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add merge request approval"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Add merge request approval
</a>
<span
class="gl-font-style-italic gl-text-gray-500"
......@@ -269,44 +263,40 @@ exports[`Learn GitLab renders correctly 1`] = `
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Create an issue"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Create an issue
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Create an issue"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Create an issue
</a>
<!---->
</div>
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Submit a merge request"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Submit a merge request
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Submit a merge request"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Submit a merge request
</a>
<!---->
</div>
......@@ -349,22 +339,20 @@ exports[`Learn GitLab renders correctly 1`] = `
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Run a Security scan using CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Run a Security scan using CI/CD
</a>
</span>
<a
class="gl-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Run a Security scan using CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Run a Security scan using CI/CD
</a>
<!---->
</div>
......
import { shallowMount } from '@vue/test-utils';
import { stubExperiments } from 'helpers/experimentation_helper';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import eventHub from '~/invite_members/event_hub';
import LearnGitlabSectionLink from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue';
const defaultAction = 'gitWrite';
......@@ -23,6 +26,9 @@ describe('Learn GitLab Section Link', () => {
});
};
const openInviteMembesrModalLink = () =>
wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
it('renders no icon when not completed', () => {
createWrapper(undefined, { completed: false });
......@@ -46,4 +52,54 @@ describe('Learn GitLab Section Link', () => {
expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
});
describe('rendering a link to open the invite_members modal instead of a regular link', () => {
it.each`
action | experimentVariant | showModal
${'userAdded'} | ${'candidate'} | ${true}
${'userAdded'} | ${'control'} | ${false}
${defaultAction} | ${'candidate'} | ${false}
${defaultAction} | ${'control'} | ${false}
`(
'when the invite_for_help_continuous_onboarding experiment has variant: $experimentVariant and action is $action, the modal link is shown: $showModal',
({ action, experimentVariant, showModal }) => {
stubExperiments({ invite_for_help_continuous_onboarding: experimentVariant });
createWrapper(action);
expect(openInviteMembesrModalLink().exists()).toBe(showModal);
},
);
});
describe('clicking the link to open the invite_members modal', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation();
stubExperiments({ invite_for_help_continuous_onboarding: 'candidate' });
createWrapper('userAdded');
});
it('calls the eventHub', () => {
openInviteMembesrModalLink().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('openModal', {
inviteeType: 'members',
source: 'learn_gitlab',
tasksToBeDoneEnabled: true,
});
});
it('tracks the click', async () => {
const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(openInviteMembesrModalLink().element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
label: 'Invite your colleagues',
property: 'Growth::Activation::Experiment::InviteForHelpContinuousOnboarding',
});
unmockTracking();
});
});
});
import { GlProgressBar } from '@gitlab/ui';
import { GlProgressBar, GlAlert } 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';
import { testActions, testSections, testProject } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
let sidebar;
let inviteMembersOpen = false;
const createWrapper = () => {
wrapper = mount(LearnGitlab, {
propsData: { actions: testActions, sections: testSections, inviteMembersOpen },
propsData: {
actions: testActions,
sections: testSections,
project: testProject,
inviteMembersOpen,
},
});
};
beforeEach(() => {
sidebar = document.createElement('div');
sidebar.innerHTML = `
<div class="sidebar-top-level-items">
<div class="active">
<div class="count"></div>
</div>
</div>
`;
document.body.appendChild(sidebar);
createWrapper();
});
......@@ -22,6 +37,7 @@ describe('Learn GitLab', () => {
wrapper.destroy();
wrapper = null;
inviteMembersOpen = false;
sidebar.remove();
});
it('renders correctly', () => {
......@@ -66,4 +82,26 @@ describe('Learn GitLab', () => {
expect(spy).not.toHaveBeenCalled();
});
});
describe('when the showSuccessfulInvitationsAlert event is fired', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
eventHub.$emit('showSuccessfulInvitationsAlert');
});
it('displays the successful invitations alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('displays a message with the project name', () => {
expect(findAlert().text()).toBe(
"Your team is growing! You've successfully invited new team members to the test-project project.",
);
});
it('modifies the sidebar percentage', () => {
expect(sidebar.textContent.trim()).toBe('22%');
});
});
});
......@@ -57,3 +57,7 @@ export const testSections = {
svg: 'plan.svg',
},
};
export const testProject = {
name: 'test-project',
};
......@@ -6,6 +6,7 @@ RSpec.describe InviteMembersHelper do
include Devise::Test::ControllerHelpers
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group, projects: [project]) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let(:owner) { project.owner }
......@@ -64,48 +65,13 @@ RSpec.describe InviteMembersHelper do
end
context 'tasks_to_be_done' do
subject(:output) { helper.common_invite_modal_dataset(source) }
let_it_be(:source) { project }
before do
stub_experiments(invite_members_for_task: true)
end
context 'when not logged in' do
before do
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
end
it "doesn't have the tasks to be done attributes" do
expect(output[:tasks_to_be_done_options]).to be_nil
expect(output[:projects]).to be_nil
expect(output[:new_project_path]).to be_nil
end
end
context 'when logged in but the open_modal param is not present' do
before do
allow(helper).to receive(:current_user).and_return(developer)
end
using RSpec::Parameterized::TableSyntax
it "doesn't have the tasks to be done attributes" do
expect(output[:tasks_to_be_done_options]).to be_nil
expect(output[:projects]).to be_nil
expect(output[:new_project_path]).to be_nil
end
end
context 'when logged in and the open_modal param is present' do
before do
allow(helper).to receive(:current_user).and_return(developer)
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
end
context 'for a group' do
let_it_be(:source) { create(:group, projects: [project]) }
subject(:output) { helper.common_invite_modal_dataset(source) }
it 'has the expected attributes', :aggregate_failures do
shared_examples_for 'including the tasks to be done attributes' do
it 'includes the tasks to be done attributes when expected' do
if expected?
expect(output[:tasks_to_be_done_options]).to eq(
[
{ value: :code, text: 'Create/import code into a project (repository)' },
......@@ -117,24 +83,75 @@ RSpec.describe InviteMembersHelper do
[{ id: project.id, title: project.title }].to_json
)
expect(output[:new_project_path]).to eq(
new_project_path(namespace_id: source.id)
source.is_a?(Project) ? '' : new_project_path(namespace_id: group.id)
)
else
expect(output[:tasks_to_be_done_options]).to be_nil
expect(output[:projects]).to be_nil
expect(output[:new_project_path]).to be_nil
end
end
end
context 'for a project' do
it 'has the expected attributes', :aggregate_failures do
expect(output[:tasks_to_be_done_options]).to eq(
[
{ value: :code, text: 'Create/import code into a project (repository)' },
{ value: :ci, text: 'Set up CI/CD pipelines to build, test, deploy, and monitor code' },
{ value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
].to_json
)
expect(output[:projects]).to eq(
[{ id: project.id, title: project.title }].to_json
)
expect(output[:new_project_path]).to eq('')
context 'the invite_members_for_task experiment' do
where(:invite_members_for_task_enabled?, :open_modal_param_present?, :logged_in?, :expected?) do
true | true | true | true
true | true | false | false
true | false | true | false
true | false | false | false
false | true | true | false
false | true | false | false
false | false | true | false
false | false | false | false
end
with_them do
before do
allow(helper).to receive(:current_user).and_return(developer) if logged_in?
stub_experiments(invite_members_for_task: true) if invite_members_for_task_enabled?
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' }) if open_modal_param_present?
end
context 'when the source is a project' do
let_it_be(:source) { project }
it_behaves_like 'including the tasks to be done attributes'
end
context 'when the source is a group' do
let_it_be(:source) { group }
it_behaves_like 'including the tasks to be done attributes'
end
end
end
context 'the invite_for_help_continuous_onboarding experiment' do
where(:invite_for_help_continuous_onboarding?, :logged_in?, :expected?) do
true | true | true
true | false | false
false | true | false
false | false | false
end
with_them do
before do
allow(helper).to receive(:current_user).and_return(developer) if logged_in?
stub_experiments(invite_for_help_continuous_onboarding: :candidate) if invite_for_help_continuous_onboarding?
end
context 'when the source is a project' do
let_it_be(:source) { project }
it_behaves_like 'including the tasks to be done attributes'
end
context 'when the source is a group' do
let_it_be(:source) { group }
let(:expected?) { false }
it_behaves_like 'including the tasks to be done attributes'
end
end
end
......
......@@ -60,6 +60,7 @@ RSpec.describe LearnGitlabHelper do
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 }
let(:onboarding_project_data) { Gitlab::Json.parse(learn_gitlab_data[:project]).deep_symbolize_keys }
shared_examples 'has all data' do
it 'has all actions' do
......@@ -82,6 +83,11 @@ RSpec.describe LearnGitlabHelper 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
it 'has all project data', :aggregate_failures do
expect(onboarding_project_data.keys).to contain_exactly(:name)
expect(onboarding_project_data.values).to match_array([project.name])
end
end
it_behaves_like 'has all data'
......
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