Commit 2ab23a78 authored by eugielimpin's avatar eugielimpin Committed by Douglas Barbosa Alexandre

Implement Pipeline Editor Walkthrough experiment

In this experiment, a walkthrough popover is displayed when the user
views the editor tab of the pipeline editor. This popover will guide
them to committing a new CI config file in the current branch. The
pipeline drawer is also closed when the user is in the candidate group
of the experiment (it is open for control group).

Changelog: added
parent d1aaf644
......@@ -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() {
......
......@@ -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';
import eventHub from '../event_hub';
export default {
directives: { Outside },
......@@ -36,7 +35,7 @@ export default {
},
handleClickCta() {
this.close();
eventHub.$emit('walkthroughPopoverCtaClicked');
this.$emit('walkthrough-popover-cta-clicked');
},
},
};
......
......@@ -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 />
......
......@@ -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,19 +34,41 @@ 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 () => {
createComponent();
expect(findDrawerContent().exists()).toBe(false);
await nextTick();
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);
});
});
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', () => {
......
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 pipelineEditorEventHub from '~/pipeline_editor/event_hub';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
......@@ -19,13 +18,12 @@ describe('WalkthroughPopover component', () => {
describe('CTA button clicked', () => {
beforeEach(async () => {
jest.spyOn(pipelineEditorEventHub, '$emit');
wrapper = createComponent(mount);
await wrapper.findByTestId('ctaBtn').trigger('click');
});
it('emits "walkthroughPopoverCtaClicked" event on Pipeline Editor eventHub', async () => {
expect(pipelineEditorEventHub.$emit).toHaveBeenCalledWith('walkthroughPopoverCtaClicked');
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