diff --git a/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql new file mode 100644 index 0000000000000000000000000000000000000000..2b831bf13388712f64570712c79e663d0206029e --- /dev/null +++ b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql @@ -0,0 +1,9 @@ +mutation dismissUserCallout($input: UserCalloutCreateInput!) { + userCalloutCreate(input: $input) { + errors + userCallout { + dismissedAt + featureName + } + } +} diff --git a/app/assets/javascripts/ide/components/file_alert.vue b/app/assets/javascripts/ide/components/file_alert.vue new file mode 100644 index 0000000000000000000000000000000000000000..2a894596bf47651ca84edd7c0a2d9a8574496930 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_alert.vue @@ -0,0 +1,26 @@ + + diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b57dcd4276c078e58b4c7f5af782d6fa1e616b32..bf2af9ffd49ae3a4bd2c70fa7a706d071bc718b4 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,4 +1,5 @@ + diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c9db9779b1fa95f617809ddef0b049084145b34e --- /dev/null +++ b/app/assets/javascripts/ide/lib/alerts/index.js @@ -0,0 +1,20 @@ +import { leftSidebarViews } from '../../constants'; +import EnvironmentsMessage from './environments.vue'; + +const alerts = [ + { + key: Symbol('ALERT_ENVIRONMENT'), + show: (state, file) => + state.currentActivityView === leftSidebarViews.commit.name && + file.path === '.gitlab-ci.yml' && + state.environmentsGuidanceAlertDetected && + !state.environmentsGuidanceAlertDismissed, + props: { variant: 'tip' }, + dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'), + message: EnvironmentsMessage, + }, +]; + +export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key; + +export const getAlert = (key) => alerts.find((x) => x.key === key); diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js index 89dda1873607021eaf3b400009957ac345111eab..c8c1031c0f31f373834817e4074749b661f42b8b 100644 --- a/app/assets/javascripts/ide/services/gql.js +++ b/app/assets/javascripts/ide/services/gql.js @@ -18,3 +18,4 @@ const getClient = memoize(() => ); export const query = (...args) => getClient().query(...args); +export const mutate = (...args) => getClient().mutate(...args); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 0aa08323d13ba99abb6e6ac09e849e0e48c42199..6bd28cd4fb618716fb0ebff5d03b3ada3552836c 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,8 +1,10 @@ import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; -import { query } from './gql'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; +import { query, mutate } from './gql'; const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data); @@ -101,4 +103,16 @@ export default { const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`; return axios.post(url); }, + getCiConfig(projectPath, content) { + return query({ + query: ciConfig, + variables: { projectPath, content }, + }).then(({ data }) => data.ciConfig); + }, + dismissUserCallout(name) { + return mutate({ + mutation: dismissUserCallout, + variables: { input: { featureName: name } }, + }).then(({ data }) => data); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index bf94f9d31c88c5802feaa9b29331aac62c810b4f..062dc150805a4cc7d77476500fe29ab17253b7c0 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -17,7 +17,7 @@ import * as types from './mutation_types'; export const redirectToUrl = (self, url) => visitUrl(url); -export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); +export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const discardAllChanges = ({ state, commit, dispatch }) => { state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path)); @@ -316,3 +316,4 @@ export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; export * from './actions/merge_request'; +export * from './actions/alert'; diff --git a/app/assets/javascripts/ide/stores/actions/alert.js b/app/assets/javascripts/ide/stores/actions/alert.js new file mode 100644 index 0000000000000000000000000000000000000000..4c33dc195209807902410592f41993558b7dc625 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/alert.js @@ -0,0 +1,18 @@ +import service from '../../services'; +import { + DETECT_ENVIRONMENTS_GUIDANCE_ALERT, + DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, +} from '../mutation_types'; + +export const detectGitlabCiFileAlerts = ({ dispatch }, content) => + dispatch('detectEnvironmentsGuidance', content); + +export const detectEnvironmentsGuidance = ({ commit, state }, content) => + service.getCiConfig(state.currentProjectId, content).then((data) => { + commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages); + }); + +export const dismissEnvironmentsGuidance = ({ commit }) => + service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => { + commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT); + }); diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index e8b1a0ea494375bc8b521389cf8ed1e0fdb3bc0d..3c02b1d1da7b56ea1522bf2ca75b9bfca24e71b0 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => { fileMatch: [`*${path}`], }; }; + +export * from './getters/alert'; diff --git a/app/assets/javascripts/ide/stores/getters/alert.js b/app/assets/javascripts/ide/stores/getters/alert.js new file mode 100644 index 0000000000000000000000000000000000000000..714e2d89b4f359da0616e95aea9c2b5f0076e76e --- /dev/null +++ b/app/assets/javascripts/ide/stores/getters/alert.js @@ -0,0 +1,3 @@ +import { findAlertKeyToShow } from '../../lib/alerts'; + +export const getAlert = (state) => (file) => findAlertKeyToShow(state, file); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 76ba8339703e09cd586e12d1aba47c1e4472279a..77755b179ef3dec97a4930d8cf46217d1f33167c 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -70,3 +70,8 @@ export const RENAME_ENTRY = 'RENAME_ENTRY'; export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY'; export const RESTORE_TREE = 'RESTORE_TREE'; + +// Alert mutation types + +export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT'; +export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 576f861a090024dd6abdeab13f0a57603e953889..48648796e660b59727a4c3ec3a3e9ef9684d483f 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import * as types from './mutation_types'; +import alertMutations from './mutations/alert'; import branchMutations from './mutations/branch'; import fileMutations from './mutations/file'; import mergeRequestMutation from './mutations/merge_request'; @@ -244,4 +245,5 @@ export default { ...fileMutations, ...treeMutations, ...branchMutations, + ...alertMutations, }; diff --git a/app/assets/javascripts/ide/stores/mutations/alert.js b/app/assets/javascripts/ide/stores/mutations/alert.js new file mode 100644 index 0000000000000000000000000000000000000000..bb2d33a836bf9b7b9f83a1f62d378ce34956a93f --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/alert.js @@ -0,0 +1,21 @@ +import { + DETECT_ENVIRONMENTS_GUIDANCE_ALERT, + DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, +} from '../mutation_types'; + +export default { + [DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) { + if (!stages) { + return; + } + const hasEnvironments = stages?.nodes?.some((stage) => + stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)), + ); + const hasParsedCi = Array.isArray(stages.nodes); + + state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi; + }, + [DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) { + state.environmentsGuidanceAlertDismissed = true; + }, +}; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index c1a83bf0726addd66b0ba5b006be659676e2047c..83551e87f099322b5e7c5d5adc874a449ea88741 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -30,4 +30,6 @@ export default () => ({ renderWhitespaceInCode: false, editorTheme: DEFAULT_THEME, codesandboxBundlerUrl: null, + environmentsGuidanceAlertDismissed: false, + environmentsGuidanceAlertDetected: false, }); diff --git a/app/experiments/in_product_guidance_environments_webide_experiment.rb b/app/experiments/in_product_guidance_environments_webide_experiment.rb new file mode 100644 index 0000000000000000000000000000000000000000..d77063a98345cac54ce9bb1079ba2f19f8a0a37f --- /dev/null +++ b/app/experiments/in_product_guidance_environments_webide_experiment.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass + exclude :has_environments? + + def control_behavior + false + end + + private + + def has_environments? + !context.project.environments.empty? + end +end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 61d8d0f779debaedff9b3f04112d21628c1112dd..a38ab97e59c589ddd29ad1b9382ddab48608816d 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -17,7 +17,8 @@ def ide_data 'file-path' => @path, 'merge-request' => @merge_request, 'fork-info' => @fork_info&.to_json, - 'project' => convert_to_project_entity_json(@project) + 'project' => convert_to_project_entity_json(@project), + 'enable-environments-guidance' => enable_environments_guidance?.to_s } end @@ -28,6 +29,18 @@ def convert_to_project_entity_json(project) API::Entities::Project.represent(project).to_json end + + def enable_environments_guidance? + experiment(:in_product_guidance_environments_webide, project: @project) do |e| + e.try { !has_dismissed_ide_environments_callout? } + + e.run + end + end + + def has_dismissed_ide_environments_callout? + current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance') + end end ::IdeHelper.prepend_if_ee('::EE::IdeHelper') diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 852ea05b77fdd86b58743ec2198d2ba22a03893e..8fc9efddac9badf44fbe776bff6743645fc1403a 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord unfinished_tag_cleanup_callout: 27, eoa_bronze_plan_banner: 28, # EE-only pipeline_needs_banner: 29, - pipeline_needs_hover_tip: 30 + pipeline_needs_hover_tip: 30, + web_ide_ci_environments_guidance: 31 } validates :user, presence: true diff --git a/config/feature_flags/experiment/in_product_guidance_environments_webide.yml b/config/feature_flags/experiment/in_product_guidance_environments_webide.yml new file mode 100644 index 0000000000000000000000000000000000000000..4eaf6e90b27d9f96576f6fd423974503304cad03 --- /dev/null +++ b/config/feature_flags/experiment/in_product_guidance_environments_webide.yml @@ -0,0 +1,8 @@ +--- +name: in_product_guidance_environments_webide +introduced_by_url: +rollout_issue_url: +milestone: '13.12' +type: experiment +group: group::release +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7f9bd21722406b096002fdf8c36daa5f5fa93f3c..161fdb670bf2fbf2c8f8bf9b278b2c317b658118 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14360,6 +14360,7 @@ Name of the feature that the callout is for. | `UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. | | `WEBHOOKS_MOVED` | Callout feature name for webhooks_moved. | | `WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. | +| `WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. | ### `UserState` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ac56337b7cf0b80d04cab3ddaf289a9d3a78b62c..78271197ff22d87a2f7fb084a28e18a404ecf199 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21975,6 +21975,9 @@ msgstr "" msgid "No data to display" msgstr "" +msgid "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}" +msgstr "" + msgid "No deployments found" msgstr "" diff --git a/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb b/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d616672173eebc04c09c2e891aebf3f9233b3d11 --- /dev/null +++ b/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe InProductGuidanceEnvironmentsWebideExperiment, :experiment do + subject { described_class.new(project: project) } + + let(:project) { create(:project, :repository) } + + before do + stub_experiments(in_product_guidance_environments_webide: :candidate) + end + + it 'excludes projects with environments' do + create(:environment, project: project) + expect(subject).to exclude(project: project) + end + + it 'does not exlude projects without environments' do + expect(subject).not_to exclude(project: project) + end +end diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index a3b327343e5c5213eac337e91a3d98ec704a9852..646e51160d800c28eb0ba88510a10104cd68de21 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -510,6 +510,7 @@ describe('RepoEditor', () => { }, }); await vm.$nextTick(); + await vm.$nextTick(); expect(vm.initEditor).toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/lib/alerts/environment_spec.js b/spec/frontend/ide/lib/alerts/environment_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d645209345ce016c72af3336767ea05c56e0c6d7 --- /dev/null +++ b/spec/frontend/ide/lib/alerts/environment_spec.js @@ -0,0 +1,21 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Environments from '~/ide/lib/alerts/environments.vue'; + +describe('~/ide/lib/alerts/environment.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(Environments); + }); + + it('shows a message regarding environments', () => { + expect(wrapper.text()).toBe( + "No deployments detected. Use environments to control your software's continuous deployment. Learn more about deployment jobs.", + ); + }); + + it('links to the help page on environments', () => { + expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/help/ci/environments/index.md'); + }); +}); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 3503834e24b764738b5f7fbb993c723afe018d1d..4a726cff3b6b3f537ad8df3bb07138a2f5fc1f5c 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -2,9 +2,11 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import services from '~/ide/services'; -import { query } from '~/ide/services/gql'; +import { query, mutate } from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import { projectData } from '../mock_data'; jest.mock('~/api'); @@ -299,4 +301,33 @@ describe('IDE services', () => { }); }); }); + describe('getCiConfig', () => { + const TEST_PROJECT_PATH = 'foo/bar'; + const TEST_CI_CONFIG = 'test config'; + + it('queries with the given CI config and project', () => { + const result = { data: { ciConfig: { test: 'data' } } }; + query.mockResolvedValue(result); + return services.getCiConfig(TEST_PROJECT_PATH, TEST_CI_CONFIG).then((data) => { + expect(data).toEqual(result.data.ciConfig); + expect(query).toHaveBeenCalledWith({ + query: ciConfig, + variables: { projectPath: TEST_PROJECT_PATH, content: TEST_CI_CONFIG }, + }); + }); + }); + }); + describe('dismissUserCallout', () => { + it('mutates the callout to dismiss', () => { + const result = { data: { callouts: { test: 'data' } } }; + mutate.mockResolvedValue(result); + return services.dismissUserCallout('test').then((data) => { + expect(data).toEqual(result.data); + expect(mutate).toHaveBeenCalledWith({ + mutation: dismissUserCallout, + variables: { input: { featureName: 'test' } }, + }); + }); + }); + }); }); diff --git a/spec/frontend/ide/stores/actions/alert_spec.js b/spec/frontend/ide/stores/actions/alert_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..1321c402ebbc26a61ffe0b47aa8916d97c8783cb --- /dev/null +++ b/spec/frontend/ide/stores/actions/alert_spec.js @@ -0,0 +1,46 @@ +import testAction from 'helpers/vuex_action_helper'; +import service from '~/ide/services'; +import { + detectEnvironmentsGuidance, + dismissEnvironmentsGuidance, +} from '~/ide/stores/actions/alert'; +import * as types from '~/ide/stores/mutation_types'; + +jest.mock('~/ide/services'); + +describe('~/ide/stores/actions/alert', () => { + describe('detectEnvironmentsGuidance', () => { + it('should try to fetch CI info', () => { + const stages = ['a', 'b', 'c']; + service.getCiConfig.mockResolvedValue({ stages }); + + return testAction( + detectEnvironmentsGuidance, + 'the content', + { currentProjectId: 'gitlab/test' }, + [{ type: types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, payload: stages }], + [], + () => expect(service.getCiConfig).toHaveBeenCalledWith('gitlab/test', 'the content'), + ); + }); + }); + describe('dismissCallout', () => { + it('should try to dismiss the given callout', () => { + const callout = { featureName: 'test', dismissedAt: 'now' }; + + service.dismissUserCallout.mockResolvedValue({ userCalloutCreate: { userCallout: callout } }); + + return testAction( + dismissEnvironmentsGuidance, + undefined, + {}, + [{ type: types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT }], + [], + () => + expect(service.dismissUserCallout).toHaveBeenCalledWith( + 'web_ide_ci_environments_guidance', + ), + ); + }); + }); +}); diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index d47dd88dd4796ebda7149b26876de84cdd8dc7bd..ad55313da9358bbc8c66c99bb510e59bfd702d3a 100644 --- a/spec/frontend/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -4,6 +4,7 @@ import eventHub from '~/ide/eventhub'; import { createRouter } from '~/ide/ide_router'; import { createStore } from '~/ide/stores'; import { + init, stageAllChanges, unstageAllChanges, toggleFileFinder, @@ -54,15 +55,15 @@ describe('Multi-file store actions', () => { }); }); - describe('setInitialData', () => { - it('commits initial data', (done) => { - store - .dispatch('setInitialData', { canCommit: true }) - .then(() => { - expect(store.state.canCommit).toBeTruthy(); - done(); - }) - .catch(done.fail); + describe('init', () => { + it('commits initial data and requests user callouts', () => { + return testAction( + init, + { canCommit: true }, + store.state, + [{ type: 'SET_INITIAL_DATA', payload: { canCommit: true } }], + [], + ); }); }); diff --git a/spec/frontend/ide/stores/getters/alert_spec.js b/spec/frontend/ide/stores/getters/alert_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7068b8e637f6ccdbab29f67f3cf0b0be0743baa8 --- /dev/null +++ b/spec/frontend/ide/stores/getters/alert_spec.js @@ -0,0 +1,46 @@ +import { getAlert } from '~/ide/lib/alerts'; +import EnvironmentsMessage from '~/ide/lib/alerts/environments.vue'; +import { createStore } from '~/ide/stores'; +import * as getters from '~/ide/stores/getters/alert'; +import { file } from '../../helpers'; + +describe('IDE store alert getters', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('alerts', () => { + describe('shows an alert about environments', () => { + let alert; + + beforeEach(() => { + const f = file('.gitlab-ci.yml'); + localState.openFiles.push(f); + localState.currentActivityView = 'repo-commit-section'; + localState.environmentsGuidanceAlertDetected = true; + localState.environmentsGuidanceAlertDismissed = false; + + const alertKey = getters.getAlert(localState)(f); + alert = getAlert(alertKey); + }); + + it('has a message suggesting to use environments', () => { + expect(alert.message).toEqual(EnvironmentsMessage); + }); + + it('dispatches to dismiss the callout on dismiss', () => { + jest.spyOn(localStore, 'dispatch').mockImplementation(); + alert.dismiss(localStore); + expect(localStore.dispatch).toHaveBeenCalledWith('dismissEnvironmentsGuidance'); + }); + + it('should be a tip alert', () => { + expect(alert.props).toEqual({ variant: 'tip' }); + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations/alert_spec.js b/spec/frontend/ide/stores/mutations/alert_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..2840ec4ebb70e14f57201cbb302b4cb0e6df2f87 --- /dev/null +++ b/spec/frontend/ide/stores/mutations/alert_spec.js @@ -0,0 +1,26 @@ +import * as types from '~/ide/stores/mutation_types'; +import mutations from '~/ide/stores/mutations/alert'; + +describe('~/ide/stores/mutations/alert', () => { + const state = {}; + + describe(types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, () => { + it('checks the stages for any that configure environments', () => { + mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, { + nodes: [{ groups: { nodes: [{ jobs: { nodes: [{}] } }] } }], + }); + expect(state.environmentsGuidanceAlertDetected).toBe(true); + mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, { + nodes: [{ groups: { nodes: [{ jobs: { nodes: [{ environment: {} }] } }] } }], + }); + expect(state.environmentsGuidanceAlertDetected).toBe(false); + }); + }); + + describe(types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, () => { + it('stops environments guidance', () => { + mutations[types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state); + expect(state.environmentsGuidanceAlertDismissed).toBe(true); + }); + }); +}); diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 963d5953d4ce9e56c06bfb862e3358916632cf72..d34358e49c0b3d985d4a6b560bf5fa0b3df9b819 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -45,5 +45,35 @@ ) end end + + context 'environments guidance experiment', :experiment do + before do + stub_experiments(in_product_guidance_environments_webide: :candidate) + self.instance_variable_set(:@project, project) + end + + context 'when project has no enviornments' do + it 'enables environment guidance' do + expect(helper.ide_data).to include('enable-environments-guidance' => 'true') + end + + context 'and the callout has been dismissed' do + it 'disables environment guidance' do + callout = create(:user_callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator) + callout.update!(dismissed_at: Time.now - 1.week) + allow(helper).to receive(:current_user).and_return(User.find(project.creator.id)) + expect(helper.ide_data).to include('enable-environments-guidance' => 'false') + end + end + end + + context 'when the project has environments' do + it 'disables environment guidance' do + create(:environment, project: project) + + expect(helper.ide_data).to include('enable-environments-guidance' => 'false') + end + end + end end end