From 58b88f6b2747877de888943a2adbcbd34343ba78 Mon Sep 17 00:00:00 2001
From: Nicolas Dular <ndular@gitlab.com>
Date: Mon, 1 Feb 2021 10:35:25 +0100
Subject: [PATCH] First iteration of learn gitlab page experiment

This adds the route, controller and index page for the upcoming
experiment where we will render two different designs of a "Learn
GitLab" page and list the actions that are completed or show a link to
explain how to complete the action.
---
 .../components/learn_gitlab_a.vue             | 27 ++++++
 .../components/learn_gitlab_b.vue             | 27 ++++++
 .../projects/learn_gitlab/constants/index.js  | 12 +++
 .../projects/learn_gitlab/index/index.js      | 25 +++++
 .../projects/learn_gitlab_controller.rb       | 19 ++++
 app/helpers/learn_gitlab_helper.rb            | 60 ++++++++++++
 app/helpers/projects_helper.rb                |  2 +
 app/models/onboarding_progress.rb             |  4 +
 .../layouts/nav/sidebar/_project.html.haml    |  7 ++
 .../projects/learn_gitlab/index.html.haml     |  4 +
 .../learn_gitlab_a_experiment_percentage.yml  |  8 ++
 .../learn_gitlab_b_experiment_percentage.yml  |  8 ++
 config/routes/project.rb                      |  2 +
 lib/gitlab/experimentation.rb                 |  6 ++
 locale/gitlab.pot                             | 24 +++++
 .../projects/learn_gitlab_controller_spec.rb  | 44 +++++++++
 .../__snapshots__/learn_gitlab_a_spec.js.snap | 66 ++++++++++++++
 .../__snapshots__/learn_gitlab_b_spec.js.snap | 66 ++++++++++++++
 .../components/learn_gitlab_a_spec.js         | 63 +++++++++++++
 .../components/learn_gitlab_b_spec.js         | 63 +++++++++++++
 spec/helpers/learn_gitlab_helper_spec.rb      | 91 +++++++++++++++++++
 spec/helpers/projects_helper_spec.rb          | 15 +++
 spec/models/onboarding_progress_spec.rb       | 16 ++++
 23 files changed, 659 insertions(+)
 create mode 100644 app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
 create mode 100644 app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
 create mode 100644 app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
 create mode 100644 app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
 create mode 100644 app/controllers/projects/learn_gitlab_controller.rb
 create mode 100644 app/helpers/learn_gitlab_helper.rb
 create mode 100644 app/views/projects/learn_gitlab/index.html.haml
 create mode 100644 config/feature_flags/experiment/learn_gitlab_a_experiment_percentage.yml
 create mode 100644 config/feature_flags/experiment/learn_gitlab_b_experiment_percentage.yml
 create mode 100644 spec/controllers/projects/learn_gitlab_controller_spec.rb
 create mode 100644 spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
 create mode 100644 spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
 create mode 100644 spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
 create mode 100644 spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
 create mode 100644 spec/helpers/learn_gitlab_helper_spec.rb

diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { ACTION_TEXT } from '../constants';
+
+export default {
+  components: { GlLink },
+  i18n: {
+    ACTION_TEXT,
+  },
+  props: {
+    actions: {
+      required: true,
+      type: Object,
+    },
+  },
+};
+</script>
+<template>
+  <ul>
+    <li v-for="(value, action) in actions" :key="action">
+      <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
+      <span v-else>
+        <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
+      </span>
+    </li>
+  </ul>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { ACTION_TEXT } from '../constants';
+
+export default {
+  components: { GlLink },
+  i18n: {
+    ACTION_TEXT,
+  },
+  props: {
+    actions: {
+      required: true,
+      type: Object,
+    },
+  },
+};
+</script>
+<template>
+  <ul>
+    <li v-for="(value, action) in actions" :key="action">
+      <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
+      <span v-else>
+        <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
+      </span>
+    </li>
+  </ul>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
new file mode 100644
index 00000000000..8606af29785
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -0,0 +1,12 @@
+import { s__ } from '~/locale';
+
+export const ACTION_TEXT = {
+  gitWrite: s__('LearnGitLab|Create a repository'),
+  userAdded: s__('LearnGitLab|Invite your colleagues'),
+  pipelineCreated: s__('LearnGitLab|Set-up CI/CD'),
+  trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'),
+  codeOwnersEnabled: s__('LearnGitLab|Add code owners'),
+  requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'),
+  mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'),
+  securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'),
+};
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
new file mode 100644
index 00000000000..c4dec89b984
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import LearnGitlabA from '../components/learn_gitlab_a.vue';
+import LearnGitlabB from '../components/learn_gitlab_b.vue';
+
+function initLearnGitlab() {
+  const el = document.getElementById('js-learn-gitlab-app');
+
+  if (!el) {
+    return false;
+  }
+
+  const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
+
+  const { learnGitlabA } = gon.experiments;
+
+  return new Vue({
+    el,
+    render(createElement) {
+      return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
+    },
+  });
+}
+
+initLearnGitlab();
diff --git a/app/controllers/projects/learn_gitlab_controller.rb b/app/controllers/projects/learn_gitlab_controller.rb
new file mode 100644
index 00000000000..162ba9bd5cb
--- /dev/null
+++ b/app/controllers/projects/learn_gitlab_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Projects::LearnGitlabController < Projects::ApplicationController
+  before_action :authenticate_user!
+  before_action :check_experiment_enabled?
+
+  feature_category :users
+
+  def index
+    push_frontend_experiment(:learn_gitlab_a, subject: current_user)
+    push_frontend_experiment(:learn_gitlab_b, subject: current_user)
+  end
+
+  private
+
+  def check_experiment_enabled?
+    return access_denied! unless helpers.learn_gitlab_experiment_enabled?(project)
+  end
+end
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
new file mode 100644
index 00000000000..e72a9c83fc9
--- /dev/null
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module LearnGitlabHelper
+  def learn_gitlab_experiment_enabled?(project)
+    return false unless current_user
+    return false unless experiment_enabled_for_user?
+
+    learn_gitlab_onboarding_available?(project)
+  end
+
+  def onboarding_actions_data(project)
+    attributes = onboarding_progress(project).attributes.symbolize_keys
+
+    action_urls.map do |action, url|
+      [
+        action,
+        url: url,
+        completed: attributes[OnboardingProgress.column_name(action)].present?
+      ]
+    end.to_h
+  end
+
+  private
+
+  ACTION_ISSUE_IDS = {
+    git_write: 2,
+    pipeline_created: 4,
+    merge_request_created: 6,
+    user_added: 7,
+    trial_started: 13,
+    required_mr_approvals_enabled: 15,
+    code_owners_enabled: 16
+  }.freeze
+
+  ACTION_DOC_URLS = {
+    security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports'
+  }.freeze
+
+  def action_urls
+    ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS)
+  end
+
+  def learn_gitlab_project
+    @learn_gitlab_project ||= LearnGitlab.new(current_user).project
+  end
+
+  def onboarding_progress(project)
+    OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
+  end
+
+  def experiment_enabled_for_user?
+    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_onboarding_available?(project)
+    OnboardingProgress.onboarding?(project.namespace) &&
+      LearnGitlab.new(current_user).available?
+  end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a2e9952f350..f5cd89d96b4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -433,6 +433,8 @@ module ProjectsHelper
 
     nav_tabs += package_nav_tabs(project, current_user)
 
+    nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project)
+
     nav_tabs
   end
   # rubocop:enable Metrics/CyclomaticComplexity
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
index 38a9489a3ad..8a444f8934e 100644
--- a/app/models/onboarding_progress.rb
+++ b/app/models/onboarding_progress.rb
@@ -47,6 +47,10 @@ class OnboardingProgress < ApplicationRecord
       safe_find_or_create_by(namespace: namespace)
     end
 
+    def onboarding?(namespace)
+      where(namespace: namespace).any?
+    end
+
     def register(namespace, action)
       return unless root_namespace?(namespace) && ACTIONS.include?(action)
 
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 822fe3fea88..c8e9546ac85 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -33,6 +33,13 @@
               = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
                 %span= _('Releases')
 
+      - if project_nav_tab? :learn_gitlab
+        = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
+          = link_to project_learn_gitlab_path(@project) do
+            .nav-icon-container
+              = sprite_icon('home')
+            %span.nav-item-name
+              = _('Learn GitLab')
 
       - if project_nav_tab? :files
         = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml
new file mode 100644
index 00000000000..d5fdbc10eb4
--- /dev/null
+++ b/app/views/projects/learn_gitlab/index.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _("Learn GitLab")
+- page_title _("Learn GitLab")
+
+#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } }
diff --git a/config/feature_flags/experiment/learn_gitlab_a_experiment_percentage.yml b/config/feature_flags/experiment/learn_gitlab_a_experiment_percentage.yml
new file mode 100644
index 00000000000..54b7ea465f1
--- /dev/null
+++ b/config/feature_flags/experiment/learn_gitlab_a_experiment_percentage.yml
@@ -0,0 +1,8 @@
+---
+name: learn_gitlab_a_experiment_percentage
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281022
+milestone: '13.9'
+type: experiment
+group: group::conversion
+default_enabled: false
diff --git a/config/feature_flags/experiment/learn_gitlab_b_experiment_percentage.yml b/config/feature_flags/experiment/learn_gitlab_b_experiment_percentage.yml
new file mode 100644
index 00000000000..cca5d35baf3
--- /dev/null
+++ b/config/feature_flags/experiment/learn_gitlab_b_experiment_percentage.yml
@@ -0,0 +1,8 @@
+---
+name: learn_gitlab_b_experiment_percentage
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
+rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/306
+milestone: '13.9'
+type: experiment
+group: group::conversion
+default_enabled: false
diff --git a/config/routes/project.rb b/config/routes/project.rb
index e6df2532479..21dfe173715 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -87,6 +87,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
           end
         end
 
+        get :learn_gitlab, action: :index, controller: 'learn_gitlab'
+
         namespace :ci do
           resource :lint, only: [:show, :create]
           resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor'
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 0deda3596f5..423f238a0a2 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -95,6 +95,12 @@ module Gitlab
       trial_onboarding_issues: {
         tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues'
       },
+      learn_gitlab_a: {
+        tracking_category: 'Growth::Conversion::Experiment::LearnGitLabA'
+      },
+      learn_gitlab_b: {
+        tracking_category: 'Growth::Activation::Experiment::LearnGitLabB'
+      },
       in_product_marketing_emails: {
         tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails'
       }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3b948e7fdda..8528dbb3c36 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17386,6 +17386,30 @@ msgstr ""
 msgid "Learn more."
 msgstr ""
 
+msgid "LearnGitLab|Add code owners"
+msgstr ""
+
+msgid "LearnGitLab|Create a repository"
+msgstr ""
+
+msgid "LearnGitLab|Enable require merge approvals"
+msgstr ""
+
+msgid "LearnGitLab|Invite your colleagues"
+msgstr ""
+
+msgid "LearnGitLab|Run a Security scan using CI/CD"
+msgstr ""
+
+msgid "LearnGitLab|Set-up CI/CD"
+msgstr ""
+
+msgid "LearnGitLab|Start a free trial of GitLab Gold"
+msgstr ""
+
+msgid "LearnGitLab|Submit a merge request (MR)"
+msgstr ""
+
 msgid "Leave"
 msgstr ""
 
diff --git a/spec/controllers/projects/learn_gitlab_controller_spec.rb b/spec/controllers/projects/learn_gitlab_controller_spec.rb
new file mode 100644
index 00000000000..f633f7aa246
--- /dev/null
+++ b/spec/controllers/projects/learn_gitlab_controller_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+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(:learn_gitlab_experiment_enabled) { true }
+    let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
+
+    subject { get :index, params: params }
+
+    before do
+      allow(controller.helpers).to receive(:learn_gitlab_experiment_enabled?).and_return(learn_gitlab_experiment_enabled)
+    end
+
+    context 'unauthenticated user' do
+      it { is_expected.to have_gitlab_http_status(:redirect) }
+    end
+
+    context 'authenticated user' do
+      before do
+        sign_in(user)
+      end
+
+      it { is_expected.to render_template(:index) }
+
+      it 'pushes experiment to frontend' do
+        expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_a, subject: user)
+        expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_b, subject: user)
+
+        subject
+      end
+
+      context 'learn_gitlab experiment not enabled' do
+        let(:learn_gitlab_experiment_enabled) { false }
+
+        it { is_expected.to have_gitlab_http_status(:not_found) }
+      end
+    end
+  end
+end
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
new file mode 100644
index 00000000000..c9141d13a46
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Learn GitLab Design A should render the loading state 1`] = `
+<ul>
+  <li>
+    <span>
+      Create a repository
+    </span>
+  </li>
+  <li>
+    <span>
+      Invite your colleagues
+    </span>
+  </li>
+  <li>
+    <span>
+      Set-up CI/CD
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Start a free trial of GitLab Gold
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Add code owners
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Enable require merge approvals
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Submit a merge request (MR)
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Run a Security scan using CI/CD
+      </gl-link-stub>
+    </span>
+  </li>
+</ul>
+`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
new file mode 100644
index 00000000000..85e3b675e5b
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Learn GitLab Design B should render the loading state 1`] = `
+<ul>
+  <li>
+    <span>
+      Create a repository
+    </span>
+  </li>
+  <li>
+    <span>
+      Invite your colleagues
+    </span>
+  </li>
+  <li>
+    <span>
+      Set-up CI/CD
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Start a free trial of GitLab Gold
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Add code owners
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Enable require merge approvals
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Submit a merge request (MR)
+      </gl-link-stub>
+    </span>
+  </li>
+  <li>
+    <span>
+      <gl-link-stub
+        href="http://example.com/"
+      >
+        Run a Security scan using CI/CD
+      </gl-link-stub>
+    </span>
+  </li>
+</ul>
+`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
new file mode 100644
index 00000000000..ddc5339e7e0
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
+
+const TEST_ACTIONS = {
+  gitWrite: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  userAdded: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  pipelineCreated: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  trialStarted: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  codeOwnersEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  requiredMrApprovalsEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  mergeRequestCreated: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  securityScanEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+};
+
+describe('Learn GitLab Design A', () => {
+  let wrapper;
+
+  afterEach(() => {
+    wrapper.destroy();
+    wrapper = null;
+  });
+
+  const createWrapper = () => {
+    wrapper = extendedWrapper(
+      shallowMount(LearnGitlabA, {
+        propsData: {
+          actions: TEST_ACTIONS,
+        },
+      }),
+    );
+  };
+
+  it('should render the loading state', () => {
+    createWrapper();
+
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
new file mode 100644
index 00000000000..be4f5768402
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
+
+const TEST_ACTIONS = {
+  gitWrite: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  userAdded: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  pipelineCreated: {
+    url: 'http://example.com/',
+    completed: true,
+  },
+  trialStarted: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  codeOwnersEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  requiredMrApprovalsEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  mergeRequestCreated: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+  securityScanEnabled: {
+    url: 'http://example.com/',
+    completed: false,
+  },
+};
+
+describe('Learn GitLab Design B', () => {
+  let wrapper;
+
+  afterEach(() => {
+    wrapper.destroy();
+    wrapper = null;
+  });
+
+  const createWrapper = () => {
+    wrapper = extendedWrapper(
+      shallowMount(LearnGitlabA, {
+        propsData: {
+          actions: TEST_ACTIONS,
+        },
+      }),
+    );
+  };
+
+  it('should render the loading state', () => {
+    createWrapper();
+
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
new file mode 100644
index 00000000000..f789eb9d940
--- /dev/null
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LearnGitlabHelper do
+  include AfterNextHelpers
+  include Devise::Test::ControllerHelpers
+
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project, name: LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
+  let_it_be(:namespace) { project.namespace }
+
+  before do
+    project.add_developer(user)
+
+    allow(helper).to receive(:user).and_return(user)
+    allow_next_instance_of(LearnGitlab) do |learn_gitlab|
+      allow(learn_gitlab).to receive(:project).and_return(project)
+    end
+
+    OnboardingProgress.onboard(namespace)
+    OnboardingProgress.register(namespace, :git_write)
+  end
+
+  describe '.onboarding_actions_data' do
+    subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
+
+    it 'has all actions' do
+      expect(onboarding_actions_data.keys).to contain_exactly(
+        :git_write,
+        :pipeline_created,
+        :merge_request_created,
+        :user_added,
+        :trial_started,
+        :required_mr_approvals_enabled,
+        :code_owners_enabled,
+        :security_scan_enabled
+      )
+    end
+
+    it 'sets correct path and completion status' do
+      expect(onboarding_actions_data[:git_write]).to eq({
+        url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:git_write]),
+        completed: true
+      })
+      expect(onboarding_actions_data[:pipeline_created]).to eq({
+        url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:pipeline_created]),
+        completed: false
+      })
+    end
+  end
+
+  describe '.learn_gitlab_experiment_enabled?' do
+    using RSpec::Parameterized::TableSyntax
+
+    let_it_be(:user) { create(:user) }
+    let_it_be(:project) { create(:project, namespace: user.namespace) }
+
+    let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
+
+    subject { helper.learn_gitlab_experiment_enabled?(project) }
+
+    where(:experiment_a, :experiment_b, :onboarding, :learn_gitlab_available, :result) do
+      true        | false         | true        | true                  | true
+      false       | true          | true        | true                  | true
+      false       | false         | true        | true                  | false
+      true        | true          | true        | false                 | false
+      true        | true          | false       | true                  | false
+    end
+
+    with_them do
+      before do
+        stub_experiment_for_subject(learn_gitlab_a: experiment_a, learn_gitlab_b: experiment_b)
+        allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
+        allow_next(LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
+      end
+
+      context 'when signed in' do
+        before do
+          sign_in(user)
+        end
+
+        it { is_expected.to eq(result) }
+      end
+
+      context 'when not signed in' do
+        it { is_expected.to eq(false) }
+      end
+    end
+  end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index b61db537159..303e3c78153 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
 
 RSpec.describe ProjectsHelper do
   include ProjectForksHelper
+  include AfterNextHelpers
 
   let_it_be_with_reload(:project) { create(:project) }
   let_it_be_with_refind(:project_with_repo) { create(:project, :repository) }
@@ -498,6 +499,20 @@ RSpec.describe ProjectsHelper do
       it { is_expected.not_to include(:confluence) }
       it { is_expected.to include(:wiki) }
     end
+
+    context 'learn gitlab experiment' do
+      context 'when it is enabled' do
+        before do
+          expect(helper).to receive(:learn_gitlab_experiment_enabled?).with(project).and_return(true)
+        end
+
+        it { is_expected.to include(:learn_gitlab) }
+      end
+
+      context 'when it is not enabled' do
+        it { is_expected.not_to include(:learn_gitlab) }
+      end
+    end
   end
 
   describe '#can_view_operations_tab?' do
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb
index 02fe0015a14..0aa19345a25 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding_progress_spec.rb
@@ -114,6 +114,22 @@ RSpec.describe OnboardingProgress do
     end
   end
 
+  describe '.onboarding?' do
+    subject(:onboarding?) { described_class.onboarding?(namespace) }
+
+    context 'when onboarded' do
+      before do
+        described_class.onboard(namespace)
+      end
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when not onboarding' do
+      it { is_expected.to eq false }
+    end
+  end
+
   describe '.register' do
     subject(:register_action) { described_class.register(namespace, action) }
 
-- 
2.30.9