Commit 67117851 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Simplify Group and Project creation

This experiment combines the group and project pages together to see if
we can increase the success rate of the "first mile".

part of:
https://gitlab.com/gitlab-org/gitlab/-/issues/285563

rebase conflict resolution

prettier, rubocop
parent 2ca1731a
...@@ -71,6 +71,17 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => { ...@@ -71,6 +71,17 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
} }
}; };
const bindHowToImport = () => {
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
$(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
};
const bindEvents = () => { const bindEvents = () => {
const $newProjectForm = $('#new_project'); const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url'); const $projectImportUrl = $('#project_import_url');
...@@ -88,14 +99,7 @@ const bindEvents = () => { ...@@ -88,14 +99,7 @@ const bindEvents = () => {
return; return;
} }
$('.how_to_import_link').on('click', (e) => { bindHowToImport();
e.preventDefault();
$(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
$('.btn_import_gitlab_project').on('click', () => { $('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href'); const importHref = $('a.btn_import_gitlab_project').attr('href');
...@@ -174,3 +178,5 @@ export default { ...@@ -174,3 +178,5 @@ export default {
onProjectNameChange, onProjectNameChange,
onProjectPathChange, onProjectPathChange,
}; };
export { bindHowToImport };
...@@ -16,7 +16,7 @@ module Registrations ...@@ -16,7 +16,7 @@ module Registrations
result = ::Users::SignupService.new(current_user, update_params).execute result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success if result[:status] == :success
return redirect_to new_users_sign_up_group_path(trial_params) if show_signup_onboarding? return redirect_to experiment(:combined_registration, user: current_user).redirect_path(trial_params) if show_signup_onboarding?
members = current_user.members members = current_user.members
......
# frozen_string_literal: true
class CombinedRegistrationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
include Rails.application.routes.url_helpers
def redirect_path(trial_params)
@trial_params = trial_params
run
end
def control_behavior
new_users_sign_up_group_path(@trial_params)
end
def candidate_behavior
new_users_sign_up_groups_project_path
end
end
...@@ -90,10 +90,14 @@ module Projects ...@@ -90,10 +90,14 @@ module Projects
def after_create_actions def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"") log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
# Skip writing the config for project imports/forks because it if @project.import?
# will always fail since the Git directory doesn't exist until experiment(:combined_registration, user: current_user).track(:import_project)
# a background job creates it (see Project#add_import_job). else
@project.set_full_path unless @project.import? # Skip writing the config for project imports/forks because it
# will always fail since the Git directory doesn't exist until
# a background job creates it (see Project#add_import_job).
@project.set_full_path
end
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki? @project.create_wiki unless skip_wiki?
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.form-group.project-name.col-sm-12 .form-group.project-name.col-sm-12
= f.label :name, class: 'label-bold' do = f.label :name, class: 'label-bold' do
%span= _("Project name") %span= _("Project name")
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true } = f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
.form-group.project-path.col-sm-6 .form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do = f.label :namespace_id, class: 'label-bold' do
%span= _('Project URL') %span= _('Project URL')
......
---
name: combined_registration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67614
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285533
milestone: '14.3'
type: experiment
group: group::adoption
default_enabled: false
...@@ -68,6 +68,9 @@ Rails.application.routes.draw do ...@@ -68,6 +68,9 @@ Rails.application.routes.draw do
Gitlab.ee do Gitlab.ee do
resources :groups, only: [:new, :create] resources :groups, only: [:new, :create]
resources :projects, only: [:new, :create] resources :projects, only: [:new, :create]
resources :groups_projects, only: [:new, :create] do
post :import, on: :collection
end
end end
end end
......
/* eslint-disable no-new */
import mountComponents from 'ee/registrations/groups_projects/new';
import Group from '~/group';
new Group();
mountComponents();
...@@ -10,3 +10,5 @@ export const STEPS = { ...@@ -10,3 +10,5 @@ export const STEPS = {
export const SUBSCRIPTON_FLOW_STEPS = [STEPS.yourProfile, STEPS.checkout, STEPS.yourGroup]; export const SUBSCRIPTON_FLOW_STEPS = [STEPS.yourProfile, STEPS.checkout, STEPS.yourGroup];
export const SIGNUP_ONBOARDING_FLOW_STEPS = [STEPS.yourProfile, STEPS.yourGroup, STEPS.yourProject]; export const SIGNUP_ONBOARDING_FLOW_STEPS = [STEPS.yourProfile, STEPS.yourGroup, STEPS.yourProject];
export const COMBINED_SIGNUP_FLOW_STEPS = [STEPS.yourProfile, STEPS.yourProject];
import $ from 'jquery';
import { bindHowToImport } from '~/projects/project_new';
import { displayGroupPath, displayProjectPath } from './path_display';
import mountProgressBar from './progress_bar';
import showTooltip from './show_tooltip';
const importButtonsSubmit = () => {
const buttons = document.querySelectorAll('.js-import-project-buttons a');
const form = document.querySelector('.js-import-project-form');
const submit = form.querySelector('input[type="submit"]');
const importUrlField = form.querySelector('.js-import-url');
const clickHandler = (e) => {
e.preventDefault();
importUrlField.value = e.target.getAttribute('href');
submit.click();
};
buttons.forEach((button) => button.addEventListener('click', clickHandler));
};
const setAutofocus = () => {
const setInputfocus = () => {
document
.querySelector('.js-group-project-tab-contents .tab-pane.active .js-group-name-field')
?.focus();
};
setInputfocus();
$('.js-group-project-tabs').on('shown.bs.tab', setInputfocus);
};
export default () => {
mountProgressBar();
displayGroupPath('.js-group-path-source', '.js-group-path-display');
displayGroupPath('.js-import-group-path-source', '.js-import-group-path-display');
displayProjectPath('.js-project-path-source', '.js-project-path-display');
showTooltip('.js-group-name-tooltip');
importButtonsSubmit();
bindHowToImport();
setAutofocus();
};
import { slugify, convertUnicodeToAscii } from '~/lib/utils/text_utility';
class DisplayInputValue {
constructor(sourceElementSelector, targetElementSelector, transformer) {
this.sourceElement = document.querySelector(sourceElementSelector);
this.targetElement = document.querySelector(targetElementSelector);
this.originalTargetValue = this.targetElement?.textContent;
this.transformer = transformer;
this.updateHandler = this.update.bind(this);
}
update() {
let { value } = this.sourceElement;
if (value.length === 0) {
value = this.originalTargetValue;
} else if (this.transformer) {
value = this.transformer(value);
}
this.targetElement.textContent = value;
}
listen(callback) {
if (!this.sourceElement || !this.targetElement) return null;
this.updateHandler();
return callback(this.sourceElement, this.updateHandler);
}
}
export const displayGroupPath = (sourceSelector, targetSelector) => {
const display = new DisplayInputValue(sourceSelector, targetSelector);
if (!display) return null;
const callback = (sourceElement, updateHandler) => {
const observer = new MutationObserver((mutationList) => {
mutationList.forEach((mutation) => {
if (mutation.attributeName === 'value') {
updateHandler();
}
});
});
observer.observe(sourceElement, { attributes: true });
};
return display.listen(callback);
};
export const displayProjectPath = (sourceSelector, displaySelector) => {
const transformer = (value) => slugify(convertUnicodeToAscii(value));
const display = new DisplayInputValue(sourceSelector, displaySelector, transformer);
if (!display) return null;
const callback = (sourceElement, updateHandler) => {
sourceElement.addEventListener('input', updateHandler);
};
return display.listen(callback);
};
import Vue from 'vue';
import ProgressBar from '../../components/progress_bar.vue';
import { STEPS, COMBINED_SIGNUP_FLOW_STEPS } from '../../constants';
export default function mountProgressBar() {
const el = document.getElementById('progress-bar');
if (!el) {
return null;
}
return new Vue({
el,
render(createElement) {
return createElement(ProgressBar, {
props: { steps: COMBINED_SIGNUP_FLOW_STEPS, currentStep: STEPS.yourProject },
});
},
});
}
import { initTooltips, add } from '~/tooltips';
export default function showTooltip(tooltipSelector) {
const tooltip = document.querySelector(tooltipSelector);
if (!tooltip) return null;
initTooltips({ selector: tooltipSelector });
return add([tooltip], { show: true });
}
import Vue from 'vue'; import Vue from 'vue';
import 'ee/registrations/welcome/other_role'; import 'ee/registrations/welcome/other_role';
import 'ee/registrations/welcome/jobs_to_be_done'; import 'ee/registrations/welcome/jobs_to_be_done';
import { experiment } from '~/experimentation/utils';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import ProgressBar from '../components/progress_bar.vue'; import ProgressBar from '../components/progress_bar.vue';
import { STEPS, SUBSCRIPTON_FLOW_STEPS, SIGNUP_ONBOARDING_FLOW_STEPS } from '../constants'; import {
STEPS,
SUBSCRIPTON_FLOW_STEPS,
SIGNUP_ONBOARDING_FLOW_STEPS,
COMBINED_SIGNUP_FLOW_STEPS,
} from '../constants';
export default () => { export default () => {
const el = document.getElementById('progress-bar'); const el = document.getElementById('progress-bar');
...@@ -18,7 +24,14 @@ export default () => { ...@@ -18,7 +24,14 @@ export default () => {
if (isInSubscriptionFlow) { if (isInSubscriptionFlow) {
steps = SUBSCRIPTON_FLOW_STEPS; steps = SUBSCRIPTON_FLOW_STEPS;
} else if (isSignupOnboardingEnabled) { } else if (isSignupOnboardingEnabled) {
steps = SIGNUP_ONBOARDING_FLOW_STEPS; experiment('combined_registration', {
use: () => {
steps = SIGNUP_ONBOARDING_FLOW_STEPS;
},
try: () => {
steps = COMBINED_SIGNUP_FLOW_STEPS;
},
});
} }
return new Vue({ return new Vue({
......
# frozen_string_literal: true
module Registrations::CreateGroup
extend ActiveSupport::Concern
included do
before_action :check_if_gl_com_or_dev
before_action :authorize_create_group!, only: :new
protected
def show_confirm_warning?
false
end
private
def authorize_create_group!
access_denied! unless can?(current_user, :create_group)
end
def group_params
params.require(:group).permit(:name, :path, :visibility_level)
end
def learn_gitlab_context
strong_memoize(:learn_gitlab_context) do
in_experiment_group_a = Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user)
in_experiment_group_b = !in_experiment_group_a && Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
{ in_experiment_group_a: in_experiment_group_a, in_experiment_group_b: in_experiment_group_b }
end
end
end
end
# frozen_string_literal: true
module Registrations::CreateProject
extend ActiveSupport::Concern
include LearnGitlabHelper
LEARN_GITLAB_TEMPLATE = 'learn_gitlab.tar.gz'
LEARN_GITLAB_ULTIMATE_TEMPLATE = 'learn_gitlab_ultimate_trial.tar.gz'
included do
private
def learn_gitlab_experiment_enabled?
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
end
def learn_gitlab_template_path
file = if helpers.in_trial_onboarding_flow?
LEARN_GITLAB_ULTIMATE_TEMPLATE
else
LEARN_GITLAB_TEMPLATE
end
Rails.root.join('vendor', 'project_templates', file)
end
def create_learn_gitlab_project
File.open(learn_gitlab_template_path) do |archive|
::Projects::GitlabProjectsImportService.new(
current_user,
namespace_id: @project.namespace_id,
file: archive,
name: learn_gitlab_project_name
).execute
end
end
def learn_gitlab_project_name
helpers.in_trial_onboarding_flow? ? s_('Learn GitLab - Ultimate trial') : s_('Learn GitLab')
end
def project_params
params.require(:project).permit(project_params_attributes)
end
def project_params_attributes
[
:namespace_id,
:name,
:path,
:visibility_level
]
end
end
end
...@@ -17,6 +17,7 @@ module EE ...@@ -17,6 +17,7 @@ module EE
] ]
before_action only: :show do before_action only: :show do
publish_combined_registration_experiment
experiment(:trial_registration_with_reassurance, actor: current_user) experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:welcome:show', user: current_user) .track(:render, label: 'registrations:welcome:show', user: current_user)
end end
...@@ -84,6 +85,10 @@ module EE ...@@ -84,6 +85,10 @@ module EE
::Project.find(params[:learn_gitlab_project_id]) ::Project.find(params[:learn_gitlab_project_id])
end end
end end
def publish_combined_registration_experiment
experiment(:combined_registration, user: current_user).publish_to_client if show_signup_onboarding?
end
end end
end end
end end
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
module Registrations module Registrations
class GroupsController < ApplicationController class GroupsController < ApplicationController
layout 'minimal' include Registrations::CreateGroup
include ::Gitlab::Utils::StrongMemoize
before_action :check_if_gl_com_or_dev layout 'minimal'
before_action :authorize_create_group!, only: :new
feature_category :onboarding feature_category :onboarding
...@@ -13,6 +13,7 @@ module Registrations ...@@ -13,6 +13,7 @@ module Registrations
experiment(:trial_registration_with_reassurance, actor: current_user) experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:groups:new', user: current_user) .track(:render, label: 'registrations:groups:new', user: current_user)
@group = Group.new(visibility_level: helpers.default_group_visibility) @group = Group.new(visibility_level: helpers.default_group_visibility)
experiment(:combined_registration, user: current_user).track(:view_new_group_action)
end end
def create def create
...@@ -22,6 +23,8 @@ module Registrations ...@@ -22,6 +23,8 @@ module Registrations
experiment(:jobs_to_be_done, user: current_user) experiment(:jobs_to_be_done, user: current_user)
.track(:create_group, namespace: @group) .track(:create_group, namespace: @group)
experiment(:combined_registration, user: current_user).track(:create_group, namespace: @group)
force_company_trial_experiment.track(:create_group, namespace: @group, user: current_user) force_company_trial_experiment.track(:create_group, namespace: @group, user: current_user)
create_successful_flow create_successful_flow
...@@ -30,12 +33,6 @@ module Registrations ...@@ -30,12 +33,6 @@ module Registrations
end end
end end
protected
def show_confirm_warning?
false
end
private private
def force_company_trial_experiment def force_company_trial_experiment
...@@ -51,14 +48,6 @@ module Registrations ...@@ -51,14 +48,6 @@ module Registrations
end end
end end
def authorize_create_group!
access_denied! unless can?(current_user, :create_group)
end
def group_params
params.require(:group).permit(:name, :path, :visibility_level)
end
def apply_trial_for_trial_onboarding_flow def apply_trial_for_trial_onboarding_flow
if apply_trial if apply_trial
record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id) record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id)
......
# frozen_string_literal: true
module Registrations
class GroupsProjectsController < ApplicationController
include Registrations::CreateProject
include Registrations::CreateGroup
layout 'minimal'
feature_category :onboarding
def new
@group = Group.new(visibility_level: helpers.default_group_visibility)
@project = Project.new(namespace: @group)
combined_registration_experiment.track(:view_new_group_action)
experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:groups:new', user: current_user)
end
def create
@group = if group_id = params[:group][:id]
Group.find_by_id(group_id)
else
Groups::CreateService.new(current_user, group_params).execute
end
if @group.persisted?
if @group.previously_new_record?
combined_registration_experiment.track(:create_group, namespace: @group)
experiment(:jobs_to_be_done, user: current_user).track(:create_group, namespace: @group)
end
@project = ::Projects::CreateService.new(current_user, project_params).execute
if @project.saved?
combined_registration_experiment.track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project
experiment(:jobs_to_be_done, user: current_user)
.track(:create_project, project: @project)
if helpers.in_trial_onboarding_flow?
record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id)
record_experiment_conversion_event(:remove_known_trial_form_fields_welcoming)
redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id)
else
success_url = current_user.setup_for_company ? new_trial_path : nil
redirect_to success_url || continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: @project.id)
end
else
render :new
end
else
@project = Project.new(project_params) # #new requires a Project
render :new
end
end
def import
@group = Groups::CreateService.new(current_user, group_params).execute
if @group.persisted?
combined_registration_experiment.track(:create_group, namespace: @group)
experiment(:jobs_to_be_done, user: current_user).track(:create_group, namespace: @group)
import_url = URI.join(root_url, params[:import_url], "?namespace_id=#{@group.id}").to_s
redirect_to import_url
else
@project = Project.new(namespace: @group) # #new requires a Project
render :new
end
end
private
def combined_registration_experiment
@combined_registration_experiment ||= experiment(:combined_registration, user: current_user)
end
def project_params
params.require(:project).permit(project_params_attributes).merge(namespace_id: @group.id)
end
end
end
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
module Registrations module Registrations
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
include LearnGitlabHelper include Registrations::CreateProject
layout 'minimal' layout 'minimal'
LEARN_GITLAB_TEMPLATE = 'learn_gitlab.tar.gz'
LEARN_GITLAB_ULTIMATE_TEMPLATE = 'learn_gitlab_ultimate_trial.tar.gz'
before_action :check_if_gl_com_or_dev before_action :check_if_gl_com_or_dev
before_action only: [:new] do before_action only: [:new] do
set_namespace set_namespace
authorize_create_project! authorize_create_project!
...@@ -24,6 +22,8 @@ module Registrations ...@@ -24,6 +22,8 @@ module Registrations
@project = ::Projects::CreateService.new(current_user, project_params).execute @project = ::Projects::CreateService.new(current_user, project_params).execute
if @project.saved? if @project.saved?
experiment(:combined_registration, user: current_user).track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project learn_gitlab_project = create_learn_gitlab_project
experiment(:jobs_to_be_done, user: current_user) experiment(:jobs_to_be_done, user: current_user)
...@@ -44,17 +44,6 @@ module Registrations ...@@ -44,17 +44,6 @@ module Registrations
private private
def create_learn_gitlab_project
File.open(learn_gitlab_template_path) do |archive|
::Projects::GitlabProjectsImportService.new(
current_user,
namespace_id: @project.namespace_id,
file: archive,
name: learn_gitlab_project_name
).execute
end
end
def authorize_create_project! def authorize_create_project!
access_denied! unless can?(current_user, :create_projects, @namespace) access_denied! unless can?(current_user, :create_projects, @namespace)
end end
...@@ -62,32 +51,5 @@ module Registrations ...@@ -62,32 +51,5 @@ module Registrations
def set_namespace def set_namespace
@namespace = Namespace.find_by_id(params[:namespace_id]) @namespace = Namespace.find_by_id(params[:namespace_id])
end end
def project_params
params.require(:project).permit(project_params_attributes)
end
def project_params_attributes
[
:namespace_id,
:name,
:path,
:visibility_level
]
end
def learn_gitlab_project_name
helpers.in_trial_onboarding_flow? ? s_('Learn GitLab - Ultimate trial') : s_('Learn GitLab')
end
def learn_gitlab_template_path
file = if helpers.in_trial_onboarding_flow?
LEARN_GITLAB_ULTIMATE_TEMPLATE
else
LEARN_GITLAB_TEMPLATE
end
Rails.root.join('vendor', 'project_templates', file)
end
end end
end end
...@@ -52,6 +52,8 @@ class TrialsController < ApplicationController ...@@ -52,6 +52,8 @@ class TrialsController < ApplicationController
.track(:apply_trial, label: 'trials:apply', namespace: @namespace, user: current_user) .track(:apply_trial, label: 'trials:apply', namespace: @namespace, user: current_user)
experiment(:force_company_trial, user: current_user).track(:create_trial, namespace: @namespace, user: current_user, label: 'trials_controller') if @namespace.created_at > 24.hours.ago experiment(:force_company_trial, user: current_user).track(:create_trial, namespace: @namespace, user: current_user, label: 'trials_controller') if @namespace.created_at > 24.hours.ago
experiment(:combined_registration, user: current_user).track(:create_trial)
if discover_group_security_flow? if discover_group_security_flow?
redirect_trial_user_to_feature_experiment_flow redirect_trial_user_to_feature_experiment_flow
else else
......
- @html_class = "subscriptions-layout-html"
- page_title _('Your GitLab group')
- form_params = { trial_onboarding_flow: params[:trial_onboarding_flow], glm_source: params[:glm_source], glm_content: params[:glm_content] }
- if in_trial_onboarding_flow?
.row
.gl-display-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-mt-3
= render 'registrations/trial_is_activated_banner'
.row.gl-flex-grow-1
.gl-display-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-px-5.gl-pb-5
.new-project.gl-display-flex.gl-flex-direction-column.gl-align-items-center
- unless in_trial_onboarding_flow?
#progress-bar
%h2.gl-text-center= _('Create or import your first project')
%p.gl-text-center= _('Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more.')
.js-toggle-container.gl-w-full
%ul.nav.nav-tabs.nav-links.gitlab-tabs.js-group-project-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a#blank-project-tab.nav-link.active{ href: '#blank-project-pane', data: { toggle: 'tab', track_label: 'blank_project', track_event: 'click_tab', track_value: '' }, role: 'tab' }
%span= s_('ProjectsNew|Create')
%li.nav-item{ role: 'presentation' }
%a#import-project-tab.nav-link{ href: '#import-project-pane', data: { toggle: 'tab', track_label: 'import_project', track_event: 'click_tab', track_value: '' }, role: 'tab' }
%span= s_('ProjectsNew|Import')
.tab-content.gitlab-tab-content.gl-bg-white.js-group-project-tab-contents
#blank-project-pane.tab-pane.js-toggle-container.active{ role: 'tabpanel' }
= form_tag users_sign_up_groups_projects_path(form_params), class: 'gl-show-field-errors gl-w-full gl-p-4' do
= form_errors(@group, type: "Group")
= form_errors(@project, type: "Project")
= render 'layouts/flash'
= fields_for :group do |gf|
.row
.form-group.group-name-holder.col-sm-12
= gf.label :name, class: 'gl-font-weight-bold' do
= _('Group name')
- if @group.persisted?
= gf.text_field :name, class: 'form-control js-group-path-source',
disabled: true
= gf.hidden_field :id
- else
= gf.text_field :name, class: 'form-control js-validate-group-path js-autofill-group-name js-group-name-tooltip js-group-name-field',
required: true,
autofocus: true,
data: { title: _('Projects are organized into groups'), placement: 'right', show: true }
= gf.hidden_field :path, class: 'form-control js-autofill-group-path js-group-path-source'
= gf.hidden_field :parent_id, id: 'group_parent_id'
= fields_for :project do |pf|
#blank-project-name.row
.form-group.project-name.col-sm-12
= pf.label :name, class: 'gl-font-weight-bold' do
%span= _('Project name')
= pf.text_field :name, id: 'blank_project_name', class: 'form-control js-project-path-source', required: true, data: { track_label: 'blank_project', track_event: 'activate_form_input', track_property: 'project_name', track_value: '' }
%p.form-text.gl-text-center
= _('Your project will be created at:')
%p.form-text.gl-text-center.monospace
= root_url
%span.js-group-path-display>= _('{group}')
%span>= _('/')
%span.js-project-path-display>= _('{project}')
%p.form-text.text-muted.gl-text-center{ class: 'gl-mb-5!' }
= _('You can always change your URL later')
= submit_tag _('Create project'), class: 'btn gl-button btn-success btn-block', data: { track_label: 'blank_project', track_event: 'click_button', track_property: 'create_project', track_value: '' }
#import-project-pane.tab-pane.import-project-pane.js-toggle-container{ role: 'tabpanel' }
- if import_sources_enabled?
= form_tag import_users_sign_up_groups_projects_path, class: 'gl-show-field-errors gl-w-full gl-p-4 js-import-project-form' do
= form_errors(@group, type: "Group")
= render 'layouts/flash'
= fields_for :group do |gf|
.row
.form-group.group-name-holder.col-sm-12
= gf.label :name, class: 'gl-font-weight-bold' do
= _('Group name')
= gf.text_field :name, id: 'import_group_name', class: 'form-control js-validate-group-path js-autofill-group-name js-group-name-field has-tooltip',
required: true,
data: { title: _('Projects are organized into groups'), placement: 'right' }
= gf.hidden_field :path, id: 'import_group_path', class: 'form-control js-autofill-group-path js-import-group-path-source'
= hidden_field_tag :import_url, nil, class: 'js-import-url'
= submit_tag nil, class: 'gl-display-none'
%p.form-text.gl-text-center
= _('Your project will be created at:')
%p.form-text.gl-text-center.monospace
= root_url
%span.js-import-group-path-display>= _('{group}')
%span>= _('/')
%span>= _('{project}')
%p.form-text.text-muted.gl-text-center{ class: 'gl-mb-5!' }
= _('You can always change your URL later')
.js-import-project-buttons
= render 'projects/import_project_pane'
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
...@@ -3,91 +3,12 @@ ...@@ -3,91 +3,12 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Registrations::GroupsController do RSpec.describe Registrations::GroupsController do
using RSpec::Parameterized::TableSyntax describe 'GET #new' do
it_behaves_like "Registrations::GroupsController GET #new"
let_it_be(:user) { create(:user) }
shared_examples 'hides email confirmation warning' do
RSpec::Matchers.define :set_confirm_warning_for do |email|
match do |response|
expect(controller).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
context 'with an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') }
it { is_expected.not_to set_confirm_warning_for(user.unconfirmed_email) }
end
context 'without an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil) }
it { is_expected.not_to set_confirm_warning_for(user.email) }
end
end
describe 'GET #new', :aggregate_failures do
let(:dev_env_or_com) { true }
subject(:get_new) { get :new }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
context 'when on .com' do
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
it 'assigns the group variable to a new Group with the default group visibility' do
get_new
expect(assigns(:group)).to be_a_new(Group)
expect(assigns(:group).visibility_level).to eq(Gitlab::CurrentSettings.default_group_visibility)
end
context 'when the trial_registration_with_reassurance experiment is active', :experiment do
before do
stub_experiments(trial_registration_with_reassurance: :control)
end
it 'tracks a "render" event' do
expect(experiment(:trial_registration_with_reassurance)).to track(
:render,
user: user,
label: 'registrations:groups:new'
).with_context(actor: user).on_next_instance
get_new
end
end
context 'user without the ability to create a group' do
let(:user) { create(:user, can_create_group: false) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
context 'when not on .com' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'hides email confirmation warning'
end
end end
describe 'POST #create', :aggregate_failure do describe 'POST #create', :aggregate_failure do
let_it_be(:user) { create(:user) }
let_it_be(:glm_params) { {} } let_it_be(:glm_params) { {} }
let_it_be(:trial_form_params) { { trial: 'false' } } let_it_be(:trial_form_params) { { trial: 'false' } }
let_it_be(:trial_onboarding_flow_params) { {} } let_it_be(:trial_onboarding_flow_params) { {} }
...@@ -269,6 +190,11 @@ RSpec.describe Registrations::GroupsController do ...@@ -269,6 +190,11 @@ RSpec.describe Registrations::GroupsController do
post_create post_create
end end
it 'tracks for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:create_group, namespace: an_instance_of(Group)).on_next_instance
subject
end
end end
context 'when failing to create a lead and apply trial' do context 'when failing to create a lead and apply trial' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Registrations::GroupsProjectsController, :experiment do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
describe 'GET #new' do
it_behaves_like "Registrations::GroupsController GET #new"
context 'not shared behavior' do
subject { get :new }
before do
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(true)
sign_in(user)
end
it 'builds a project object' do
subject
expect(assigns(:project)).to be_a_new(Project)
end
it 'tracks an event for the combined_registration experiment' do
expect(experiment(:combined_registration)).to track(:view_new_group_action).on_next_instance
subject
end
end
end
describe 'POST #create' do
subject { post :create, params: params }
let(:params) { { group: group_params, project: project_params }.merge(extra_params) }
let(:extra_params) { {} }
let(:group_params) { { name: 'Group name', path: 'group-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE, emails: ['', ''] } }
let(:project_params) { { name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
it_behaves_like 'hides email confirmation warning'
context 'when group and project can be created' do
context 'when in the in_trial_onboarding_flow' do
let(:extra_params) { { trial_onboarding_flow: true } }
it 'tracks events for the remove_known_trial_form_fields_welcoming experiment' do
expect(controller).to receive(:record_experiment_user).with(:remove_known_trial_form_fields_welcoming, namespace_id: anything)
expect(controller).to receive(:record_experiment_conversion_event).with(:remove_known_trial_form_fields_welcoming)
subject
end
end
it 'creates a group' do
expect { subject }.to change { Group.count }.by(1)
end
it 'tracks an event for the jobs_to_be_done experiment' do
stub_experiments(jobs_to_be_done: :candidate)
expect(experiment(:jobs_to_be_done)).to track(:create_group, namespace: an_instance_of(Group))
.on_next_instance
.for(:candidate)
.with_context(user: user)
subject
end
it 'tracks create events for the combined_registration experiment' do
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:after_create_actions)
end
wrapped_experiment(experiment(:combined_registration)) do |e|
expect(e).to receive(:track).with(:create_group, namespace: an_instance_of(Group))
expect(e).to receive(:track).with(:create_project, namespace: an_instance_of(Group))
end
subject
end
end
context 'when the group cannot be created' do
let(:group_params) { { name: '', path: '' } }
it 'does not create a group', :aggregate_failures do
expect { subject }.not_to change { Group.count }
expect(assigns(:group).errors).not_to be_blank
end
it 'does not tracks events for the combined_registration experiment' do
wrapped_experiment(experiment(:combined_registration)) do |e|
expect(e).not_to receive(:track).with(:create_group)
expect(e).not_to receive(:track).with(:create_project)
end
subject
end
it 'the project is not disgarded completely' do
subject
expect(assigns(:project).name).to eq('New project')
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context "when group can be created but the project can't" do
let(:project_params) { { name: '', path: '', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
it 'does not create a project', :aggregate_failures do
expect { subject }.to change { Group.count }
expect { subject }.not_to change { Project.count }
expect(assigns(:project).errors).not_to be_blank
end
it 'selectively tracks events for the combined_registration experiment' do
wrapped_experiment(experiment(:combined_registration)) do |e|
expect(e).to receive(:track).with(:create_group, namespace: an_instance_of(Group))
expect(e).not_to receive(:track).with(:create_project)
end
subject
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context "when a group is already created but a project isn't" do
before do
group.add_owner(user)
end
let(:group_params) { { id: group.id } }
it 'creates a project and not another group', :aggregate_failures do
expect { subject }.to change { Project.count }
expect { subject }.not_to change { Group.count }
end
it 'selectively tracks events for the combined_registration experiment' do
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:after_create_actions)
end
wrapped_experiment(experiment(:combined_registration)) do |e|
expect(e).not_to receive(:track).with(:create_group, namespace: an_instance_of(Group))
expect(e).to receive(:track).with(:create_project, namespace: an_instance_of(Group))
end
subject
end
context 'it redirects' do
let_it_be(:project) { create(:project) }
before do
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(project)
end
end
it { is_expected.to redirect_to(continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: project.id)) }
end
end
end
shared_context 'groups_projects projects concern' do
let_it_be(:project) { create(:project) }
let_it_be(:namespace) { create(:group) }
let(:group_params) { { name: 'Group name', path: 'group-path', visibility_level: "#{Gitlab::VisibilityLevel::PRIVATE}" } }
let(:extra_params) { { group: group_params } }
let(:params) { { name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:create_service) { double(:create_service) }
before do
allow(controller).to receive(:record_experiment_user).and_call_original
allow(controller).to receive(:record_experiment_conversion_event).and_call_original
allow(Groups::CreateService).to receive(:new).and_call_original
allow(Groups::CreateService).to receive(:new).with(user, ActionController::Parameters.new(group_params).permit!).and_return(create_service)
allow(create_service).to receive(:execute).and_return(namespace)
end
end
it_behaves_like "Registrations::ProjectsController POST #create" do
include_context 'groups_projects projects concern'
end
context 'when the user is setup_for_company: true it redirects to the new_trial_path' do
it_behaves_like "Registrations::ProjectsController POST #create" do
let_it_be(:user) { create(:user, setup_for_company: true) }
let(:success_path) { new_trial_path }
include_context 'groups_projects projects concern'
end
end
end
describe 'POST #import' do
subject { post :import, params: params }
let(:params) { { group: group_params, import_url: new_import_github_path } }
let(:group_params) { { name: 'Group name', path: 'group-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE, emails: ['', ''] } }
let(:dev_env_or_com) { true }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
it_behaves_like 'hides email confirmation warning'
context "when a group can't be created" do
before do
allow_next_instance_of(::Groups::CreateService) do |service|
allow(service).to receive(:execute).and_return(Group.new)
end
end
it "doesn't track for the combined_registration experiment" do
expect(experiment(:combined_registration)).not_to track(:create_group)
subject
end
it { is_expected.to render_template(:new) }
end
context 'when group can be created' do
it 'creates a group' do
expect { subject }.to change { Group.count }.by(1)
end
it 'tracks an event for the jobs_to_be_done experiment' do
stub_experiments(jobs_to_be_done: :candidate)
expect(experiment(:jobs_to_be_done)).to track(:create_group, namespace: an_instance_of(Group))
.on_next_instance
.for(:candidate)
.with_context(user: user)
subject
end
it 'tracks an event for the combined_registration experiment' do
expect(experiment(:combined_registration)).to track(:create_group, namespace: an_instance_of(Group))
.on_next_instance
subject
end
it 'redirects to the import url with a namespace_id parameter' do
allow_next_instance_of(::Groups::CreateService) do |service|
allow(service).to receive(:execute).and_return(group)
end
expect(subject).to redirect_to(new_import_github_url(namespace_id: group.id))
end
end
end
end
end
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Registrations::ProjectsController do RSpec.describe Registrations::ProjectsController do
include AfterNextHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) } let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
...@@ -53,48 +51,20 @@ RSpec.describe Registrations::ProjectsController do ...@@ -53,48 +51,20 @@ RSpec.describe Registrations::ProjectsController do
end end
describe 'POST #create' do describe 'POST #create' do
subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params) } it_behaves_like "Registrations::ProjectsController POST #create"
let_it_be(:trial_onboarding_flow_params) { {} }
let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
context 'with an unauthenticated user' do context 'force_company_trial_experiment' do
it { is_expected.to have_gitlab_http_status(:redirect) } let(:project) { create(:project, namespace: namespace) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user', :sidekiq_inline do let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let_it_be(:first_project) { create(:project) }
before do before do
namespace.add_owner(user) namespace.add_owner(user)
sign_in(user) sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com) allow(::Gitlab).to receive(:dev_env_or_com?).and_return(true)
end
it 'creates a new project, a "Learn GitLab" project, sets a cookie and redirects to the continuous onboarding page' do
allow_next_instance_of(::Projects::CreateService) do |service| allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(first_project)
end
allow_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
allow(service).to receive(:execute).and_return(project) allow(service).to receive(:execute).and_return(project)
end end
expect(subject).to have_gitlab_http_status(:redirect)
expect(subject).to redirect_to(continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: first_project.id))
end
it 'tracks an event for the jobs_to_be_done experiment', :experiment do
stub_experiments(jobs_to_be_done: :candidate)
expect(experiment(:jobs_to_be_done)).to track(:create_project, project: an_instance_of(Project))
.on_next_instance
.for(:candidate)
.with_context(user: user)
subject
end end
it 'tracks an event for the force_company_trial experiment', :experiment do it 'tracks an event for the force_company_trial experiment', :experiment do
...@@ -102,72 +72,7 @@ RSpec.describe Registrations::ProjectsController do ...@@ -102,72 +72,7 @@ RSpec.describe Registrations::ProjectsController do
.with_context(user: user) .with_context(user: user)
.on_next_instance .on_next_instance
subject post :create, params: { project: params }
end
context 'learn gitlab project' do
using RSpec::Parameterized::TableSyntax
where(:trial, :project_name, :template) do
false | 'Learn GitLab' | described_class::LEARN_GITLAB_TEMPLATE
true | 'Learn GitLab - Ultimate trial' | described_class::LEARN_GITLAB_ULTIMATE_TEMPLATE
end
with_them do
let(:path) { Rails.root.join('vendor', 'project_templates', template) }
let(:expected_arguments) { { namespace_id: namespace.id, file: handle, name: project_name } }
let(:handle) { double }
let(:trial_onboarding_flow_params) { { trial_onboarding_flow: trial } }
before do
allow(File).to receive(:open).and_call_original
expect(File).to receive(:open).with(path).and_yield(handle)
end
specify do
expect_next(::Projects::GitlabProjectsImportService, user, expected_arguments)
.to receive(:execute).and_return(project)
subject
end
end
end
context 'when the trial onboarding is active' do
let_it_be(:trial_onboarding_flow_params) { { trial_onboarding_flow: true } }
it 'creates a new project, a "Learn GitLab - Ultimate trial" project, does not set a cookie' do
expect { subject }.to change { namespace.projects.pluck(:name) }.from([]).to(['New project', s_('Learn GitLab - Ultimate trial')])
expect(subject).to have_gitlab_http_status(:redirect)
expect(namespace.projects.find_by_name(s_('Learn GitLab - Ultimate trial'))).to be_import_finished
end
it 'records context and redirects to the trial getting started page' do
expect_next_instance_of(::Projects::CreateService) do |service|
expect(service).to receive(:execute).and_return(first_project)
end
expect_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
expect(service).to receive(:execute).and_return(project)
end
expect(subject).to redirect_to(trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: project.id))
end
end
context 'when the project cannot be saved' do
let(:params) { { name: '', path: '' } }
it 'does not create a project' do
expect { subject }.not_to change { Project.count }
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context 'with signup onboarding not enabled' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end end
end end
end end
......
...@@ -8,21 +8,15 @@ RSpec.describe Registrations::WelcomeController do ...@@ -8,21 +8,15 @@ RSpec.describe Registrations::WelcomeController do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
describe '#show' do describe '#show' do
context 'when the trial_registration_with_reassurance experiment is active', :experiment do it 'publishes combined_registration experiment data to the client' do
before do sign_in(user)
sign_in(user) allow(controller.helpers).to receive(:signup_onboarding_enabled?).and_return(true)
stub_experiments(trial_registration_with_reassurance: :control)
end
it 'tracks a "render" event' do wrapped_experiment(experiment(:combined_registration)) do |e|
expect(experiment(:trial_registration_with_reassurance)).to track( expect(e).to receive(:publish_to_client)
:render,
user: user,
label: 'registrations:welcome:show'
).with_context(actor: user).on_next_instance
get :show
end end
get :show
end end
end end
...@@ -248,6 +242,14 @@ RSpec.describe Registrations::WelcomeController do ...@@ -248,6 +242,14 @@ RSpec.describe Registrations::WelcomeController do
allow(controller.helpers).to receive(:signup_onboarding_enabled?).and_return(true) allow(controller.helpers).to receive(:signup_onboarding_enabled?).and_return(true)
end end
context 'when combined_registration is candidate variant' do
before do
stub_experiments(combined_registration: :candidate)
end
it { is_expected.to redirect_to new_users_sign_up_groups_project_path }
end
context 'and force_company_trial experiment is candidate' do context 'and force_company_trial experiment is candidate' do
let(:setup_for_company) { 'true' } let(:setup_for_company) { 'true' }
......
...@@ -236,6 +236,12 @@ RSpec.describe TrialsController do ...@@ -236,6 +236,12 @@ RSpec.describe TrialsController do
end end
end end
it 'calls tracking event for combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:create_trial).on_next_instance
subject
end
context 'redirect trial user to feature' do context 'redirect trial user to feature' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -276,6 +282,8 @@ RSpec.describe TrialsController do ...@@ -276,6 +282,8 @@ RSpec.describe TrialsController do
context 'with an old namespace' do context 'with an old namespace' do
it 'does not track for the force_company_trial experiment' do it 'does not track for the force_company_trial experiment' do
allow(controller).to receive(:experiment).and_call_original
namespace.update!(created_at: 2.days.ago) namespace.update!(created_at: 2.days.ago)
expect(controller).not_to receive(:experiment).with(:force_company_trial, user: user) expect(controller).not_to receive(:experiment).with(:force_company_trial, user: user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CombinedRegistrationExperiment, :experiment do
subject { described_class.new(user: user) }
let_it_be(:user) { create(:user) }
describe '#redirect_path' do
it 'when control passes trial_params to path' do
stub_experiments(combined_registration: :control)
expect(subject.redirect_path(trial: true)).to eq(Rails.application.routes.url_helpers.new_users_sign_up_group_path(trial: true))
end
it 'when candidate returns path' do
stub_experiments(combined_registration: :candidate)
expect(subject.redirect_path(trial: true)).to eq(Rails.application.routes.url_helpers.new_users_sign_up_groups_project_path)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Combined registration flow', :js do
let_it_be(:user) { create(:user) }
let(:experiments) { {} }
before do
# https://gitlab.com/gitlab-org/gitlab/-/issues/338737
stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 250)
stub_experiments(experiments)
allow(Gitlab).to receive(:com?).and_return(true)
sign_in(user)
visit users_sign_up_welcome_path
expect(page).to have_content('Welcome to GitLab')
page.within('.bar') do
expect(page).to have_content('Your profile')
expect(page).to have_content('Your first project')
expect(page).not_to have_content('Your GitLab group')
end
choose 'My company or team'
click_on 'Continue'
end
context 'when combined_registration experiment variant is candidate' do
let(:experiments) { { combined_registration: :candidate } }
it 'A user can create a group and project' do
expect(page).to have_content('Your first project')
page.within '.js-group-path-display' do
expect(page).to have_content('{group}')
end
page.within '.js-project-path-display' do
expect(page).to have_content('{project}')
end
fill_in 'group_name', with: 'test group'
fill_in 'blank_project_name', with: 'test project'
page.within '.js-group-path-display' do
expect(page).to have_content('test-group')
end
page.within '.js-project-path-display' do
expect(page).to have_content('test-project')
end
click_on 'Create project'
expect(page).to have_content('Start your Free Ultimate Trial')
end
it 'a user can create a group and import a project' do
expect(page).to have_content('Your first project')
click_on 'Import'
page.within '.js-import-group-path-display' do
expect(page).to have_content('{group}')
end
click_on 'GitHub'
page.within('.gl-field-error') do
expect(page).to have_content('This field is required.')
end
fill_in 'import_group_name', with: 'test group'
page.within '.js-import-group-path-display' do
expect(page).to have_content('test-group')
end
click_on 'GitHub'
expect(page).to have_content('To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories.')
end
end
end
import mountComponents from 'ee/registrations/groups_projects/new';
describe('importButtonsSubmit', () => {
const fixture = `
<div class="js-import-project-buttons">
<a href="/import/github">github</a>
</div>
<div class="js-import-project-form">
<input type="hidden" class="js-import-url" />
<input type="submit" />
</form>
`;
beforeEach(() => {
setFixtures(fixture);
mountComponents();
});
const findSubmit = () => document.querySelector('.js-import-project-form input[type="submit"]');
const findImportUrlValue = () => document.querySelector('.js-import-url').value;
const findImportGithubButton = () => document.querySelector('.js-import-project-buttons a');
it('sets the import-url field with the value of the href and clicks submit', () => {
const submitSpy = jest.spyOn(findSubmit(), 'click');
findImportGithubButton().click();
expect(findImportUrlValue()).toBe('/import/github');
expect(submitSpy).toHaveBeenCalled();
});
});
import {
displayGroupPath,
displayProjectPath,
} from 'ee/registrations/groups_projects/new/path_display';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
const fixture = `<input type='text' class='source'><div class='display'>original value<div>`;
beforeEach(() => {
setFixtures(fixture);
});
const findSource = () => document.querySelector('.source');
const displayValue = () => document.querySelector('.display').textContent;
describe('displayGroupPath', () => {
const { trigger: triggerMutate } = useMockMutationObserver();
beforeEach(() => {
displayGroupPath('.source', '.display');
});
const inputSource = (value) => {
const source = findSource();
source.value = value;
triggerMutate(source, {
entry: { attributeName: 'value' },
options: { attributes: true },
});
};
it('coppies values from the source to the display', () => {
inputSource('peanut-butter-jelly-time');
expect(displayValue()).toBe('peanut-butter-jelly-time');
});
});
describe('displayProjectPath', () => {
beforeEach(() => {
displayProjectPath('.source', '.display');
});
const inputSource = (value) => {
const source = findSource();
source.value = value;
source.dispatchEvent(new Event('input'));
};
it('displays the default display value when source is empty', () => {
expect(displayValue()).toBe('original value');
inputSource('its a peanut butter jelly');
expect(displayValue()).not.toBe('original value');
inputSource('');
expect(displayValue()).toBe('original value');
});
it('sluggifies values from the source to the display', () => {
inputSource('peanut butter jelly time');
expect(displayValue()).toBe('peanut-butter-jelly-time');
});
});
import { nextTick } from 'vue';
import showTooltip from 'ee/registrations/groups_projects/new/show_tooltip';
const fixture = `<div class='my-tooltip' title='this is a tooltip!'></div>`;
beforeEach(() => {
setFixtures(fixture);
});
const findBodyText = () => document.body.innerText;
describe('showTooltip', () => {
it('renders a tooltip immediately', async () => {
expect(findBodyText()).toBe('');
showTooltip('.my-tooltip');
await nextTick();
expect(findBodyText()).toBe('this is a tooltip!');
});
});
# frozen_string_literal: true
RSpec.shared_examples 'hides email confirmation warning' do
RSpec::Matchers.define :set_confirm_warning_for do |email|
match do |response|
expect(controller).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
context 'with an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') }
it { is_expected.not_to set_confirm_warning_for(user.unconfirmed_email) }
end
context 'without an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil) }
it { is_expected.not_to set_confirm_warning_for(user.email) }
end
end
RSpec.shared_examples "Registrations::GroupsController GET #new" do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let(:dev_env_or_com) { true }
let(:learn_gitlab_context) do
{
in_experiment_group_a: false,
in_experiment_group_b: false
}
end
subject { get :new }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
context 'when on .com' do
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
it 'assigns the group variable to a new Group with the default group visibility', :aggregate_failures do
subject
expect(assigns(:group)).to be_a_new(Group)
expect(assigns(:group).visibility_level).to eq(Gitlab::CurrentSettings.default_group_visibility)
end
context 'when the trial_registration_with_reassurance experiment is active', :experiment do
before do
stub_experiments(trial_registration_with_reassurance: :control)
end
it 'tracks a "render" event' do
expect(experiment(:trial_registration_with_reassurance)).to track(
:render,
user: user,
label: 'registrations:groups:new'
).with_context(actor: user).on_next_instance
get :new
end
end
context 'user without the ability to create a group' do
let(:user) { create(:user, can_create_group: false) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it 'tracks an event for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:view_new_group_action).on_next_instance
subject
end
end
context 'when not on .com' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'hides email confirmation warning'
end
end
# frozen_string_literal: true
RSpec.shared_examples "Registrations::ProjectsController POST #create" do
include AfterNextHelpers
subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params).merge(extra_params) }
let_it_be(:trial_onboarding_flow_params) { {} }
let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
let(:extra_params) { {} }
let(:success_path) { nil }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user', :sidekiq_inline do
let_it_be(:first_project) { create(:project) }
before do
namespace.add_owner(user)
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
allow(controller).to receive(:experiment).and_call_original
end
it 'creates a new project, a "Learn GitLab" project, sets a cookie and redirects to the success_path' do
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(first_project)
end
allow_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
allow(service).to receive(:execute).and_return(project)
end
expect(subject).to have_gitlab_http_status(:redirect)
expect(subject).to redirect_to(success_path || continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: first_project.id))
end
context 'jobs_to_be_done experiment' do
let(:jobs_to_be_done_experiment) { experiment(:jobs_to_be_done_experiment) }
it 'tracks an event for the jobs_to_be_done experiment', :experiment do
allow(controller).to receive(:experiment).with(:jobs_to_be_done, user: user).and_return(jobs_to_be_done_experiment)
allow(jobs_to_be_done_experiment).to receive(:track).and_call_original
expect(jobs_to_be_done_experiment).to receive(:track).with(:create_project, project: an_instance_of(Project))
subject
end
end
context 'learn gitlab project' do
using RSpec::Parameterized::TableSyntax
where(:trial, :project_name, :template) do
false | 'Learn GitLab' | described_class::LEARN_GITLAB_TEMPLATE
true | 'Learn GitLab - Ultimate trial' | described_class::LEARN_GITLAB_ULTIMATE_TEMPLATE
end
with_them do
let(:path) { Rails.root.join('vendor', 'project_templates', template) }
let(:expected_arguments) { { namespace_id: namespace.id, file: handle, name: project_name } }
let(:handle) { double }
let(:trial_onboarding_flow_params) { { trial_onboarding_flow: trial } }
before do
allow(File).to receive(:open).and_call_original
expect(File).to receive(:open).with(path).and_yield(handle)
end
specify do
expect_next(::Projects::GitlabProjectsImportService, user, expected_arguments)
.to receive(:execute).and_return(project)
subject
end
end
end
context 'when the trial onboarding is active' do
let_it_be(:trial_onboarding_flow_params) { { trial_onboarding_flow: true } }
it 'creates a new project, a "Learn GitLab - Ultimate trial" project, does not set a cookie' do
expect { subject }.to change { namespace.projects.pluck(:name) }.from([]).to(['New project', s_('Learn GitLab - Ultimate trial')])
expect(subject).to have_gitlab_http_status(:redirect)
expect(namespace.projects.find_by_name(s_('Learn GitLab - Ultimate trial'))).to be_import_finished
end
it 'records context and redirects to the success page' do
expect_next_instance_of(::Projects::CreateService) do |service|
expect(service).to receive(:execute).and_return(first_project)
end
expect_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
expect(service).to receive(:execute).and_return(project)
end
expect(subject).to redirect_to(trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: project.id))
end
end
context 'when the project cannot be saved' do
let(:params) { { name: '', path: '' } }
it 'does not create a project' do
expect { subject }.not_to change { Project.count }
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context 'with signup onboarding not enabled' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
end
...@@ -1200,6 +1200,9 @@ msgstr "" ...@@ -1200,6 +1200,9 @@ msgstr ""
msgid "." msgid "."
msgstr "" msgstr ""
msgid "/"
msgstr ""
msgid "0 bytes" msgid "0 bytes"
msgstr "" msgstr ""
...@@ -9528,6 +9531,9 @@ msgstr "" ...@@ -9528,6 +9531,9 @@ msgstr ""
msgid "Create new..." msgid "Create new..."
msgstr "" msgstr ""
msgid "Create or import your first project"
msgstr ""
msgid "Create project" msgid "Create project"
msgstr "" msgstr ""
...@@ -26615,9 +26621,15 @@ msgstr "" ...@@ -26615,9 +26621,15 @@ msgstr ""
msgid "Projects are graded based on the highest severity vulnerability present" msgid "Projects are graded based on the highest severity vulnerability present"
msgstr "" msgstr ""
msgid "Projects are organized into groups"
msgstr ""
msgid "Projects contributed to" msgid "Projects contributed to"
msgstr "" msgstr ""
msgid "Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more."
msgstr ""
msgid "Projects shared with %{group_name}" msgid "Projects shared with %{group_name}"
msgstr "" msgstr ""
...@@ -38180,6 +38192,9 @@ msgstr "" ...@@ -38180,6 +38192,9 @@ msgstr ""
msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}" msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
msgstr "" msgstr ""
msgid "You can always change your URL later"
msgstr ""
msgid "You can always edit this later" msgid "You can always edit this later"
msgstr "" msgstr ""
...@@ -38879,6 +38894,9 @@ msgstr "" ...@@ -38879,6 +38894,9 @@ msgstr ""
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it" msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr "" msgstr ""
msgid "Your project will be created at:"
msgstr ""
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr ""
...@@ -40657,3 +40675,9 @@ msgstr "" ...@@ -40657,3 +40675,9 @@ msgstr ""
msgid "your settings" msgid "your settings"
msgstr "" msgstr ""
msgid "{group}"
msgstr ""
msgid "{project}"
msgstr ""
...@@ -346,6 +346,12 @@ RSpec.describe Projects::CreateService, '#execute' do ...@@ -346,6 +346,12 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(imported_project.import_data.data).to eq(import_data[:data]) expect(imported_project.import_data.data).to eq(import_data[:data])
expect(imported_project.import_url).to eq('http://import-url') expect(imported_project.import_url).to eq('http://import-url')
end end
it 'tracks for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:import_project).on_next_instance
imported_project
end
end end
context 'builds_enabled global setting' do context 'builds_enabled global setting' do
......
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