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 @@
+
+
+
+
+ {{ content }}
+
+
+
+
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