Commit d4d4c9cc authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'experiment/pipeline-editor-walkthrough' into 'master'

Experiment: Pipeline Editor Walkthrough

See merge request gitlab-org/gitlab!73050
parents e147d7c8 1ec67ecd
......@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
scrollToCommitForm: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -52,6 +57,13 @@ export default {
return !(this.message && this.targetBranch);
},
},
watch: {
scrollToCommitForm(flag) {
if (flag) {
this.scrollIntoView();
}
},
},
methods: {
onSubmit() {
this.$emit('submit', {
......@@ -63,6 +75,10 @@ export default {
onReset() {
this.$emit('cancel');
},
scrollIntoView() {
this.$el.scrollIntoView({ behavior: 'smooth' });
this.$emit('scrolled-to-commit-form');
},
},
i18n: {
commitMessage: __('Commit message'),
......
......@@ -45,6 +45,11 @@ export default {
required: false,
default: false,
},
scrollToCommitForm: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -146,6 +151,8 @@ export default {
:current-branch="currentBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
:scroll-to-commit-form="scrollToCommitForm"
v-on="$listeners"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
......
......@@ -2,6 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { experiment } from '~/experimentation/utils';
import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue';
......@@ -53,12 +54,23 @@ export default {
},
methods: {
setInitialExpandState() {
let isExpanded;
experiment('pipeline_editor_walkthrough', {
control: () => {
isExpanded = true;
},
candidate: () => {
isExpanded = false;
},
});
// We check in the local storage and if no value is defined, we want the default
// to be true. We want to explicitly set it to true here so that the drawer
// animates to open on load.
const localValue = localStorage.getItem(this.$options.localDrawerKey);
if (localValue === null) {
this.isExpanded = true;
this.isExpanded = isExpanded;
}
},
setTopPosition() {
......
......@@ -112,7 +112,7 @@ export default {
isBranchesLoading() {
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
},
showBranchSwitcher() {
enableBranchSwitcher() {
return this.branches.length > 0 || this.searchTerm.length > 0;
},
},
......@@ -230,11 +230,11 @@ export default {
<template>
<gl-dropdown
v-if="showBranchSwitcher"
v-gl-tooltip.hover
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
data-qa-selector="branch_selector_button"
data-testid="branch-selector"
......
......@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
......@@ -22,6 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
import WalkthroughPopover from './walkthrough_popover.vue';
export default {
i18n: {
......@@ -63,6 +65,8 @@ export default {
GlTabs,
PipelineGraph,
TextEditor,
GitlabExperiment,
WalkthroughPopover,
},
mixins: [glFeatureFlagsMixin()],
props: {
......@@ -79,6 +83,10 @@ export default {
required: false,
default: '',
},
isNewCiConfigFile: {
type: Boolean,
required: true,
},
},
apollo: {
appStatus: {
......@@ -136,11 +144,17 @@ export default {
>
<editor-tab
class="gl-mb-3"
title-link-class="js-walkthrough-popover-target"
:title="$options.i18n.tabEdit"
lazy
data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
<gitlab-experiment name="pipeline_editor_walkthrough">
<template #candidate>
<walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
</template>
</gitlab-experiment>
<ci-editor-header />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
......
<script>
import { GlButton, GlPopover, GlSprintf, GlOutsideDirective as Outside } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
directives: { Outside },
i18n: {
title: s__('pipelineEditorWalkthrough|See how GitLab pipelines work'),
description: s__(
'pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline.',
),
instruction: s__(
'pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline.',
),
ctaText: s__("pipelineEditorWalkthrough|Let's do this!"),
},
components: {
GlButton,
GlPopover,
GlSprintf,
},
data() {
return {
show: true,
};
},
computed: {
targetElement() {
return document.querySelector('.js-walkthrough-popover-target');
},
},
methods: {
close() {
this.show = false;
},
handleClickCta() {
this.close();
this.$emit('walkthrough-popover-cta-clicked');
},
},
};
</script>
<template>
<gl-popover
:show.sync="show"
:title="$options.i18n.title"
:target="targetElement"
placement="right"
triggers="focus"
>
<div v-outside="close" class="gl-display-flex gl-flex-direction-column">
<p>
<gl-sprintf :message="$options.i18n.description">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.i18n.instruction">
<template #bold="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</p>
<gl-button
class="gl-align-self-end"
category="tertiary"
data-testid="ctaBtn"
variant="confirm"
@click="handleClickCta"
>
<gl-emoji data-name="rocket" />
{{ this.$options.i18n.ctaText }}
</gl-button>
</div>
</gl-popover>
</template>
......@@ -58,6 +58,7 @@ export default {
data() {
return {
currentTab: CREATE_TAB,
scrollToCommitForm: false,
shouldLoadNewBranch: false,
showSwitchBranchModal: false,
};
......@@ -81,6 +82,9 @@ export default {
setCurrentTab(tabName) {
this.currentTab = tabName;
},
setScrollToCommitForm(newValue = true) {
this.scrollToCommitForm = newValue;
},
},
};
</script>
......@@ -117,8 +121,10 @@ export default {
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners"
@set-current-tab="setCurrentTab"
@walkthrough-popover-cta-clicked="setScrollToCommitForm"
/>
<commit-section
v-if="showCommitForm"
......@@ -126,6 +132,8 @@ export default {
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
:scroll-to-commit-form="scrollToCommitForm"
@scrolled-to-commit-form="setScrollToCommitForm(false)"
v-on="$listeners"
/>
<pipeline-editor-drawer />
......
......@@ -2,6 +2,7 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action :setup_walkthrough_experiment, only: :show
before_action do
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
......@@ -16,4 +17,10 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
def check_can_collaborate!
render_404 unless can_collaborate_with_project?(@project)
end
def setup_walkthrough_experiment
experiment(:pipeline_editor_walkthrough, actor: current_user) do |e|
e.candidate {}
end
end
end
---
name: pipeline_editor_walkthrough
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73050
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345558
milestone: '14.5'
type: experiment
group: group::activation
default_enabled: false
......@@ -41698,6 +41698,18 @@ msgstr ""
msgid "pipeline schedules documentation"
msgstr ""
msgid "pipelineEditorWalkthrough|Let's do this!"
msgstr ""
msgid "pipelineEditorWalkthrough|See how GitLab pipelines work"
msgstr ""
msgid "pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline."
msgstr ""
msgid "pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline."
msgstr ""
msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
msgstr ""
......
......@@ -6,6 +6,8 @@ RSpec.describe Projects::Ci::PipelineEditorController do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
subject(:show_request) { get :show, params: { namespace_id: project.namespace, project_id: project } }
before do
sign_in(user)
end
......@@ -14,8 +16,7 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'with enough privileges' do
before do
project.add_developer(user)
get :show, params: { namespace_id: project.namespace, project_id: project }
show_request
end
it { expect(response).to have_gitlab_http_status(:ok) }
......@@ -28,13 +29,27 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'without enough privileges' do
before do
project.add_reporter(user)
get :show, params: { namespace_id: project.namespace, project_id: project }
show_request
end
it 'responds with 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'pipeline_editor_walkthrough experiment' do
before do
project.add_developer(user)
end
it 'tracks the assignment', :experiment do
expect(experiment(:pipeline_editor_walkthrough))
.to track(:assignment)
.with_context(actor: user)
.on_next_instance
show_request
end
end
end
end
......@@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
describe('Pipeline Editor | Commit Form', () => {
let wrapper;
......@@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => {
expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
});
});
describe('when scrollToCommitForm becomes true', () => {
beforeEach(async () => {
createComponent();
wrapper.setProps({ scrollToCommitForm: true });
await wrapper.vm.$nextTick();
});
it('scrolls into view', () => {
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
});
it('emits "scrolled-to-commit-form"', () => {
expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
});
});
});
......@@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => {
expect(wrapper.emitted('resetContent')).toHaveLength(1);
});
});
it('sets listeners on commit form', () => {
const handler = jest.fn();
createComponent({ options: { listeners: { event: handler } } });
findCommitForm().vm.$emit('event');
expect(handler).toHaveBeenCalled();
});
it('passes down scroll-to-commit-form prop to commit form', () => {
createComponent({ props: { 'scroll-to-commit-form': true } });
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
});
});
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { stubExperiments } from 'helpers/experimentation_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
......@@ -33,20 +34,42 @@ describe('Pipeline editor drawer', () => {
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
const originalObjects = [];
beforeEach(() => {
originalObjects.push(window.gon, window.gl);
stubExperiments({ pipeline_editor_walkthrough: 'control' });
});
afterEach(() => {
wrapper.destroy();
localStorage.clear();
[window.gon, window.gl] = originalObjects;
});
it('it sets the drawer to be opened by default', async () => {
describe('default expanded state', () => {
describe('when experiment control', () => {
it('sets the drawer to be opened by default', async () => {
createComponent();
expect(findDrawerContent().exists()).toBe(false);
await nextTick();
expect(findDrawerContent().exists()).toBe(true);
});
});
describe('when experiment candidate', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
});
it('sets the drawer to be closed by default', async () => {
createComponent();
expect(findDrawerContent().exists()).toBe(false);
await nextTick();
expect(findDrawerContent().exists()).toBe(false);
});
});
});
describe('when the drawer is collapsed', () => {
beforeEach(async () => {
......
......@@ -141,8 +141,8 @@ describe('Pipeline editor branch switcher', () => {
createComponentWithApollo();
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
it('disables the dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
});
......@@ -189,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
expect(findDropdown().props('disabled')).toBe(true);
});
it('shows an error message', () => {
......
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
import { stubExperiments } from 'helpers/experimentation_helper';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
......@@ -19,6 +21,8 @@ import {
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
Vue.config.ignoredElements = ['gl-emoji'];
describe('Pipeline editor tabs component', () => {
let wrapper;
const MockTextEditor = {
......@@ -26,6 +30,7 @@ describe('Pipeline editor tabs component', () => {
};
const createComponent = ({
listeners = {},
props = {},
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
......@@ -35,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isNewCiConfigFile: true,
...props,
},
data() {
......@@ -47,6 +53,7 @@ describe('Pipeline editor tabs component', () => {
TextEditor: MockTextEditor,
EditorTab,
},
listeners,
});
};
......@@ -62,6 +69,7 @@ describe('Pipeline editor tabs component', () => {
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
afterEach(() => {
wrapper.destroy();
......@@ -236,4 +244,63 @@ describe('Pipeline editor tabs component', () => {
expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
});
describe('pipeline_editor_walkthrough experiment', () => {
describe('when in control path', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'control' });
});
it('does not show walkthrough popover', async () => {
createComponent({ mountFn: mount });
await nextTick();
expect(findWalkthroughPopover().exists()).toBe(false);
});
});
describe('when in candidate path', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
});
describe('when isNewCiConfigFile prop is true (default)', () => {
beforeEach(async () => {
createComponent({
mountFn: mount,
});
await nextTick();
});
it('shows walkthrough popover', async () => {
expect(findWalkthroughPopover().exists()).toBe(true);
});
});
describe('when isNewCiConfigFile prop is false', () => {
it('does not show walkthrough popover', async () => {
createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
await nextTick();
expect(findWalkthroughPopover().exists()).toBe(false);
});
});
});
});
it('sets listeners on walkthrough popover', async () => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
const handler = jest.fn();
createComponent({
mountFn: mount,
listeners: {
event: handler,
},
});
await nextTick();
findWalkthroughPopover().vm.$emit('event');
expect(handler).toHaveBeenCalled();
});
});
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
describe('WalkthroughPopover component', () => {
let wrapper;
const createComponent = (mountFn = shallowMount) => {
return extendedWrapper(mountFn(WalkthroughPopover));
};
afterEach(() => {
wrapper.destroy();
});
describe('CTA button clicked', () => {
beforeEach(async () => {
wrapper = createComponent(mount);
await wrapper.findByTestId('ctaBtn').trigger('click');
});
it('emits "walkthrough-popover-cta-clicked" event', async () => {
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
});
});
});
......@@ -152,4 +152,27 @@ describe('Pipeline editor home wrapper', () => {
expect(findCommitSection().exists()).toBe(true);
});
});
describe('WalkthroughPopover events', () => {
beforeEach(() => {
createComponent();
});
describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => {
it('passes down `scrollToCommitForm=true` to commit section', async () => {
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
});
});
describe('when "scrolled-to-commit-form" is emitted from commit section', () => {
it('passes down `scrollToCommitForm=false` to commit section', async () => {
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
await findCommitSection().vm.$emit('scrolled-to-commit-form');
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
});
});
});
});
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