From b6aa758e0842beae124f1dbd813433393627f6eb Mon Sep 17 00:00:00 2001 From: Jiaan Louw Date: Mon, 8 Sep 2025 14:41:34 +0200 Subject: [PATCH 1/2] Remove deprecated product analytics onboarding frontend Removes all of the frontend files related to product analytics onboarding experience from Analytics dashboards --- .../components/dashboards_list.vue | 36 +-- .../analytics/analytics_dashboards/index.js | 14 +- .../analytics/analytics_dashboards/router.js | 24 +- ...tialize_product_analytics.mutation.graphql | 9 - ...s_project_settings_update.mutation.graphql | 23 -- ...t_analytics_project_settings.query.graphql | 11 - .../get_product_analytics_state.query.graphql | 6 - .../get_project_tracking_key.query.graphql | 6 - .../instrumentation_instructions.vue | 122 -------- ...strumentation_instructions_sdk_details.vue | 47 --- .../components/onboarding_list_item.vue | 105 ------- .../components/onboarding_state.vue | 102 ------- .../clear_project_settings_modal.vue | 124 -------- .../components/providers/constants.js | 5 - .../gitlab_managed_provider_card.vue | 140 --------- .../providers/provider_selection_view.vue | 196 ------------ .../providers/provider_settings_form.vue | 209 ------------- .../providers/provider_settings_preview.vue | 69 ----- .../providers/self_managed_provider_card.vue | 185 ------------ .../onboarding/components/providers/utils.js | 74 ----- .../product_analytics/onboarding/constants.js | 37 --- .../product_analytics/onboarding/index.js | 27 -- .../onboarding/onboarding_setup.vue | 135 --------- .../onboarding/onboarding_view.vue | 89 ------ .../settings_instrumentation_instructions.vue | 76 ----- .../shared/analytics_clipboard_input.vue | 80 ----- .../components/dashboards_list_spec.js | 83 ----- .../analytics_dashboards/router_spec.js | 29 +- .../frontend/product_analytics/mock_data.js | 86 ------ ...mentation_instructions_sdk_details_spec.js | 38 --- .../instrumentation_instructions_spec.js | 77 ----- .../components/onboarding_list_item_spec.js | 133 -------- .../components/onboarding_state_spec.js | 130 -------- .../clear_project_settings_modal_spec.js | 224 -------------- .../gitlab_managed_provider_card_spec.js | 168 ----------- .../provider/provider_selection_view_spec.js | 247 --------------- .../provider/provider_settings_form_spec.js | 285 ------------------ .../provider_settings_preview_spec.js | 70 ----- .../self_managed_provider_card_spec.js | 284 ----------------- .../components/provider/utils_spec.js | 110 ------- .../onboarding/onboarding_setup_spec.js | 127 -------- .../onboarding/onboarding_view_spec.js | 170 ----------- ...tings_instrumentation_instructions_spec.js | 82 ----- .../shared/analytics_clipboard_input_spec.js | 77 ----- locale/gitlab.pot | 207 ------------- 45 files changed, 17 insertions(+), 4561 deletions(-) delete mode 100644 ee/app/assets/javascripts/product_analytics/graphql/mutations/initialize_product_analytics.mutation.graphql delete mode 100644 ee/app/assets/javascripts/product_analytics/graphql/mutations/product_analytics_project_settings_update.mutation.graphql delete mode 100644 ee/app/assets/javascripts/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql delete mode 100644 ee/app/assets/javascripts/product_analytics/graphql/queries/get_product_analytics_state.query.graphql delete mode 100644 ee/app/assets/javascripts/product_analytics/graphql/queries/get_project_tracking_key.query.graphql delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/instrumentation_instructions.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_list_item.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_state.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/constants.js delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_selection_view.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_form.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_preview.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/self_managed_provider_card.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/components/providers/utils.js delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/constants.js delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/index.js delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/onboarding_setup.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/onboarding_view.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/onboarding/settings_instrumentation_instructions.vue delete mode 100644 ee/app/assets/javascripts/product_analytics/shared/analytics_clipboard_input.vue delete mode 100644 ee/spec/frontend/product_analytics/mock_data.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_sdk_details_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/onboarding_list_item_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/onboarding_state_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/clear_project_settings_modal_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/gitlab_managed_provider_card_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/provider_selection_view_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_form_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_preview_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/self_managed_provider_card_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/components/provider/utils_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/onboarding_setup_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/onboarding_view_spec.js delete mode 100644 ee/spec/frontend/product_analytics/onboarding/settings_instrumentation_instructions_spec.js delete mode 100644 ee/spec/frontend/product_analytics/shared/analytics_clipboard_input_spec.js diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/dashboards_list.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/dashboards_list.vue index 18ccbb63671608..8005f38dadd95c 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/dashboards_list.vue +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/dashboards_list.vue @@ -1,5 +1,5 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue deleted file mode 100644 index 3eb2490a89cfbf..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_list_item.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_list_item.vue deleted file mode 100644 index 652281801e64e0..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_list_item.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_state.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_state.vue deleted file mode 100644 index 062264cab6bf35..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/onboarding_state.vue +++ /dev/null @@ -1,102 +0,0 @@ - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue deleted file mode 100644 index 620cff34a8e6c1..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/constants.js b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/constants.js deleted file mode 100644 index c834c79bf6f777..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/constants.js +++ /dev/null @@ -1,5 +0,0 @@ -export const FORM_FIELD_PRODUCT_ANALYTICS_CONFIGURATOR_CONNECTION_STRING = - 'productAnalyticsConfiguratorConnectionString'; -export const FORM_FIELD_PRODUCT_ANALYTICS_DATA_COLLECTOR_HOST = 'productAnalyticsDataCollectorHost'; -export const FORM_FIELD_CUBE_API_BASE_URL = 'cubeApiBaseUrl'; -export const FORM_FIELD_CUBE_API_KEY = 'cubeApiKey'; diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue deleted file mode 100644 index b7361e0bdd9907..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue +++ /dev/null @@ -1,140 +0,0 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_selection_view.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_selection_view.vue deleted file mode 100644 index 3f4a133a1de243..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_selection_view.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_form.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_form.vue deleted file mode 100644 index b2b9a5efdfa2b4..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_form.vue +++ /dev/null @@ -1,209 +0,0 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_preview.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_preview.vue deleted file mode 100644 index be67ed4c3e0bbf..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/provider_settings_preview.vue +++ /dev/null @@ -1,69 +0,0 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/self_managed_provider_card.vue b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/self_managed_provider_card.vue deleted file mode 100644 index 95f76262feca57..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/self_managed_provider_card.vue +++ /dev/null @@ -1,185 +0,0 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/utils.js b/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/utils.js deleted file mode 100644 index f0e13cf0274439..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/components/providers/utils.js +++ /dev/null @@ -1,74 +0,0 @@ -import { __ } from '~/locale'; -import { isAbsolute, isValidURL } from '~/lib/utils/url_utility'; -import getProductAnalyticsProjectSettings from 'ee/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql'; -import { - FORM_FIELD_CUBE_API_BASE_URL, - FORM_FIELD_CUBE_API_KEY, - FORM_FIELD_PRODUCT_ANALYTICS_CONFIGURATOR_CONNECTION_STRING, - FORM_FIELD_PRODUCT_ANALYTICS_DATA_COLLECTOR_HOST, -} from 'ee/product_analytics/onboarding/components/providers/constants'; - -export function projectSettingsValidator(prop) { - const expected = [ - FORM_FIELD_PRODUCT_ANALYTICS_CONFIGURATOR_CONNECTION_STRING, - FORM_FIELD_PRODUCT_ANALYTICS_DATA_COLLECTOR_HOST, - FORM_FIELD_CUBE_API_BASE_URL, - FORM_FIELD_CUBE_API_KEY, - ]; - - return ( - Object.keys(prop).length === expected.length && - expected.every((key) => key in prop && (typeof prop[key] === 'string' || prop[key] === null)) - ); -} - -export function getProjectSettingsValidationErrors(formValues) { - const errors = {}; - - const requiresUrl = [ - FORM_FIELD_PRODUCT_ANALYTICS_CONFIGURATOR_CONNECTION_STRING, - FORM_FIELD_PRODUCT_ANALYTICS_DATA_COLLECTOR_HOST, - FORM_FIELD_CUBE_API_BASE_URL, - ]; - for (const key of requiresUrl) { - if (formValues[key] && (!isValidURL(formValues[key]) || !isAbsolute(formValues[key]))) { - errors[key] = __('Enter a valid URL'); - } - } - - const required = [ - FORM_FIELD_PRODUCT_ANALYTICS_CONFIGURATOR_CONNECTION_STRING, - FORM_FIELD_PRODUCT_ANALYTICS_DATA_COLLECTOR_HOST, - FORM_FIELD_CUBE_API_BASE_URL, - FORM_FIELD_CUBE_API_KEY, - ]; - for (const key of required) { - if (!formValues[key]) { - errors[key] = __('This field is required'); - } - } - - return errors; -} - -export function updateProjectSettingsApolloCache(apolloStore, projectPath, updatedProjectSettings) { - const cacheData = apolloStore.readQuery({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath }, - }); - - apolloStore.writeQuery({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath }, - data: { - ...cacheData, - project: { - ...cacheData.project, - productAnalyticsSettings: { - ...cacheData.project.productAnalyticsSettings, - ...updatedProjectSettings, - }, - }, - }, - }); -} diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/constants.js b/ee/app/assets/javascripts/product_analytics/onboarding/constants.js deleted file mode 100644 index 13f0ce573fe220..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/constants.js +++ /dev/null @@ -1,37 +0,0 @@ -export const INSTALL_NPM_PACKAGE = `yarn add @gitlab/application-sdk-browser - -OR - -npm install @gitlab/application-sdk-browser`; - -export const IMPORT_NPM_PACKAGE = `// import as an ES module -import { glClientSDK } from '@gitlab/application-sdk-browser'; - -OR - -// import as a CommonJS module -const { glClientSDK } = require('@gitlab/application-sdk-browser'); -`; - -export const INIT_TRACKING = `this.glClient = glClientSDK({ appId: '%{appId}', host: '%{host}' });`; - -export const HTML_SCRIPT_SETUP = ` -`; - -export const SHORT_POLLING_INTERVAL = 1000; - -export const LONG_POLLING_INTERVAL = 2500; - -export const STATE_CREATE_INSTANCE = 'CREATE_INSTANCE'; - -export const STATE_LOADING_INSTANCE = 'LOADING_INSTANCE'; - -export const STATE_WAITING_FOR_EVENTS = 'WAITING_FOR_EVENTS'; - -export const STATE_COMPLETE = 'COMPLETE'; diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/index.js b/ee/app/assets/javascripts/product_analytics/onboarding/index.js deleted file mode 100644 index bba50479f79147..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import Vue from 'vue'; - -import ProductAnalyticsSettingsInstrumentationInstructions from './settings_instrumentation_instructions.vue'; - -export function initProductAnalyticsSettingsInstrumentationInstructions() { - const el = document.getElementById('js-product-analytics-instrumentation-settings'); - if (!el) { - return null; - } - - const { collectorHost, trackingKey, dashboardsPath, onboardingPath } = el.dataset; - - return new Vue({ - el, - provide: { - collectorHost, - }, - render: (createElement) => - createElement(ProductAnalyticsSettingsInstrumentationInstructions, { - props: { - trackingKey, - dashboardsPath, - onboardingPath, - }, - }), - }); -} diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_setup.vue b/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_setup.vue deleted file mode 100644 index 9bf3f7550ec99e..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_setup.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_view.vue b/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_view.vue deleted file mode 100644 index 3930e4594d7ffd..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/onboarding_view.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/product_analytics/onboarding/settings_instrumentation_instructions.vue b/ee/app/assets/javascripts/product_analytics/onboarding/settings_instrumentation_instructions.vue deleted file mode 100644 index c0261ebea7fd1d..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/onboarding/settings_instrumentation_instructions.vue +++ /dev/null @@ -1,76 +0,0 @@ - - diff --git a/ee/app/assets/javascripts/product_analytics/shared/analytics_clipboard_input.vue b/ee/app/assets/javascripts/product_analytics/shared/analytics_clipboard_input.vue deleted file mode 100644 index ad08f746f527f8..00000000000000 --- a/ee/app/assets/javascripts/product_analytics/shared/analytics_clipboard_input.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/dashboards_list_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/dashboards_list_spec.js index 1e47bd1a45bee7..6ba86df037cf6c 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/dashboards_list_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/dashboards_list_spec.js @@ -2,7 +2,6 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { GlSprintf } from '@gitlab/ui'; import { mockTracking } from 'helpers/tracking_helper'; -import ProductAnalyticsOnboarding from 'ee/product_analytics/onboarding/components/onboarding_list_item.vue'; import DashboardsList from 'ee/analytics/analytics_dashboards/components/dashboards_list.vue'; import DashboardListItem from 'ee/analytics/analytics_dashboards/components/list/dashboard_list_item.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -41,7 +40,6 @@ describe('DashboardsList', () => { const findListItems = () => wrapper.findAllComponents(DashboardListItem); const findLoadingStateList = () => wrapper.findComponent(ResourceListsLoadingStateList); - const findProductAnalyticsOnboarding = () => wrapper.findComponent(ProductAnalyticsOnboarding); const findPageTitle = () => wrapper.findByTestId('page-heading'); const findPageDescription = () => wrapper.findByTestId('page-heading-description'); const findHelpLink = () => wrapper.findByTestId('help-link'); @@ -135,26 +133,6 @@ describe('DashboardsList', () => { }); }); - describe('for projects', () => { - describe('when custom dashboards project is configured', () => { - describe('when product analytics is onboarded', () => { - beforeEach(async () => { - mockAnalyticsDashboardsHandler = jest - .fn() - .mockResolvedValue(TEST_ALL_DASHBOARDS_GRAPHQL_SUCCESS_RESPONSE); - - createWrapper({ - features: ['productAnalytics'], - }); - - await waitForPromises(); - - findProductAnalyticsOnboarding().vm.$emit('complete'); - }); - }); - }); - }); - describe('for groups', () => { describe('with successful dashboards query', () => { beforeEach(() => { @@ -180,67 +158,6 @@ describe('DashboardsList', () => { }); }); - describe('when the product analytics feature is enabled', () => { - beforeEach(() => { - mockAnalyticsDashboardsHandler = jest - .fn() - .mockResolvedValue(TEST_ALL_DASHBOARDS_GRAPHQL_SUCCESS_RESPONSE); - - createWrapper({ features: ['productAnalytics'] }); - }); - - it('renders the onboarding component', () => { - expect(findProductAnalyticsOnboarding().exists()).toBe(true); - }); - - describe('when the onboarding component emits "complete"', () => { - beforeEach(async () => { - await waitForPromises(); - - findProductAnalyticsOnboarding().vm.$emit('complete'); - }); - - it('removes the onboarding component from the DOM', () => { - expect(findProductAnalyticsOnboarding().exists()).toBe(false); - }); - - it('refetches the list of dashboards', () => { - expect(mockAnalyticsDashboardsHandler).toHaveBeenCalledTimes(2); - }); - - it('renders a loading state', () => { - expect(findLoadingStateList().exists()).toBe(true); - }); - }); - - describe('when the onboarding component emits "error"', () => { - const message = 'some error'; - const error = new Error(message); - - beforeEach(async () => { - await waitForPromises(); - - findProductAnalyticsOnboarding().vm.$emit('error', error, true, message); - }); - - it('creates an alert for the error', () => { - expect(createAlert).toHaveBeenCalledWith({ - captureError: true, - message, - error, - }); - }); - - it('dismisses the alert when the component is destroyed', async () => { - wrapper.destroy(); - - await nextTick(); - - expect(mockAlertDismiss).toHaveBeenCalled(); - }); - }); - }); - describe('when the list of dashboards have been fetched', () => { const setupWithResponse = (mockResponseVal) => { mockAnalyticsDashboardsHandler = jest.fn().mockResolvedValue(mockResponseVal); diff --git a/ee/spec/frontend/analytics/analytics_dashboards/router_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/router_spec.js index daa36de90d959f..0942b0f4dd5e8d 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/router_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/router_spec.js @@ -1,8 +1,6 @@ import createRouter from 'ee/analytics/analytics_dashboards/router'; import DashboardsList from 'ee/analytics/analytics_dashboards/components/dashboards_list.vue'; import AnalyticsDashboard from 'ee/analytics/analytics_dashboards/components/analytics_dashboard.vue'; -import ProductAnalyticsOnboardingView from 'ee/product_analytics/onboarding/onboarding_view.vue'; -import ProductAnalyticsOnboardingSetup from 'ee/product_analytics/onboarding/onboarding_setup.vue'; describe('Dashboards list router', () => { const base = '/dashboard'; @@ -30,12 +28,10 @@ describe('Dashboards list router', () => { }); it.each` - path | component | name - ${'/'} | ${DashboardsList} | ${'Analytics dashboards'} - ${'/product-analytics-onboarding'} | ${ProductAnalyticsOnboardingView} | ${'Product analytics onboarding'} - ${'/product-analytics-setup'} | ${ProductAnalyticsOnboardingSetup} | ${'Product analytics onboarding'} - ${'/test-dashboard-1'} | ${AnalyticsDashboard} | ${'Test dashboard 1'} - ${'/test-dashboard-2'} | ${AnalyticsDashboard} | ${'Test dashboard 2'} + path | component | name + ${'/'} | ${DashboardsList} | ${'Analytics dashboards'} + ${'/test-dashboard-1'} | ${AnalyticsDashboard} | ${'Test dashboard 1'} + ${'/test-dashboard-2'} | ${AnalyticsDashboard} | ${'Test dashboard 2'} `('sets component as $component.name for path "$path"', async ({ path, component, name }) => { breadcrumbState.name = name; @@ -71,21 +67,4 @@ describe('Dashboards list router', () => { expect(router.currentRoute.meta.root).toBe(true); }); }); - - describe('when user does not have permission to configure project settings', () => { - beforeEach(() => { - router = createRouter(base, breadcrumbState, { canConfigureProjectSettings: false }); - }); - - it.each(['/product-analytics-onboarding', '/product-analytics-setup'])( - 'does not include the "%s" route', - (onboardingRoute) => { - const productAnalyticsOnboardingRoute = router.options.routes.find( - (route) => route.path === onboardingRoute, - ); - - expect(productAnalyticsOnboardingRoute).toBeUndefined(); - }, - ); - }); }); diff --git a/ee/spec/frontend/product_analytics/mock_data.js b/ee/spec/frontend/product_analytics/mock_data.js deleted file mode 100644 index 8a56661fbc3e9a..00000000000000 --- a/ee/spec/frontend/product_analytics/mock_data.js +++ /dev/null @@ -1,86 +0,0 @@ -export const TEST_PROJECT_FULL_PATH = 'group-1/project-1'; - -export const TEST_PROJECT_ID = '2'; - -export const createInstanceResponse = (errors = []) => ({ - data: { - projectInitializeProductAnalytics: { - project: { - id: 'gid://gitlab/Project/2', - fullPath: '', - }, - errors, - }, - }, -}); - -export const getTrackingKeyResponse = (trackingKey = null) => ({ - data: { - project: { - id: 'gid://gitlab/Project/2', - trackingKey, - }, - }, -}); - -export const getProductAnalyticsStateResponse = (productAnalyticsState = null) => ({ - data: { - project: { - id: 'gid://gitlab/Project/2', - productAnalyticsState, - }, - }, -}); - -export const getProductAnalyticsProjectSettingsUpdateResponse = ( - updatedSettings = { - productAnalyticsConfiguratorConnectionString: null, - productAnalyticsDataCollectorHost: null, - cubeApiBaseUrl: null, - cubeApiKey: null, - }, - errors = [], -) => ({ - data: { - productAnalyticsProjectSettingsUpdate: { - __typename: 'ProductAnalyticsProjectSettingsUpdatePayload', - errors, - ...updatedSettings, - }, - }, -}); - -export const getProjectLevelAnalyticsProviderSettings = () => ({ - productAnalyticsConfiguratorConnectionString: 'https://configurator.example.com', - productAnalyticsDataCollectorHost: 'https://collector.example.com', - cubeApiBaseUrl: 'https://cubejs.example.com', - cubeApiKey: 'abc-123', -}); - -export const getPartialProjectLevelAnalyticsProviderSettings = () => ({ - productAnalyticsConfiguratorConnectionString: null, - productAnalyticsDataCollectorHost: null, - cubeApiBaseUrl: 'https://cubejs.example.com', - cubeApiKey: 'abc-123', -}); - -export const getEmptyProjectLevelAnalyticsProviderSettings = () => ({ - productAnalyticsConfiguratorConnectionString: null, - productAnalyticsDataCollectorHost: null, - cubeApiBaseUrl: null, - cubeApiKey: null, -}); - -export const getProductAnalyticsProjectSettingsResponse = ( - settings = getEmptyProjectLevelAnalyticsProviderSettings(), - projectId = 'gid://gitlab/Project/2', -) => ({ - data: { - project: { - id: projectId, - productAnalyticsSettings: { - ...settings, - }, - }, - }, -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_sdk_details_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_sdk_details_spec.js deleted file mode 100644 index 6711d08bc45c67..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_sdk_details_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import InstrumentationInstructionsSdkDetails from 'ee/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { - TEST_COLLECTOR_HOST, - TEST_TRACKING_KEY, -} from 'ee_jest/analytics/analytics_dashboards/mock_data'; - -describe('ProductAnalyticsInstrumentationInstructionsSdkDetails', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const findInputByValue = (value) => wrapper.findByDisplayValue(value); - - const createWrapper = (props = {}, provide = {}) => { - wrapper = mountExtended(InstrumentationInstructionsSdkDetails, { - propsData: { - trackingKey: TEST_TRACKING_KEY, - ...props, - }, - provide: { - collectorHost: TEST_COLLECTOR_HOST, - ...provide, - }, - }); - }; - - describe('when mounted', () => { - beforeEach(() => createWrapper()); - - it('renders the SDK host', () => { - expect(findInputByValue(TEST_COLLECTOR_HOST).exists()).toBe(true); - }); - - it('renders the SDK appId', () => { - expect(findInputByValue(TEST_TRACKING_KEY).exists()).toBe(true); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_spec.js deleted file mode 100644 index 3ffc1110c2b1cb..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/instrumentation_instructions_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import InstrumentationInstructions from 'ee/product_analytics/onboarding/components/instrumentation_instructions.vue'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { mockTracking } from 'helpers/tracking_helper'; -import { IMPORT_NPM_PACKAGE, INSTALL_NPM_PACKAGE } from 'ee/product_analytics/onboarding/constants'; -import { - TEST_COLLECTOR_HOST, - TEST_TRACKING_KEY, -} from 'ee_jest/analytics/analytics_dashboards/mock_data'; - -describe('ProductAnalyticsInstrumentationInstructions', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - let trackingSpy; - - const findNpmInstructions = () => wrapper.findByTestId('npm-instrumentation-instructions'); - const findHtmlInstructions = () => wrapper.findByTestId('html-instrumentation-instructions'); - const findFurtherBrowserSDKInfo = () => wrapper.findByTestId('further-browser-sdk-info'); - const findSummaryText = () => wrapper.findByTestId('summary-text'); - - const createWrapper = (mountFn = shallowMountExtended) => { - trackingSpy = mockTracking(undefined, window.document, jest.spyOn); - wrapper = mountFn(InstrumentationInstructions, { - propsData: { - trackingKey: TEST_TRACKING_KEY, - dashboardsPath: '/foo/bar/dashboards', - }, - provide: { - collectorHost: TEST_COLLECTOR_HOST, - }, - }); - }; - - describe('when mounted', () => { - beforeEach(() => createWrapper()); - - it('renders the expected instructions', () => { - createWrapper(mountExtended); - - const expectedAppIdFragment = `appId: '${TEST_TRACKING_KEY}'`; - const expectedHostFragment = `host: '${TEST_COLLECTOR_HOST}'`; - - const npmInstructions = findNpmInstructions().text(); - expect(npmInstructions).toContain(INSTALL_NPM_PACKAGE); - expect(npmInstructions).toContain(IMPORT_NPM_PACKAGE); - expect(npmInstructions).toContain(expectedAppIdFragment); - expect(npmInstructions).toContain(expectedHostFragment); - - const htmlInstructions = findHtmlInstructions().text(); - expect(htmlInstructions).toContain(expectedAppIdFragment); - expect(htmlInstructions).toContain(expectedHostFragment); - }); - - it('tracks that instrumentation instructions has been viewed', () => { - createWrapper(); - - expect(trackingSpy).toHaveBeenCalledWith( - undefined, - 'user_viewed_instrumentation_directions', - expect.any(Object), - ); - }); - - describe('static text', () => { - it('renders the further browser SDK info text', () => { - expect(findFurtherBrowserSDKInfo().attributes('message')).toBe( - 'For more information, see the %{linkStart}docs%{linkEnd}.', - ); - }); - - it('renders the summary text', () => { - expect(findSummaryText().attributes('message')).toBe( - 'After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}.', - ); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/onboarding_list_item_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/onboarding_list_item_spec.js deleted file mode 100644 index 3ebabf513cf864..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/onboarding_list_item_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import OnboardingListItem from 'ee/product_analytics/onboarding/components/onboarding_list_item.vue'; -import OnboardingState from 'ee/product_analytics/onboarding/components/onboarding_state.vue'; -import AnalyticsFeatureListItem from 'ee/analytics/analytics_dashboards/components/list/feature_list_item.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import { - STATE_CREATE_INSTANCE, - STATE_LOADING_INSTANCE, - STATE_WAITING_FOR_EVENTS, -} from 'ee/product_analytics/onboarding/constants'; - -import { TEST_PROJECT_FULL_PATH } from '../../mock_data'; - -describe('OnboardingListItem', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const findListItem = () => wrapper.findComponent(AnalyticsFeatureListItem); - const findState = () => wrapper.findComponent(OnboardingState); - - const createWrapper = (state, provide = {}) => { - wrapper = shallowMountExtended(OnboardingListItem, { - provide: { - canConfigureProjectSettings: true, - namespaceFullPath: TEST_PROJECT_FULL_PATH, - ...provide, - }, - }); - - return findState().vm.$emit('change', state); - }; - - describe('default behaviour', () => { - beforeEach(() => { - return createWrapper(STATE_CREATE_INSTANCE); - }); - - it('renders the list item', () => { - expect(findListItem().props()).toMatchObject({ - title: 'Product Analytics', - description: - 'Track the performance of your product, and optimize your product and development processes.', - badgeText: null, - badgePopoverText: null, - to: 'product-analytics-onboarding', - }); - }); - - describe('and the state is complete', () => { - beforeEach(() => { - return findState().vm.$emit('complete'); - }); - - it('emits the complete event', () => { - expect(wrapper.emitted('complete')).toEqual([[]]); - }); - }); - - describe('and the state emitted an error', () => { - const error = new Error('error'); - - beforeEach(() => { - return findState().vm.$emit('error', error); - }); - - it('emits an error event with a message', () => { - expect(wrapper.emitted('error')).toEqual([ - [error, true, 'An error occurred while fetching data. Refresh the page to try again.'], - ]); - }); - }); - }); - - describe('badge text', () => { - it.each` - state | badgeText | badgePopoverText - ${STATE_WAITING_FOR_EVENTS} | ${'Waiting for events'} | ${'An analytics provider has been successfully created, but it has not received any events yet. To continue with the setup, instrument your application and start sending events.'} - ${STATE_LOADING_INSTANCE} | ${'Loading instance'} | ${'The system is creating your analytics provider. In the meantime, you can instrument your application.'} - `( - 'renders "$badgeText" with popover "$badgePopoverText" when the state is "$state"', - async ({ state, badgeText, badgePopoverText }) => { - await createWrapper(state); - - expect(findListItem().props()).toMatchObject( - expect.objectContaining({ - badgeText, - badgePopoverText, - }), - ); - }, - ); - }); - - describe('action text', () => { - it.each` - state | actionText - ${STATE_CREATE_INSTANCE} | ${'Set up'} - ${STATE_WAITING_FOR_EVENTS} | ${'Continue set up'} - ${STATE_LOADING_INSTANCE} | ${'Continue set up'} - `( - 'renders action text "$actionText" when the state is "$state"', - async ({ state, actionText }) => { - await createWrapper(state); - - expect(findListItem().props('actionText')).toBe(actionText); - }, - ); - }); - - describe('when user does not have required permissions', () => { - beforeEach(() => { - createWrapper(STATE_CREATE_INSTANCE, { - canConfigureProjectSettings: false, - }); - }); - - it('does disables the setup button', () => { - expect(findListItem().props('actionDisabled')).toBe(true); - }); - - it('renders a badge informing the user they have insufficient permissions', () => { - expect(findListItem().props()).toMatchObject( - expect.objectContaining({ - badgeText: 'Additional permissions required', - badgePopoverText: - 'Contact the GitLab administrator or project maintainer to onboard this project with product analytics. %{linkStart}Learn more%{linkEnd}.', - badgePopoverLink: - '/help/development/internal_analytics/product_analytics#onboard-a-gitlab-project', - }), - ); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/onboarding_state_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/onboarding_state_spec.js deleted file mode 100644 index 4959b05bdbfb30..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/onboarding_state_spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import VueApollo from 'vue-apollo'; -import Vue from 'vue'; -import OnboardingState from 'ee/product_analytics/onboarding/components/onboarding_state.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import getProductAnalyticsState from 'ee/product_analytics/graphql/queries/get_product_analytics_state.query.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; - -import { - STATE_CREATE_INSTANCE, - SHORT_POLLING_INTERVAL, - LONG_POLLING_INTERVAL, - STATE_LOADING_INSTANCE, - STATE_WAITING_FOR_EVENTS, - STATE_COMPLETE, -} from 'ee/product_analytics/onboarding/constants'; - -import { TEST_PROJECT_FULL_PATH, getProductAnalyticsStateResponse } from '../../mock_data'; - -Vue.use(VueApollo); - -describe('OnboardingState', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const fatalError = new Error('GraphQL networkError'); - const mockApolloFatalError = jest.fn().mockRejectedValue(fatalError); - const createApolloSuccess = (state) => - jest.fn().mockResolvedValue(getProductAnalyticsStateResponse(state)); - - const createWrapper = ({ props = {}, apolloMock } = {}) => { - wrapper = shallowMountExtended(OnboardingState, { - apolloProvider: createMockApollo([[getProductAnalyticsState, apolloMock]]), - provide: { - namespaceFullPath: TEST_PROJECT_FULL_PATH, - }, - propsData: { - stateProp: '', - pollState: false, - ...props, - }, - }); - }; - - const advanceApolloTimers = async () => { - jest.runOnlyPendingTimers(); - await waitForPromises(); - }; - - describe('default behaviour', () => { - beforeEach(() => { - createWrapper({ apolloMock: createApolloSuccess(STATE_CREATE_INSTANCE) }); - return waitForPromises(); - }); - - it('fetches the state and emits a change event', () => { - expect(wrapper.emitted('change')).toEqual([[STATE_CREATE_INSTANCE]]); - }); - }); - - describe(`when the response is ${STATE_COMPLETE}`, () => { - beforeEach(() => { - createWrapper({ apolloMock: createApolloSuccess(STATE_COMPLETE) }); - return waitForPromises(); - }); - - it('emits a "complete" event', () => { - expect(wrapper.emitted('complete')).toEqual([[]]); - }); - }); - - describe('when a fatal error occurs', () => { - beforeEach(() => { - createWrapper({ - props: { stateProp: STATE_LOADING_INSTANCE }, - apolloMock: mockApolloFatalError, - }); - return waitForPromises(); - }); - - it('emits an "error" event with the captured error', () => { - expect(wrapper.emitted('error')).toEqual([[fatalError]]); - }); - - it('does not poll the query resource', async () => { - await advanceApolloTimers(); - - expect(mockApolloFatalError).toHaveBeenCalledTimes(1); - }); - }); - - describe('polling', () => { - const mock = createApolloSuccess(STATE_CREATE_INSTANCE); - - it('polls when pollState is true', async () => { - createWrapper({ - props: { stateProp: STATE_CREATE_INSTANCE, pollState: true }, - apolloMock: mock, - }); - - await waitForPromises(); - - expect(mock).toHaveBeenCalledTimes(1); - }); - - describe.each` - state | polls | pollInterval - ${''} | ${false} | ${0} - ${STATE_CREATE_INSTANCE} | ${false} | ${0} - ${STATE_LOADING_INSTANCE} | ${true} | ${SHORT_POLLING_INTERVAL} - ${STATE_WAITING_FOR_EVENTS} | ${true} | ${LONG_POLLING_INTERVAL} - ${STATE_COMPLETE} | ${false} | ${0} - `('when the state is "$state"', ({ state, polls, pollInterval }) => { - beforeEach(() => { - createWrapper({ props: { stateProp: state }, apolloMock: mock }); - return waitForPromises(); - }); - - it(`${polls ? 'polls' : 'does not poll'} the query resource`, async () => { - await advanceApolloTimers(); - - expect(mock).toHaveBeenCalledTimes(polls ? 2 : 1); - }); - - it(`sets the poll interval to ${pollInterval}`, () => { - expect(wrapper.vm.$apollo.queries.state.pollInterval).toBe(pollInterval); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/clear_project_settings_modal_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/clear_project_settings_modal_spec.js deleted file mode 100644 index 7cea08377cbdd6..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/clear_project_settings_modal_spec.js +++ /dev/null @@ -1,224 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; - -import { GlLink, GlModal, GlSprintf } from '@gitlab/ui'; - -import * as Sentry from '~/sentry/sentry_browser_wrapper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import ClearProjectSettingsModal from 'ee/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue'; -import productAnalyticsProjectSettingsUpdate from 'ee/product_analytics/graphql/mutations/product_analytics_project_settings_update.mutation.graphql'; -import getProductAnalyticsProjectSettings from 'ee/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql'; -import { - getEmptyProjectLevelAnalyticsProviderSettings, - getPartialProjectLevelAnalyticsProviderSettings, - getProductAnalyticsProjectSettingsUpdateResponse, - TEST_PROJECT_FULL_PATH, - TEST_PROJECT_ID, -} from '../../../mock_data'; - -Vue.use(VueApollo); - -jest.mock('~/sentry/sentry_browser_wrapper'); - -describe('ClearProjectSettingsModal', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - let mockApollo; - - const mockGetProjectSettings = jest.fn(); - const mockMutate = jest.fn(); - - const findModal = () => wrapper.findComponent(GlModal); - const findModalError = () => wrapper.findByTestId('modal-error'); - - const createWrapper = (props = {}, provide = {}) => { - mockApollo = createMockApollo([ - [getProductAnalyticsProjectSettings, mockGetProjectSettings], - [productAnalyticsProjectSettingsUpdate, mockMutate], - ]); - - wrapper = shallowMountExtended(ClearProjectSettingsModal, { - apolloProvider: mockApollo, - propsData: { - visible: true, - ...props, - }, - provide: { - analyticsSettingsPath: `/${TEST_PROJECT_FULL_PATH}/-/settings/analytics`, - namespaceFullPath: TEST_PROJECT_FULL_PATH, - ...provide, - }, - stubs: { - GlSprintf, - }, - }); - }; - - const confirmRemoveSetting = async () => { - findModal().vm.$emit('primary', { preventDefault: jest.fn() }); - await nextTick(); - }; - - describe('default behaviour', () => { - beforeEach(() => createWrapper()); - - it('should render modal', () => { - expect(findModal().props('visible')).toBe(true); - }); - }); - - describe('when cancelling', () => { - beforeEach(() => { - createWrapper(); - findModal().vm.$emit('canceled'); - return nextTick(); - }); - - it('should emit "hide" event', () => { - expect(wrapper.emitted('hide')).toHaveLength(1); - }); - - it('should not modify project settings', () => { - expect(mockMutate).not.toHaveBeenCalled(); - }); - }); - - describe('when confirming', () => { - let mockWriteQuery; - beforeEach(() => { - mockWriteQuery = jest.fn(); - createWrapper(); - mockApollo.clients.defaultClient.cache.readQuery = jest.fn().mockReturnValue({ - project: { - id: TEST_PROJECT_ID, - productAnalyticsSettings: getPartialProjectLevelAnalyticsProviderSettings(), - }, - }); - mockApollo.clients.defaultClient.cache.writeQuery = mockWriteQuery; - }); - - it('should set loading state', async () => { - mockMutate.mockReturnValue(new Promise(() => {})); - await confirmRemoveSetting(); - - const modal = findModal(); - expect(modal.props('actionPrimary').attributes.loading).toBe(true); - expect(modal.props('actionCancel').attributes.disabled).toBe(true); - }); - - it('should clear settings', async () => { - mockMutate.mockResolvedValue(getProductAnalyticsProjectSettingsUpdateResponse()); - await confirmRemoveSetting(); - - expect(mockMutate).toHaveBeenCalledWith({ - fullPath: 'group-1/project-1', - productAnalyticsConfiguratorConnectionString: null, - productAnalyticsDataCollectorHost: null, - cubeApiBaseUrl: null, - cubeApiKey: null, - }); - }); - - it('updates the apollo cache after a successful mutation', async () => { - mockMutate.mockResolvedValue(getProductAnalyticsProjectSettingsUpdateResponse()); - await confirmRemoveSetting(); - await waitForPromises(); - - expect(mockWriteQuery).toHaveBeenCalledTimes(1); - expect(mockWriteQuery).toHaveBeenCalledWith({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath: TEST_PROJECT_FULL_PATH }, - data: { - project: { - id: TEST_PROJECT_ID, - productAnalyticsSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }, - }, - }); - }); - - describe('when the mutation fails', () => { - describe('with a network level error', () => { - const error = new Error('uh oh!'); - beforeEach(async () => { - mockMutate.mockRejectedValue(error); - await confirmRemoveSetting(); - return waitForPromises(); - }); - - it('should display an error', () => { - expect(findModalError().text()).toContain( - 'Failed to clear project-level settings. Please try again or clear them manually.', - ); - expect(findModalError().findComponent(GlLink).attributes('href')).toBe( - '/group-1/project-1/-/settings/analytics', - ); - }); - - it('should not show loading state', () => { - const modal = findModal(); - expect(modal.props('actionPrimary').attributes.loading).toBe(false); - expect(modal.props('actionCancel').attributes.disabled).toBe(false); - }); - - it('should log to Sentry', () => { - expect(Sentry.captureException).toHaveBeenCalledWith(error); - }); - }); - - describe('with a response error', () => { - beforeEach(async () => { - mockMutate.mockResolvedValue( - getProductAnalyticsProjectSettingsUpdateResponse( - getEmptyProjectLevelAnalyticsProviderSettings(), - [new Error('uh oh!')], - ), - ); - await confirmRemoveSetting(); - return waitForPromises(); - }); - - it('should display an error', () => { - expect(findModalError().text()).toContain( - 'Failed to clear project-level settings. Please try again or clear them manually.', - ); - expect(findModalError().findComponent(GlLink).attributes('href')).toBe( - '/group-1/project-1/-/settings/analytics', - ); - }); - - it('should not show loading state', () => { - const modal = findModal(); - expect(modal.props('actionPrimary').attributes.loading).toBe(false); - expect(modal.props('actionCancel').attributes.disabled).toBe(false); - }); - - it('should not log to Sentry', () => { - expect(Sentry.captureException).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when the settings have successfully cleared', () => { - beforeEach(async () => { - mockMutate.mockResolvedValue(getProductAnalyticsProjectSettingsUpdateResponse()); - await confirmRemoveSetting(); - await wrapper.setProps({ - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }); - return waitForPromises(); - }); - - it('should emit "hide" event', () => { - expect(wrapper.emitted('hide')).toHaveLength(1); - }); - - it('should emit "cleared" event', () => { - expect(wrapper.emitted('cleared')).toHaveLength(1); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/gitlab_managed_provider_card_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/gitlab_managed_provider_card_spec.js deleted file mode 100644 index ea8e5cc526ef07..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/gitlab_managed_provider_card_spec.js +++ /dev/null @@ -1,168 +0,0 @@ -import { nextTick } from 'vue'; - -import { GlSprintf } from '@gitlab/ui'; - -import { PROMO_URL } from '~/constants'; -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import GitlabManagedProviderCard from 'ee/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue'; -import ClearProjectSettingsModal from 'ee/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue'; -import { - getEmptyProjectLevelAnalyticsProviderSettings, - getPartialProjectLevelAnalyticsProviderSettings, -} from '../../../mock_data'; - -describe('GitlabManagedProviderCard', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const findContactSalesBtn = () => wrapper.findByTestId('contact-sales-team-btn'); - const findConnectGitLabProviderBtn = () => - wrapper.findByTestId('connect-gitlab-managed-provider-btn'); - const findRegionAgreementCheckbox = () => wrapper.findByTestId('region-agreement-checkbox'); - const findGcpZoneError = () => wrapper.findByTestId('gcp-zone-error'); - const findClearSettingsModal = () => wrapper.findComponent(ClearProjectSettingsModal); - - const createWrapper = (props = {}, provide = {}) => { - wrapper = shallowMountExtended(GitlabManagedProviderCard, { - propsData: { - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - ...props, - }, - provide: { - managedClusterPurchased: true, - ...provide, - }, - stubs: { - GlSprintf, - }, - }); - }; - - const initProvider = () => { - findRegionAgreementCheckbox().vm.$emit('input', true); - findConnectGitLabProviderBtn().vm.$emit('click'); - return waitForPromises(); - }; - - describe('default behaviour', () => { - beforeEach(() => createWrapper()); - - it('should render a title and description', () => { - expect(wrapper.text()).toContain('GitLab-managed provider'); - expect(wrapper.text()).toContain( - 'Use a GitLab-managed infrastructure to process, store, and query analytics events data.', - ); - }); - }); - - describe('when group does not have product analytics provider purchase', () => { - beforeEach(() => createWrapper({}, { managedClusterPurchased: false })); - - it('does not show the GitLab-managed provider setup button', () => { - expect(findConnectGitLabProviderBtn().exists()).toBe(false); - }); - - it('does not show the GCP zone confirmation checkbox', () => { - expect(findRegionAgreementCheckbox().exists()).toBe(false); - }); - - it('shows a link to contact sales', () => { - const btn = findContactSalesBtn(); - expect(btn.text()).toBe('Contact our sales team'); - expect(btn.attributes('href')).toBe(`${PROMO_URL}/sales/`); - }); - }); - - describe('when group has product analytics provider purchase', () => { - describe('when some project provider settings are already configured', () => { - beforeEach(() => { - const projectSettings = getPartialProjectLevelAnalyticsProviderSettings(); - createWrapper({ - projectSettings, - }); - }); - - describe('when clicking setup', () => { - it('should show the clear settings modal', async () => { - createWrapper({ - projectSettings: getPartialProjectLevelAnalyticsProviderSettings(), - }); - - await initProvider(); - - const modal = findClearSettingsModal(); - expect(modal.props('visible')).toBe(true); - expect(modal.text()).toContain( - 'This project has analytics provider settings configured. If you continue, the settings for projects will be reset so that GitLab-managed provider settings can be used.', - ); - }); - - it('should hide the modal when it emits "hide"', async () => { - await initProvider(); - - findClearSettingsModal().vm.$emit('hide'); - await nextTick(); - - expect(findClearSettingsModal().props('visible')).toBe(false); - }); - - it('should select the provider when the modal emits "cleared"', async () => { - await initProvider(); - - await wrapper.setProps({ - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }); - findClearSettingsModal().vm.$emit('cleared'); - await nextTick(); - - expect(wrapper.emitted('confirm')).toEqual([['file-mock']]); - }); - }); - }); - - describe('when project has no existing settings configured', () => { - beforeEach(() => - createWrapper({ - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }), - ); - - describe('when initialising without agreeing to region', () => { - beforeEach(() => { - findConnectGitLabProviderBtn().vm.$emit('click'); - return waitForPromises(); - }); - - it('should show an error', () => { - expect(findGcpZoneError().text()).toBe( - 'To continue, you must agree to event storage and processing in this region.', - ); - }); - - it('should not emit "confirm" event', () => { - expect(wrapper.emitted('confirm')).toBeUndefined(); - }); - - describe('when agreeing to region', () => { - beforeEach(() => { - const checkbox = findRegionAgreementCheckbox(); - checkbox.vm.$emit('input', true); - - findConnectGitLabProviderBtn().vm.$emit('click'); - return waitForPromises(); - }); - - it('should clear the error message', () => { - expect(findGcpZoneError().exists()).toBe(false); - }); - - it('should emit "confirm" event', () => { - expect(wrapper.emitted('confirm')).toEqual([['file-mock']]); - }); - }); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_selection_view_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_selection_view_spec.js deleted file mode 100644 index f20d380d0cffdf..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_selection_view_spec.js +++ /dev/null @@ -1,247 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; - -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { visitUrl } from '~/lib/utils/url_utility'; - -import getProductAnalyticsProjectSettings from 'ee/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql'; -import initializeProductAnalyticsMutation from 'ee/product_analytics/graphql/mutations/initialize_product_analytics.mutation.graphql'; -import ProviderSelectionView from 'ee/product_analytics/onboarding/components/providers/provider_selection_view.vue'; -import GitLabManagedProviderCard from 'ee/product_analytics/onboarding/components/providers/gitlab_managed_provider_card.vue'; -import SelfManagedProviderCard from 'ee/product_analytics/onboarding/components/providers/self_managed_provider_card.vue'; - -import { - createInstanceResponse, - getEmptyProjectLevelAnalyticsProviderSettings, - getProductAnalyticsProjectSettingsResponse, -} from '../../../mock_data'; - -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - visitUrl: jest.fn(), -})); - -Vue.use(VueApollo); - -describe('ProviderSelectionView', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const fatalError = new Error('GraphQL networkError'); - const apiErrorMsg = 'Product analytics initialization is already complete'; - const mockCreateInstanceSuccess = jest.fn().mockResolvedValue(createInstanceResponse([])); - const mockCreateInstanceError = jest - .fn() - .mockResolvedValue(createInstanceResponse([apiErrorMsg])); - const mockCreateInstanceFatalError = jest.fn().mockRejectedValue(fatalError); - const getProductAnalyticsSettingsMock = jest - .fn() - .mockResolvedValue(getProductAnalyticsProjectSettingsResponse()); - - const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); - const findInstanceLoadingState = () => - wrapper.findByTestId('provider-selection-instance-loading'); - const findHelpLink = () => wrapper.findComponent(GlLink); - const findProviderCardSkeletonLoaders = () => - wrapper.findAllByTestId('provider-card-skeleton-loader'); - const findSelfManagedProviderCard = () => wrapper.findComponent(SelfManagedProviderCard); - const findGitLabManagedProviderCard = () => wrapper.findComponent(GitLabManagedProviderCard); - const findErrorAlert = () => wrapper.findByTestId('provider-settings-error-alert'); - - const createWrapper = (createInstanceMock = mockCreateInstanceSuccess, provide = {}) => { - wrapper = shallowMountExtended(ProviderSelectionView, { - apolloProvider: createMockApollo([ - [initializeProductAnalyticsMutation, createInstanceMock], - [getProductAnalyticsProjectSettings, getProductAnalyticsSettingsMock], - ]), - propsData: { - loadingInstance: false, - }, - provide: { - analyticsSettingsPath: '/settings/analytics', - canSelectGitlabManagedProvider: true, - namespaceFullPath: 'group/project', - projectLevelAnalyticsProviderSettings: {}, - ...provide, - }, - stubs: { - GlSprintf, - }, - }); - }; - - describe('default behaviour', () => { - describe('while project settings are loading', () => { - beforeEach(() => createWrapper()); - - it('should render a description', () => { - expect(wrapper.text()).toContain( - 'Set up Product Analytics to track how your product is performing. Combine analytics with your GitLab data to better understand where you can improve your product and development processes.', - ); - }); - - it('should show provider cards loading state', () => { - expect(findProviderCardSkeletonLoaders()).toHaveLength(2); - - expect(findSelfManagedProviderCard().exists()).toBe(false); - expect(findGitLabManagedProviderCard().exists()).toBe(false); - }); - - it('does not render the instance loading state', () => { - expect(findInstanceLoadingState().exists()).toBe(false); - }); - - it('renders the help link', () => { - expect(findHelpLink().attributes('href')).toBe( - '/help/development/internal_analytics/product_analytics#onboard-a-gitlab-project', - ); - }); - }); - - describe('once project settings have loaded', () => { - describe('when response is successful', () => { - let projectSettings; - - beforeEach(() => { - projectSettings = getEmptyProjectLevelAnalyticsProviderSettings(); - getProductAnalyticsSettingsMock.mockResolvedValue( - getProductAnalyticsProjectSettingsResponse(projectSettings), - ); - createWrapper(); - return waitForPromises(); - }); - - it('should show provider cards', () => { - expect(findProviderCardSkeletonLoaders()).toHaveLength(0); - - expect(findSelfManagedProviderCard().props()).toStrictEqual( - expect.objectContaining({ projectSettings }), - ); - expect(findGitLabManagedProviderCard().props()).toStrictEqual( - expect.objectContaining({ projectSettings }), - ); - }); - }); - - describe('when response is unsuccessful', () => { - const error = new Error('Oh no project settings failed to load!'); - - beforeEach(() => { - getProductAnalyticsSettingsMock.mockRejectedValue(error); - createWrapper(); - return waitForPromises(); - }); - - it('displays an error message', () => { - expect(findErrorAlert().text()).toBe( - 'An error occurred while fetching project settings. Refresh the page to try again.', - ); - }); - - it('does not render a title mentioning options', () => { - expect(wrapper.text()).not.toContain('Select an option'); - }); - - it('does not render the Self-managed provider card', () => { - expect(findSelfManagedProviderCard().exists()).toBe(false); - }); - - it('does not render the GitLab-managed provider card', () => { - expect(findGitLabManagedProviderCard().exists()).toBe(false); - }); - }); - }); - }); - - describe('when GitLab-managed provider is unavailable', () => { - beforeEach(() => { - createWrapper(mockCreateInstanceSuccess, { canSelectGitlabManagedProvider: false }); - }); - - it('does not render a title mentioning options', () => { - expect(wrapper.text()).not.toContain('Select an option'); - }); - - it('does not render the GitLab-managed provider card', () => { - expect(findGitLabManagedProviderCard().exists()).toBe(false); - }); - }); - - describe.each` - scenario | findComponent | loadingStateSvgPath - ${'self-managed'} | ${findSelfManagedProviderCard} | ${'/self-managed.svg'} - ${'GitLab-managed'} | ${findGitLabManagedProviderCard} | ${'/gitlab-managed.svg'} - `('$scenario', ({ findComponent, loadingStateSvgPath }) => { - describe('when component emits "confirm" event', () => { - describe('when initialization succeeds', () => { - beforeEach(async () => { - getProductAnalyticsSettingsMock.mockResolvedValue( - getProductAnalyticsProjectSettingsResponse(), - ); - createWrapper(); - await waitForPromises(); - findComponent().vm.$emit('confirm', loadingStateSvgPath); - return waitForPromises(); - }); - - it('should emit `initialized`', () => { - expect(wrapper.emitted('initialized')).toStrictEqual([[]]); - }); - - it('should show instance loading state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - title: 'Creating your product analytics instance…', - svgPath: loadingStateSvgPath, - }); - expect(findInstanceLoadingState().exists()).toBe(true); - }); - }); - - describe('when initialize fails', () => { - describe.each` - type | error | apolloMock - ${'api'} | ${new Error(apiErrorMsg)} | ${mockCreateInstanceError} - ${'fatal'} | ${fatalError} | ${mockCreateInstanceFatalError} - `('with a $type error', ({ error, apolloMock }) => { - beforeEach(async () => { - getProductAnalyticsSettingsMock.mockResolvedValue( - getProductAnalyticsProjectSettingsResponse(), - ); - createWrapper(apolloMock); - await waitForPromises(); - findComponent().vm.$emit('confirm'); - return waitForPromises(); - }); - - it('does not render the instance loading state', () => { - expect(findInstanceLoadingState().exists()).toBe(false); - }); - - it('emits the captured error', () => { - expect(wrapper.emitted('error')).toEqual([[error]]); - }); - }); - }); - }); - }); - - describe('when self-managed provider component emits "open-settings" event', () => { - beforeEach(async () => { - getProductAnalyticsSettingsMock.mockResolvedValue( - getProductAnalyticsProjectSettingsResponse(), - ); - createWrapper(); - await waitForPromises(); - - findSelfManagedProviderCard().vm.$emit('open-settings'); - return waitForPromises(); - }); - - it('should redirect the user to settings', () => { - expect(visitUrl).toHaveBeenCalledWith('/settings/analytics#js-analytics-data-sources', true); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_form_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_form_spec.js deleted file mode 100644 index dc6e2a8d2bed46..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_form_spec.js +++ /dev/null @@ -1,285 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; - -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import * as Sentry from '~/sentry/sentry_browser_wrapper'; - -import ProviderSettingsForm from 'ee/product_analytics/onboarding/components/providers/provider_settings_form.vue'; -import productAnalyticsProjectSettingsUpdate from 'ee/product_analytics/graphql/mutations/product_analytics_project_settings_update.mutation.graphql'; -import getProductAnalyticsProjectSettings from 'ee/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql'; -import { - getPartialProjectLevelAnalyticsProviderSettings, - getProductAnalyticsProjectSettingsUpdateResponse, - getProjectLevelAnalyticsProviderSettings, - TEST_PROJECT_FULL_PATH, - TEST_PROJECT_ID, -} from '../../../mock_data'; - -Vue.use(VueApollo); - -jest.mock('~/sentry/sentry_browser_wrapper'); - -describe('ProviderSettingsForm', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - let mockApollo; - let validProjectSettings; - - const mockGetProjectSettings = jest.fn(); - const mockMutate = jest.fn(); - - const findConfiguratorConnectionStringInput = () => - wrapper.findByTestId('configurator-connection-string-input'); - const findCollectorHostInput = () => wrapper.findByTestId('collector-host-input'); - const findCollectorHostFormGroup = () => wrapper.findByTestId('collector-host-form-group'); - const findCubeApiUrlInput = () => wrapper.findByTestId('cube-api-url-input'); - const findCubeApiUrlFormGroup = () => wrapper.findByTestId('cube-api-url-form-group'); - const findCubeApiKeyInput = () => wrapper.findByTestId('cube-api-key-input'); - const findSubmitButton = () => wrapper.findByTestId('submit-button'); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findModalError = () => - wrapper.findByTestId('clear-project-level-settings-confirmation-modal-error'); - - const createWrapper = (props = {}, provide = {}) => { - validProjectSettings = getProjectLevelAnalyticsProviderSettings(); - mockApollo = createMockApollo([ - [getProductAnalyticsProjectSettings, mockGetProjectSettings], - [productAnalyticsProjectSettingsUpdate, mockMutate], - ]); - - wrapper = shallowMountExtended(ProviderSettingsForm, { - apolloProvider: mockApollo, - propsData: { - projectSettings: getProjectLevelAnalyticsProviderSettings(), - ...props, - }, - provide: { - namespaceFullPath: TEST_PROJECT_FULL_PATH, - ...provide, - }, - }); - }; - - const submitForm = async () => { - findSubmitButton().vm.$emit('click'); - await nextTick(); - }; - - const expectLoadingState = (isLoading) => { - expect(findSubmitButton().props('loading')).toBe(isLoading); - - expect(findSubmitButton().props('disabled')).toBe(isLoading); - expect(findCancelButton().props('disabled')).toBe(isLoading); - - const expectedAttributeState = isLoading ? 'true' : undefined; - expect(findConfiguratorConnectionStringInput().attributes().disabled).toBe( - expectedAttributeState, - ); - expect(findCollectorHostInput().attributes().disabled).toBe(expectedAttributeState); - expect(findCubeApiUrlInput().attributes().disabled).toBe(expectedAttributeState); - expect(findCubeApiKeyInput().attributes().disabled).toBe(expectedAttributeState); - }; - - describe('default behaviour', () => { - beforeEach(() => createWrapper()); - - it('should render form fields', () => { - expect(findConfiguratorConnectionStringInput().exists()).toBe(true); - expect(findCollectorHostInput().exists()).toBe(true); - expect(findCubeApiUrlInput().exists()).toBe(true); - expect(findCubeApiKeyInput().exists()).toBe(true); - }); - - it('should populate form with existing values', () => { - expect(findConfiguratorConnectionStringInput().props('value')).toBe( - 'https://configurator.example.com', - ); - expect(findCollectorHostInput().attributes('value')).toBe('https://collector.example.com'); - expect(findCubeApiUrlInput().attributes('value')).toBe('https://cubejs.example.com'); - expect(findCubeApiKeyInput().props('value')).toBe('abc-123'); - }); - - it('should mask existing sensitive values by default', () => { - expect(findConfiguratorConnectionStringInput().props('initialVisibility')).toBe(false); - expect(findCubeApiKeyInput().props('initialVisibility')).toBe(false); - }); - }); - - describe('when cancelling', () => { - beforeEach(() => { - createWrapper(); - findCancelButton().vm.$emit('click'); - return nextTick(); - }); - - it('should emit "canceled" event', () => { - expect(wrapper.emitted('canceled')).toHaveLength(1); - }); - - it('should not modify project settings', () => { - expect(mockMutate).not.toHaveBeenCalled(); - }); - }); - - describe('with validation issues', () => { - beforeEach(async () => { - createWrapper(); - findConfiguratorConnectionStringInput().vm.$emit('input', 'not-a-valid-url'); - findCollectorHostInput().vm.$emit('input', 'not-a-valid-url'); - findCubeApiUrlInput().vm.$emit('input', ''); - findCubeApiKeyInput().vm.$emit('input', ''); - - await submitForm(); - await nextTick(); - }); - - it('should set expected validation messages', () => { - expect(findConfiguratorConnectionStringInput().attributes('invalid-feedback')).toBe( - 'Enter a valid URL', - ); - expect(findCollectorHostFormGroup().attributes('invalid-feedback')).toBe('Enter a valid URL'); - expect(findCubeApiUrlFormGroup().attributes('invalid-feedback')).toBe( - 'This field is required', - ); - expect(findCubeApiKeyInput().attributes('invalid-feedback')).toBe('This field is required'); - }); - - it('should not modify project settings', () => { - expect(mockMutate).not.toHaveBeenCalled(); - }); - }); - - describe('with valid values', () => { - let mockWriteQuery; - - beforeEach(() => { - mockWriteQuery = jest.fn(); - createWrapper(); - mockApollo.clients.defaultClient.cache.writeQuery = mockWriteQuery; - mockApollo.clients.defaultClient.cache.readQuery = jest.fn().mockReturnValue({ - project: { - id: TEST_PROJECT_ID, - productAnalyticsSettings: getPartialProjectLevelAnalyticsProviderSettings(), - }, - }); - }); - - it('should not show validation errors', () => { - expect( - findConfiguratorConnectionStringInput().attributes('invalid-feedback'), - ).toBeUndefined(); - expect(findCollectorHostFormGroup().attributes('invalid-feedback')).toBeUndefined(); - expect(findCubeApiUrlFormGroup().attributes('invalid-feedback')).toBeUndefined(); - expect(findCubeApiKeyInput().attributes('invalid-feedback')).toBeUndefined(); - }); - - it('should set loading state', async () => { - mockMutate.mockReturnValue(new Promise(() => {})); - await submitForm(); - - expectLoadingState(true); - }); - - it('should save the settings', async () => { - mockMutate.mockResolvedValue( - getProductAnalyticsProjectSettingsUpdateResponse(validProjectSettings), - ); - await submitForm(); - - expect(mockMutate).toHaveBeenCalledWith({ - fullPath: 'group-1/project-1', - productAnalyticsConfiguratorConnectionString: 'https://configurator.example.com', - productAnalyticsDataCollectorHost: 'https://collector.example.com', - cubeApiBaseUrl: 'https://cubejs.example.com', - cubeApiKey: 'abc-123', - }); - }); - - it('updates the apollo cache after a successful mutation', async () => { - mockMutate.mockResolvedValue( - getProductAnalyticsProjectSettingsUpdateResponse(validProjectSettings), - ); - await submitForm(); - await waitForPromises(); - - expect(mockWriteQuery).toHaveBeenCalledTimes(1); - expect(mockWriteQuery).toHaveBeenCalledWith({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath: TEST_PROJECT_FULL_PATH }, - data: { - project: { - id: TEST_PROJECT_ID, - productAnalyticsSettings: validProjectSettings, - }, - }, - }); - }); - - describe('when the mutation fails', () => { - describe('with a network level error', () => { - const error = new Error('uh oh!'); - beforeEach(async () => { - mockMutate.mockRejectedValue(error); - await submitForm(); - return waitForPromises(); - }); - - it('should display an error', () => { - expect(findModalError().text()).toContain( - 'Failed to update project-level settings. Please try again.', - ); - }); - - it('should not show loading state', () => { - expectLoadingState(false); - }); - - it('should log to Sentry', () => { - expect(Sentry.captureException).toHaveBeenCalledWith(error); - }); - }); - - describe('with a response error', () => { - beforeEach(async () => { - mockMutate.mockResolvedValue( - getProductAnalyticsProjectSettingsUpdateResponse(validProjectSettings, [ - new Error('uh oh!'), - ]), - ); - await submitForm(); - return waitForPromises(); - }); - - it('should display an error', () => { - expect(findModalError().text()).toContain( - 'Failed to update project-level settings. Please try again.', - ); - }); - - it('should not show loading state', () => { - expectLoadingState(false); - }); - - it('should not log to Sentry', () => { - expect(Sentry.captureException).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when the mutation succeeds', () => { - beforeEach(async () => { - mockMutate.mockResolvedValue( - getProductAnalyticsProjectSettingsUpdateResponse(validProjectSettings), - ); - await submitForm(); - return waitForPromises(); - }); - - it('should emit "saved" event', () => { - expect(wrapper.emitted('saved')).toHaveLength(1); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_preview_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_preview_spec.js deleted file mode 100644 index 031900a115d40d..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/provider_settings_preview_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import ProviderSettingsPreview from 'ee/product_analytics/onboarding/components/providers/provider_settings_preview.vue'; - -describe('ProviderSettingsPreview', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(ProviderSettingsPreview, { - propsData: { - configuratorConnectionString: 'test-connection-string', - collectorHost: 'test-collector-host', - cubeApiBaseUrl: 'test-cube-api-url', - cubeApiKey: 'test-cube-api-key', - ...props, - }, - }); - }; - - it('renders the component with the correct settings', () => { - createComponent(); - - expect(wrapper.findAll('dt')).toHaveLength(4); - expect(wrapper.findAll('dd')).toHaveLength(4); - - expect(wrapper.findAll('dt').at(0).text()).toBe('Snowplow configurator connection string'); - expect(wrapper.findAll('dd').at(0).text()).toBe('****************'); - - expect(wrapper.findAll('dt').at(1).text()).toBe('Collector host'); - expect(wrapper.findAll('dd').at(1).text()).toBe('test-collector-host'); - - expect(wrapper.findAll('dt').at(2).text()).toBe('Cube API URL'); - expect(wrapper.findAll('dd').at(2).text()).toBe('test-cube-api-url'); - - expect(wrapper.findAll('dt').at(3).text()).toBe('Cube API key'); - expect(wrapper.findAll('dd').at(3).text()).toBe('****************'); - }); - - it('does not render settings with empty values', () => { - createComponent({ - configuratorConnectionString: '', - collectorHost: '', - cubeApiBaseUrl: '', - cubeApiKey: '', - }); - - expect(wrapper.findAll('dt')).toHaveLength(0); - expect(wrapper.findAll('dd')).toHaveLength(0); - }); - - it('masks sensitive values with asterisks', () => { - createComponent({ - configuratorConnectionString: 'sensitive-connection-string', - cubeApiKey: 'sensitive-api-key', - }); - - expect(wrapper.findAll('dd').at(0).text()).toBe('****************'); - expect(wrapper.findAll('dd').at(3).text()).toBe('****************'); - }); - - it('limits the masked value length to 16 characters', () => { - createComponent({ - configuratorConnectionString: 'a-very-long-sensitive-connection-string', - cubeApiKey: 'a-very-long-sensitive-api-key', - }); - - expect(wrapper.findAll('dd').at(0).text()).toHaveLength(16); - expect(wrapper.findAll('dd').at(3).text()).toHaveLength(16); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/self_managed_provider_card_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/self_managed_provider_card_spec.js deleted file mode 100644 index 8014ccd3d367ac..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/self_managed_provider_card_spec.js +++ /dev/null @@ -1,284 +0,0 @@ -import { nextTick } from 'vue'; -import { GlSprintf, GlLink } from '@gitlab/ui'; - -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import SelfManagedProviderCard from 'ee/product_analytics/onboarding/components/providers/self_managed_provider_card.vue'; -import ProviderSettingsPreview from 'ee/product_analytics/onboarding/components/providers/provider_settings_preview.vue'; -import ClearProjectSettingsModal from 'ee/product_analytics/onboarding/components/providers/clear_project_settings_modal.vue'; -import ProviderSettingsForm from 'ee/product_analytics/onboarding/components/providers/provider_settings_form.vue'; -import { - getEmptyProjectLevelAnalyticsProviderSettings, - getPartialProjectLevelAnalyticsProviderSettings, - getProjectLevelAnalyticsProviderSettings, -} from '../../../mock_data'; - -describe('SelfManagedProviderCard', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const findProviderSettingsPreview = () => wrapper.findComponent(ProviderSettingsPreview); - const findConnectSelfManagedProviderBtn = () => - wrapper.findByTestId('connect-your-own-provider-btn'); - const findUseInstanceConfigurationCheckbox = () => - wrapper.findByTestId('use-instance-configuration-checkbox'); - const findLink = () => wrapper.findComponent(GlLink); - const findClearSettingsModal = () => wrapper.findComponent(ClearProjectSettingsModal); - const findEditSettingsModal = () => wrapper.findByTestId('edit-project-level-settings-modal'); - const findProviderSettingsForm = () => wrapper.findComponent(ProviderSettingsForm); - - const createWrapper = (props = {}, provide = {}) => { - wrapper = shallowMountExtended(SelfManagedProviderCard, { - propsData: { - projectSettings: getProjectLevelAnalyticsProviderSettings(), - ...props, - }, - provide: { - isInstanceConfiguredWithSelfManagedAnalyticsProvider: true, - ...provide, - }, - stubs: { - GlSprintf, - }, - }); - }; - - const initProvider = () => { - findConnectSelfManagedProviderBtn().vm.$emit('click'); - return waitForPromises(); - }; - - const checkUseInstanceConfiguration = (checked) => { - findUseInstanceConfigurationCheckbox().vm.$emit('input', checked); - }; - - const itShouldUseEditSettingsModal = () => { - describe('when clicking setup', () => { - beforeEach(() => initProvider()); - - it('should show the settings modal', () => { - expect(findEditSettingsModal().props('visible')).toBe(true); - expect(findEditSettingsModal().props('title')).toBe('Edit project provider settings'); - }); - - it('should hide the modal when it is closed externally', async () => { - findEditSettingsModal().vm.$emit('change', false); - await nextTick(); - - expect(findEditSettingsModal().props('visible')).toBe(false); - }); - - it('should hide the modal when settings form emits "canceled"', async () => { - findProviderSettingsForm().vm.$emit('canceled'); - await nextTick(); - - expect(findEditSettingsModal().props('visible')).toBe(false); - }); - - it('should select the provider when the settings form emits "saved"', async () => { - await wrapper.setProps({ - projectSettings: getProjectLevelAnalyticsProviderSettings(), - }); - findProviderSettingsForm().vm.$emit('saved'); - await nextTick(); - - expect(findEditSettingsModal().props('visible')).toBe(false); - expect(wrapper.emitted('confirm')).toHaveLength(1); - expect(wrapper.emitted('confirm').at(0)).toStrictEqual(['file-mock']); - }); - }); - }; - - const itShouldUseClearSettingsModal = () => { - it('should show the clear settings modal', async () => { - await initProvider(); - - const modal = findClearSettingsModal(); - expect(modal.props('visible')).toBe(true); - expect(modal.text()).toContain( - 'This project has analytics provider settings configured. If you continue, the settings for projects will be reset so that provider settings for the instance can be used.', - ); - }); - - it('should hide the modal when it emits "hide"', async () => { - await initProvider(); - - findClearSettingsModal().vm.$emit('hide'); - await nextTick(); - - expect(findClearSettingsModal().props('visible')).toBe(false); - }); - - it('should select the provider when the modal emits "cleared"', async () => { - await initProvider(); - - await wrapper.setProps({ - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }); - findClearSettingsModal().vm.$emit('cleared'); - await nextTick(); - - expect(wrapper.emitted('confirm')).toEqual([['file-mock']]); - }); - }; - - describe('default behaviour', () => { - beforeEach(() => createWrapper()); - - it('should render a title and description', () => { - expect(wrapper.text()).toContain('Self-managed provider'); - expect(wrapper.text()).toContain( - 'Manage your own analytics provider to process, store, and query analytics data.', - ); - }); - - it('should show "Use instance provider settings" checkbox', () => { - expect(findUseInstanceConfigurationCheckbox().exists()).toBe(true); - }); - }); - - describe('when instance config is a GitLab-managed provider', () => { - it('should not show "Use instance provider settings" checkbox', () => { - createWrapper( - {}, - { - isInstanceConfiguredWithSelfManagedAnalyticsProvider: false, - }, - ); - - expect(findUseInstanceConfigurationCheckbox().exists()).toBe(false); - }); - }); - - describe('"Use instance provider settings" checkbox default state', () => { - it.each` - defaultUseInstanceConfiguration | expectedCheckedState - ${true} | ${'true'} - ${false} | ${undefined} - `( - 'when state is $defaultUseInstanceConfiguration', - ({ defaultUseInstanceConfiguration, expectedCheckedState }) => { - createWrapper( - {}, - { - defaultUseInstanceConfiguration, - }, - ); - - expect(findUseInstanceConfigurationCheckbox().attributes('checked')).toBe( - expectedCheckedState, - ); - }, - ); - }); - - describe('when no project provider settings are configured', () => { - beforeEach(() => { - return createWrapper({ - projectSettings: getEmptyProjectLevelAnalyticsProviderSettings(), - }); - }); - - describe('when "Use instance provider settings" is checked', () => { - beforeEach(() => checkUseInstanceConfiguration(true)); - - it('should inform user instance-settings will be used', () => { - expect(wrapper.text()).toContain( - 'Your instance will be created on the provider configured in your instance settings.', - ); - }); - - it('does not render the link to the public helm-charts project', () => { - expect(findLink().exists()).toBe(false); - }); - - describe('when selecting provider', () => { - beforeEach(() => initProvider()); - - it('should emit "confirm" event', () => { - expect(wrapper.emitted('confirm')).toHaveLength(1); - }); - }); - }); - - describe('when "Use instance provider settings" is unchecked', () => { - beforeEach(() => checkUseInstanceConfiguration(false)); - - it('renders the link to the public helm-charts project', () => { - expect(findLink().attributes('href')).toBe( - 'https://gitlab.com/gitlab-org/analytics-section/product-analytics/helm-charts', - ); - }); - - itShouldUseEditSettingsModal(); - }); - }); - - describe('when some project provider settings are configured', () => { - beforeEach(() => { - return createWrapper({ - projectSettings: getPartialProjectLevelAnalyticsProviderSettings(), - }); - }); - - describe('when "Use instance provider settings" is checked', () => { - beforeEach(() => checkUseInstanceConfiguration(true)); - - it('should not show summary of existing project-level settings', () => { - expect(findProviderSettingsPreview().exists()).toBe(false); - }); - - itShouldUseClearSettingsModal(); - }); - - describe('when "Use instance provider settings" is unchecked', () => { - beforeEach(() => checkUseInstanceConfiguration(false)); - - it('should not show summary of existing project-level settings', () => { - expect(findProviderSettingsPreview().exists()).toBe(false); - }); - - itShouldUseEditSettingsModal(); - }); - }); - - describe('when all project provider settings are configured', () => { - beforeEach(() => { - return createWrapper({ - projectSettings: getProjectLevelAnalyticsProviderSettings(), - }); - }); - - describe('when "Use instance provider settings" is checked', () => { - beforeEach(() => checkUseInstanceConfiguration(true)); - - it('should not show summary of existing project-level settings', () => { - expect(findProviderSettingsPreview().exists()).toBe(false); - }); - - itShouldUseClearSettingsModal(); - }); - - describe('when "Use instance provider settings" is unchecked', () => { - beforeEach(() => checkUseInstanceConfiguration(false)); - - it('should show summary of existing project-level settings', () => { - expect(findProviderSettingsPreview().props()).toMatchObject({ - configuratorConnectionString: 'https://configurator.example.com', - collectorHost: 'https://collector.example.com', - cubeApiBaseUrl: 'https://cubejs.example.com', - cubeApiKey: 'abc-123', - }); - }); - - describe('when selecting provider', () => { - beforeEach(() => initProvider()); - - it('should emit "confirm" event', () => { - expect(wrapper.emitted('confirm')).toEqual([['file-mock']]); - }); - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/components/provider/utils_spec.js b/ee/spec/frontend/product_analytics/onboarding/components/provider/utils_spec.js deleted file mode 100644 index c57eb0b37fb42d..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/components/provider/utils_spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import { InMemoryCache } from '@apollo/client/cache'; - -import getProductAnalyticsProjectSettings from 'ee/product_analytics/graphql/queries/get_product_analytics_project_settings.query.graphql'; -import { - projectSettingsValidator, - getProjectSettingsValidationErrors, - updateProjectSettingsApolloCache, -} from 'ee/product_analytics/onboarding/components/providers/utils'; -import { - getPartialProjectLevelAnalyticsProviderSettings, - TEST_PROJECT_FULL_PATH, - TEST_PROJECT_ID, -} from 'ee_jest/product_analytics/mock_data'; - -describe('product analytics onboarding provider utils', () => { - describe('projectSettingsValidator', () => { - const validProp = { - productAnalyticsConfiguratorConnectionString: 'https://test:test@configurator.example.com', - productAnalyticsDataCollectorHost: 'https://collector.example.com', - cubeApiBaseUrl: 'https://cube.example.com', - cubeApiKey: '123-some-cube-key', - }; - const { cubeApiKey, ...propMissingCube } = validProp; - - const testCases = [ - ['valid settings', validProp, true], - ['null value', { ...validProp, cubeApiKey: null }, true], - ['missing property', propMissingCube, false], - ['unexpected property', { ...validProp, someUnexpectedProp: 'test' }, false], - ['invalid value type', { ...validProp, cubeApiKey: 123 }, false], - ['empty object', {}, false], - ]; - - it.each(testCases)('%s', (_, prop, expected) => { - expect(projectSettingsValidator(prop)).toBe(expected); - }); - }); - - describe('getProjectSettingsValidationErrors', () => { - const validPayload = { - productAnalyticsConfiguratorConnectionString: 'https://configurator.example.com', - productAnalyticsDataCollectorHost: 'https://collector.example.com', - cubeApiBaseUrl: 'https://cube.example.com', - cubeApiKey: 'abc', - }; - - it.each` - payload | expected - ${{ productAnalyticsConfiguratorConnectionString: 'not-a-url' }} | ${{ productAnalyticsConfiguratorConnectionString: 'Enter a valid URL' }} - ${{ productAnalyticsConfiguratorConnectionString: '/not/an/absolute/url' }} | ${{ productAnalyticsConfiguratorConnectionString: 'Enter a valid URL' }} - ${{ productAnalyticsConfiguratorConnectionString: '' }} | ${{ productAnalyticsConfiguratorConnectionString: 'This field is required' }} - ${{ productAnalyticsDataCollectorHost: 'not-a-url' }} | ${{ productAnalyticsDataCollectorHost: 'Enter a valid URL' }} - ${{ productAnalyticsDataCollectorHost: '/not/an/absolute/url' }} | ${{ productAnalyticsDataCollectorHost: 'Enter a valid URL' }} - ${{ productAnalyticsDataCollectorHost: '' }} | ${{ productAnalyticsDataCollectorHost: 'This field is required' }} - ${{ cubeApiBaseUrl: 'not-a-url' }} | ${{ cubeApiBaseUrl: 'Enter a valid URL' }} - ${{ cubeApiBaseUrl: '/not/an/absolute/url' }} | ${{ cubeApiBaseUrl: 'Enter a valid URL' }} - ${{ cubeApiBaseUrl: '' }} | ${{ cubeApiBaseUrl: 'This field is required' }} - ${{ cubeApiKey: '' }} | ${{ cubeApiKey: 'This field is required' }} - ${{}} | ${{}} - `('returns $expected for $payload', ({ expected, payload }) => { - expect(getProjectSettingsValidationErrors({ ...validPayload, ...payload })).toEqual(expected); - }); - }); - - describe('updateProjectSettingsApolloCache', () => { - let apolloCache; - const projectPath = TEST_PROJECT_FULL_PATH; - const existingSettings = getPartialProjectLevelAnalyticsProviderSettings(); - - beforeEach(() => { - apolloCache = new InMemoryCache(); - - apolloCache.writeQuery({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath }, - data: { - project: { - id: TEST_PROJECT_ID, - productAnalyticsSettings: existingSettings, - __typename: 'Project', - }, - }, - }); - }); - - it.each([ - { - productAnalyticsConfiguratorConnectionString: 'https://new-configurator.example.com', - productAnalyticsDataCollectorHost: 'https://new-collector.example.com', - cubeApiBaseUrl: 'https://new-cube.example.com', - cubeApiKey: 'new-cube-key', - }, - { - productAnalyticsConfiguratorConnectionString: null, - productAnalyticsDataCollectorHost: null, - cubeApiBaseUrl: null, - cubeApiKey: null, - }, - ])('updates the cache with the provided settings', (updatedSettings) => { - updateProjectSettingsApolloCache(apolloCache, projectPath, updatedSettings); - - const data = apolloCache.readQuery({ - query: getProductAnalyticsProjectSettings, - variables: { projectPath }, - }); - - expect(data.project.productAnalyticsSettings).toEqual(updatedSettings); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/onboarding_setup_spec.js b/ee/spec/frontend/product_analytics/onboarding/onboarding_setup_spec.js deleted file mode 100644 index 33206b708b103b..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/onboarding_setup_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import VueApollo from 'vue-apollo'; -import Vue from 'vue'; -import { GlLoadingIcon } from '@gitlab/ui'; -import ProductAnalyticsSetupView from 'ee/product_analytics/onboarding/onboarding_setup.vue'; -import InstrumentationInstructionsSdkDetails from 'ee/product_analytics/onboarding/components/instrumentation_instructions_sdk_details.vue'; -import InstrumentationInstructions from 'ee/product_analytics/onboarding/components/instrumentation_instructions.vue'; -import getProjectJitsuKeyQuery from 'ee/product_analytics/graphql/queries/get_project_tracking_key.query.graphql'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { - TEST_COLLECTOR_HOST, - TEST_TRACKING_KEY, -} from 'ee_jest/analytics/analytics_dashboards/mock_data'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { getTrackingKeyResponse, TEST_PROJECT_FULL_PATH } from '../mock_data'; - -const { i18n } = ProductAnalyticsSetupView; - -Vue.use(VueApollo); - -describe('ProductAnalyticsSetupView', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const fatalError = new Error('GraphQL networkError'); - const defaultProps = { - isInitialSetup: false, - dashboardsPath: '/path/to/dashboard', - }; - - const mockApolloFatalError = jest.fn().mockRejectedValue(fatalError); - const mockApolloSuccess = jest.fn().mockResolvedValue(getTrackingKeyResponse(TEST_TRACKING_KEY)); - - const findTitle = () => wrapper.findByTestId('title'); - const findDescription = () => wrapper.findByTestId('description'); - const findHelpLink = () => wrapper.findByTestId('help-link'); - const findIntroduction = () => wrapper.findByTestId('introduction'); - const findBackToDashboardsButton = () => wrapper.findByTestId('back-to-dashboards-button'); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findSdkDetails = () => wrapper.findComponent(InstrumentationInstructionsSdkDetails); - const findInstrumentationInstructions = () => wrapper.findComponent(InstrumentationInstructions); - - const createWrapper = (props = {}, provide = {}, apolloMock = mockApolloSuccess) => { - wrapper = mountExtended(ProductAnalyticsSetupView, { - apolloProvider: createMockApollo([[getProjectJitsuKeyQuery, apolloMock]]), - propsData: { - ...defaultProps, - ...props, - }, - provide: { - namespaceFullPath: TEST_PROJECT_FULL_PATH, - collectorHost: TEST_COLLECTOR_HOST, - trackingKey: TEST_TRACKING_KEY, - ...provide, - }, - }); - }; - - describe('when mounted', () => { - it('should render the heading section', () => { - createWrapper(); - - expect(findTitle().text()).toContain(i18n.title); - expect(findHelpLink().text()).toContain(i18n.learnMore); - expect(findHelpLink().attributes('href')).toBe(ProductAnalyticsSetupView.docsPath); - }); - - it('does not render the loading icon', () => { - createWrapper(); - - expect(findLoadingIcon().exists()).toBe(false); - }); - - it.each` - isInitialSetup | description - ${true} | ${i18n.initialSetupDescription} - ${false} | ${i18n.description} - `( - 'should render the right heading when "isInitialSetup" is "$isInitialSetup"', - ({ isInitialSetup, description }) => { - createWrapper({ isInitialSetup }); - - expect(findDescription().text()).toContain(description); - expect(findIntroduction().exists()).toBe(isInitialSetup); - expect(findBackToDashboardsButton().exists()).toBe(!isInitialSetup); - }, - ); - }); - - describe('when no trackingKey is provided', () => { - it('displays the loading icon', () => { - createWrapper({}, { trackingKey: null }); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('displays the SDK details when the query succeeds', async () => { - createWrapper({}, { trackingKey: null }); - - await waitForPromises(); - - const sdkDetails = findSdkDetails(); - - expect(sdkDetails.exists()).toBe(true); - expect(sdkDetails.props('trackingKey')).toBe(TEST_TRACKING_KEY); - }); - - it('displays the instrumentation instructions when the query succeeds', async () => { - createWrapper({}, { trackingKey: null }); - - await waitForPromises(); - - const instrumentationInstructions = findInstrumentationInstructions(); - - expect(instrumentationInstructions.exists()).toBe(true); - expect(instrumentationInstructions.props('trackingKey')).toBe(TEST_TRACKING_KEY); - }); - - it('emits an error when the query errors', async () => { - createWrapper({}, { trackingKey: null }, mockApolloFatalError); - - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([[fatalError]]); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/onboarding_view_spec.js b/ee/spec/frontend/product_analytics/onboarding/onboarding_view_spec.js deleted file mode 100644 index aa7fbbc87a7e77..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/onboarding_view_spec.js +++ /dev/null @@ -1,170 +0,0 @@ -import { nextTick } from 'vue'; -import { GlLoadingIcon } from '@gitlab/ui'; -import ProductAnalyticsOnboardingView from 'ee/product_analytics/onboarding/onboarding_view.vue'; -import ProductAnalyticsOnboardingSetup from 'ee/product_analytics/onboarding/onboarding_setup.vue'; -import ProductAnalyticsOnboardingState from 'ee/product_analytics/onboarding/components/onboarding_state.vue'; -import ProviderSelectionView from 'ee/product_analytics/onboarding/components/providers/provider_selection_view.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/alert'; -import { - STATE_LOADING_INSTANCE, - STATE_CREATE_INSTANCE, - STATE_WAITING_FOR_EVENTS, -} from 'ee/product_analytics/onboarding/constants'; -import { - TEST_TRACKING_KEY, - TEST_COLLECTOR_HOST, -} from 'ee_jest/analytics/analytics_dashboards/mock_data'; -import { TEST_PROJECT_FULL_PATH } from '../mock_data'; - -jest.mock('~/alert'); - -describe('ProductAnalyticsOnboardingView', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const $router = { - push: jest.fn(), - }; - - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findProviderSelection = () => wrapper.findComponent(ProviderSelectionView); - const findSetupView = () => wrapper.findComponent(ProductAnalyticsOnboardingSetup); - const findStateComponent = () => wrapper.findComponent(ProductAnalyticsOnboardingState); - - const createWrapper = (provide = {}) => { - wrapper = shallowMountExtended(ProductAnalyticsOnboardingView, { - provide: { - namespaceFullPath: TEST_PROJECT_FULL_PATH, - collectorHost: TEST_COLLECTOR_HOST, - trackingKey: TEST_TRACKING_KEY, - dashboardsPath: '/analytics/dashboards', - canSelectGitlabManagedProvider: false, - ...provide, - }, - stubs: { - OnboardingSetup: ProductAnalyticsOnboardingSetup, - }, - mocks: { - $router, - }, - }); - }; - - const emitStateChange = async (state) => { - await findStateComponent().vm.$emit('change', state); - await nextTick(); - }; - - const expectAlertOnError = async ({ finder, captureError, message }) => { - const error = new Error('oh no!'); - - finder().vm.$emit('error', error); - - await nextTick(); - - expect(createAlert).toHaveBeenCalledWith({ - message, - captureError, - error, - }); - }; - - afterEach(() => { - createAlert.mockClear(); - }); - - describe('when mounted', () => { - beforeEach(() => { - createWrapper(); - }); - - it('shows the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not show the provider selection view', () => { - expect(findProviderSelection().exists()).toBe(false); - }); - - it('does not show the setup view', () => { - expect(findSetupView().exists()).toBe(false); - }); - - it('creates an onboarding state component', () => { - expect(findStateComponent().props()).toMatchObject({ - stateProp: '', - pollState: false, - }); - }); - }); - - describe('create and loading instance', () => { - beforeEach(() => { - createWrapper({ - canSelectGitlabManagedProvider: true, - }); - }); - - it.each([STATE_CREATE_INSTANCE, STATE_LOADING_INSTANCE])( - 'renders provider_selection when the state is "%s"', - async (state) => { - await emitStateChange(state); - - expect(findProviderSelection().props('loadingInstance')).toBe( - state === STATE_LOADING_INSTANCE, - ); - }, - ); - }); - - describe('when waiting for events', () => { - beforeEach(() => { - createWrapper(); - return emitStateChange(STATE_WAITING_FOR_EVENTS); - }); - - it('renders the setup view', () => { - expect(findSetupView().props('isInitialSetup')).toBe(true); - }); - }); - - describe('provider selection component events', () => { - beforeEach(() => { - createWrapper({ - canSelectGitlabManagedProvider: true, - }); - return emitStateChange(STATE_CREATE_INSTANCE); - }); - - it(`activates polling on initialized`, async () => { - findProviderSelection().vm.$emit('initialized'); - - await nextTick(); - - expect(findStateComponent().props('pollState')).toBe(true); - }); - }); - - describe('state component events', () => { - beforeEach(() => { - createWrapper(); - }); - - it('routes to "index" on complete', async () => { - findStateComponent().vm.$emit('complete'); - - await nextTick(); - - expect($router.push).toHaveBeenCalledWith({ name: 'index' }); - }); - - it('creates an alert on error with a fixed message', () => { - expectAlertOnError({ - finder: findStateComponent, - captureError: false, - message: 'An error occurred while fetching data. Refresh the page to try again.', - }); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/onboarding/settings_instrumentation_instructions_spec.js b/ee/spec/frontend/product_analytics/onboarding/settings_instrumentation_instructions_spec.js deleted file mode 100644 index de3fdaf4fff643..00000000000000 --- a/ee/spec/frontend/product_analytics/onboarding/settings_instrumentation_instructions_spec.js +++ /dev/null @@ -1,82 +0,0 @@ -import { nextTick } from 'vue'; -import { GlModal, GlLink } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { - TEST_COLLECTOR_HOST, - TEST_TRACKING_KEY, -} from 'ee_jest/analytics/analytics_dashboards/mock_data'; -import SettingsInstrumentationInstructions from 'ee/product_analytics/onboarding/settings_instrumentation_instructions.vue'; -import InstrumentationInstructions from 'ee/product_analytics/onboarding/components/instrumentation_instructions.vue'; - -import { stubComponent } from 'helpers/stub_component'; - -describe('ProductAnalyticsSettingsInstrumentationInstructions', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - const dashboardsPath = '/foo/bar/dashboards'; - - const findModal = () => wrapper.findComponent(GlModal); - const findLink = () => wrapper.findComponent(GlLink); - const findInstrumentationInstructionsButton = () => - wrapper.findByRole('button', { - name: 'View instrumentation instructions', - }); - const findInstrumentationInstructions = () => wrapper.findComponent(InstrumentationInstructions); - - const createWrapper = (props = {}) => { - wrapper = mountExtended(SettingsInstrumentationInstructions, { - provide: { - collectorHost: TEST_COLLECTOR_HOST, - }, - propsData: { - dashboardsPath, - trackingKey: TEST_TRACKING_KEY, - ...props, - }, - stubs: { - GlModal: stubComponent(GlModal), - }, - }); - }; - - describe('default behaviour', () => { - beforeEach(() => createWrapper()); - - it('has button to show instrumentation instructions', () => { - expect(findInstrumentationInstructionsButton().exists()).toBe(true); - }); - - it('shows modal when clicking button', async () => { - findInstrumentationInstructionsButton().trigger('click'); - await nextTick(); - - expect(findModal().props('visible')).toBe(true); - }); - - it('hides modal when it is dismissed', async () => { - const modal = findModal(); - - findInstrumentationInstructionsButton().trigger('click'); - await nextTick(); - - modal.vm.$emit('change', false); - await nextTick(); - - expect(findModal().props('visible')).toBe(false); - }); - - it('shows instrumentation instructions in modal', async () => { - findInstrumentationInstructionsButton().trigger('click'); - await nextTick(); - - expect(findInstrumentationInstructions().props()).toMatchObject({ - dashboardsPath, - trackingKey: TEST_TRACKING_KEY, - }); - }); - - it('shows the link to the dashboards path', () => { - expect(findLink().attributes('href')).toBe(dashboardsPath); - }); - }); -}); diff --git a/ee/spec/frontend/product_analytics/shared/analytics_clipboard_input_spec.js b/ee/spec/frontend/product_analytics/shared/analytics_clipboard_input_spec.js deleted file mode 100644 index d2b1541b0ee918..00000000000000 --- a/ee/spec/frontend/product_analytics/shared/analytics_clipboard_input_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { nextTick } from 'vue'; -import { GlButton, GlFormGroup, GlFormInput, GlTooltip, GlFormInputGroup } from '@gitlab/ui'; -import AnalyticsClipboardInput, { - TOOLTIP_ALERT_TIMEOUT, -} from 'ee/product_analytics/shared/analytics_clipboard_input.vue'; -import waitForPromises from 'helpers/wait_for_promises'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -const { i18n } = AnalyticsClipboardInput; - -const TEST_LABEL = 'SDK key'; -const TEST_DESCRIPTION = 'The SDK key'; -const TEST_VALUE = 'XyZ'; - -describe('AnalyticsClipboardInput', () => { - /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ - let wrapper; - - const findFormGroup = () => wrapper.findComponent(GlFormGroup); - const findButton = () => wrapper.findComponent(GlButton); - const findInput = () => wrapper.findComponent(GlFormInput); - const findTooltip = () => wrapper.findComponent(GlTooltip); - - const createWrapper = () => { - wrapper = shallowMountExtended(AnalyticsClipboardInput, { - propsData: { - label: TEST_LABEL, - description: TEST_DESCRIPTION, - value: TEST_VALUE, - }, - stubs: { - 'gl-form-group': GlFormGroup, - 'gl-form-input-group': GlFormInputGroup, - }, - }); - }; - - beforeEach(() => { - createWrapper(); - }); - - it('renders label and description', () => { - expect(findFormGroup().attributes('label')).toBe(TEST_LABEL); - expect(findFormGroup().props('labelDescription')).toBe(TEST_DESCRIPTION); - expect(findInput().attributes('value')).toBe(TEST_VALUE); - }); - - it('copies provided value to clipboard and updates the tooltip', async () => { - jest.spyOn(navigator.clipboard, 'writeText'); - - findButton().vm.$emit('click'); - - await waitForPromises(); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(TEST_VALUE); - expect(findTooltip().attributes('title')).toBe(i18n.copied); - - jest.advanceTimersByTime(TOOLTIP_ALERT_TIMEOUT); - - await nextTick(); - - expect(findTooltip().attributes('title')).toBe(i18n.copyToClipboard); - }); - - it('shows hint when copying fails', async () => { - jest.spyOn(navigator.clipboard, 'writeText').mockRejectedValue(new Error('Failed')); - - expect(findFormGroup().attributes('description')).toBe(''); - - findButton().vm.$emit('click'); - - await waitForPromises(); - - expect(findFormGroup().attributes('description')).toBe(i18n.failedToCopy); - expect(findTooltip().attributes('title')).toBe(i18n.copyToClipboard); - }); -}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fb9421ede24c43..bf380d92339c45 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7983,18 +7983,12 @@ msgstr "" msgid "Analytics|Exclude anonymous users" msgstr "" -msgid "Analytics|Failed to clear project-level settings. Please try again or %{linkStart}clear them manually%{linkEnd}." -msgstr "" - msgid "Analytics|Failed to fetch data" msgstr "" msgid "Analytics|Failed to load dashboard" msgstr "" -msgid "Analytics|Failed to update project-level settings. Please try again." -msgstr "" - msgid "Analytics|Invalid dashboard configuration" msgstr "" @@ -25868,9 +25862,6 @@ msgstr "" msgid "Enter a search query to find more branches, or use * to create a wildcard." msgstr "" -msgid "Enter a valid URL" -msgstr "" - msgid "Enter admin mode" msgstr "" @@ -49210,210 +49201,24 @@ msgstr "" msgid "Proceed with caution." msgstr "" -msgid "Product Analytics" -msgstr "" - msgid "Product analytics" msgstr "" -msgid "ProductAnalytics|1. Add the NPM package to your package.json using your preferred package manager" -msgstr "" - -msgid "ProductAnalytics|2. Import the new package into your JS code" -msgstr "" - -msgid "ProductAnalytics|3. Initiate the tracking" -msgstr "" - -msgid "ProductAnalytics|A deployed instance of the %{linkStart}helm-charts%{linkEnd} project." -msgstr "" - -msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window" -msgstr "" - -msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:" -msgstr "" - -msgid "ProductAnalytics|Additional permissions required" -msgstr "" - -msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}." -msgstr "" - -msgid "ProductAnalytics|An analytics provider has been successfully created, but it has not received any events yet. To continue with the setup, instrument your application and start sending events." -msgstr "" - -msgid "ProductAnalytics|An error occurred while fetching data. Refresh the page to try again." -msgstr "" - -msgid "ProductAnalytics|An error occurred while fetching project settings. Refresh the page to try again." -msgstr "" - -msgid "ProductAnalytics|Analyze your product with Product Analytics" -msgstr "" - msgid "ProductAnalytics|Audience" msgstr "" -msgid "ProductAnalytics|Back to dashboards" -msgstr "" - -msgid "ProductAnalytics|Collector host" -msgstr "" - -msgid "ProductAnalytics|Configurator connection string" -msgstr "" - -msgid "ProductAnalytics|Connect your own provider" -msgstr "" - -msgid "ProductAnalytics|Contact our sales team" -msgstr "" - -msgid "ProductAnalytics|Contact the GitLab administrator or project maintainer to onboard this project with product analytics. %{linkStart}Learn more%{linkEnd}." -msgstr "" - -msgid "ProductAnalytics|Continue set up" -msgstr "" - -msgid "ProductAnalytics|Creating your product analytics instance…" -msgstr "" - -msgid "ProductAnalytics|Cube API URL" -msgstr "" - msgid "ProductAnalytics|Cube API key" msgstr "" -msgid "ProductAnalytics|Details on how to configure product analytics to collect data." -msgstr "" - -msgid "ProductAnalytics|Edit project provider settings" -msgstr "" - -msgid "ProductAnalytics|For more information, see the %{linkStart}docs%{linkEnd}." -msgstr "" - -msgid "ProductAnalytics|For the product analytics dashboard to start showing you some data, you need to add the analytics tracking code to your project." -msgstr "" - -msgid "ProductAnalytics|For this option, you need:" -msgstr "" - -msgid "ProductAnalytics|For this option:" -msgstr "" - -msgid "ProductAnalytics|GitLab-managed provider" -msgstr "" - -msgid "ProductAnalytics|I agree to event collection and processing in this region." -msgstr "" - -msgid "ProductAnalytics|Instrument your application" -msgstr "" - -msgid "ProductAnalytics|Loading instance" -msgstr "" - -msgid "ProductAnalytics|Manage your own analytics provider to process, store, and query analytics data." -msgstr "" - -msgid "ProductAnalytics|Override the instance analytics configuration for this project." -msgstr "" - -msgid "ProductAnalytics|Product analytics onboarding" -msgstr "" - -msgid "ProductAnalytics|Reset existing project provider settings" -msgstr "" - -msgid "ProductAnalytics|SDK application ID" -msgstr "" - -msgid "ProductAnalytics|SDK clients" -msgstr "" - -msgid "ProductAnalytics|SDK host" -msgstr "" - -msgid "ProductAnalytics|Self-managed provider" -msgstr "" - -msgid "ProductAnalytics|Set up Product Analytics to track how your product is performing. Combine analytics with your GitLab data to better understand where you can improve your product and development processes. %{linkStart}Learn more%{linkEnd}." -msgstr "" - msgid "ProductAnalytics|Snowplow configurator connection string" msgstr "" -msgid "ProductAnalytics|The Product Analytics Beta on GitLab.com is offered only in the Google Cloud Platform zone %{zone}." -msgstr "" - msgid "ProductAnalytics|The connection string for your Snowplow configurator instance." msgstr "" -msgid "ProductAnalytics|The connection string for your configurator instance." -msgstr "" - -msgid "ProductAnalytics|The receiver of tracking events" -msgstr "" - -msgid "ProductAnalytics|The sender of tracking events" -msgstr "" - -msgid "ProductAnalytics|The system is creating your analytics provider. In the meantime, you can instrument your application." -msgstr "" - -msgid "ProductAnalytics|This might take a while, feel free to navigate away from this page and come back later." -msgstr "" - -msgid "ProductAnalytics|This project has analytics provider settings configured. If you continue, the settings for projects will be reset so that GitLab-managed provider settings can be used." -msgstr "" - -msgid "ProductAnalytics|This project has analytics provider settings configured. If you continue, the settings for projects will be reset so that provider settings for the instance can be used." -msgstr "" - -msgid "ProductAnalytics|To continue, you must agree to event storage and processing in this region." -msgstr "" - -msgid "ProductAnalytics|To instrument your application, select one of the options below. After an option has been instrumented and data is being collected, this page will progress to the next step." -msgstr "" - -msgid "ProductAnalytics|Track the performance of your product, and optimize your product and development processes." -msgstr "" - -msgid "ProductAnalytics|Uncheck if you would like to configure a different provider for this project." -msgstr "" - -msgid "ProductAnalytics|Use GitLab-managed provider" -msgstr "" - -msgid "ProductAnalytics|Use a GitLab-managed infrastructure to process, store, and query analytics events data." -msgstr "" - -msgid "ProductAnalytics|Use instance provider settings" -msgstr "" - msgid "ProductAnalytics|Used to retrieve dashboard data from the Cube instance." msgstr "" -msgid "ProductAnalytics|Using JS module" -msgstr "" - -msgid "ProductAnalytics|Valid project settings." -msgstr "" - -msgid "ProductAnalytics|Waiting for events" -msgstr "" - -msgid "ProductAnalytics|You can instrument your application using a JS module or an HTML script. Follow the instructions below for the option you prefer." -msgstr "" - -msgid "ProductAnalytics|Your instance will be created on the provider configured in your instance settings." -msgstr "" - -msgid "ProductAnalytics|Your instance will be created on this provider:" -msgstr "" - msgid "ProductUsageData|Dismiss product usage data collection notice" msgstr "" @@ -50914,9 +50719,6 @@ msgstr "" msgid "ProjectSettings|Infrastructure" msgstr "" -msgid "ProjectSettings|Instrumentation details" -msgstr "" - msgid "ProjectSettings|Internal" msgstr "" @@ -51244,9 +51046,6 @@ msgstr "" msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab. GitLab Pages uses a caching mechanism for efficiency. Your changes may not take effect until that cache is invalidated, which usually takes less than a minute." msgstr "" -msgid "ProjectSettings|Your project is set up. %{instructionsLinkStart}View instrumentation instructions%{instructionsLinkEnd} and %{dashboardsLinkStart}Analytics Dashboards%{dashboardsLinkEnd}." -msgstr "" - msgid "ProjectSetting|already in use" msgstr "" @@ -60311,9 +60110,6 @@ msgstr "" msgid "Select an iteration" msgstr "" -msgid "Select an option" -msgstr "" - msgid "Select artifacts" msgstr "" @@ -70364,9 +70160,6 @@ msgstr "" msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}" msgstr "" -msgid "Using HTML script" -msgstr "" - msgid "Using required encryption strategy when encrypted field is missing!" msgstr "" -- GitLab From 93c9fb489b69c525493d7df075789ca5cdf73217 Mon Sep 17 00:00:00 2001 From: Jiaan Louw Date: Mon, 8 Sep 2025 16:30:47 +0200 Subject: [PATCH 2/2] Remove onboarding QA specs --- ...e_end_string_concatenation_indentation.yml | 1 - .../analyze/analytics_dashboards/initial.rb | 33 ------------ .../analyze/analytics_dashboards/setup.rb | 50 ------------------- 3 files changed, 84 deletions(-) delete mode 100644 qa/qa/ee/page/project/analyze/analytics_dashboards/initial.rb delete mode 100644 qa/qa/ee/page/project/analyze/analytics_dashboards/setup.rb diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml index cd3315d2e6d804..cbcb8c50f7bfe1 100644 --- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml +++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml @@ -368,7 +368,6 @@ Layout/LineEndStringConcatenationIndentation: - 'lib/users/internal.rb' - 'qa/qa/ee/flow/product_analytics.rb' - 'qa/qa/ee/page/project/analyze/analytics_dashboards/dashboard.rb' - - 'qa/qa/ee/page/project/analyze/analytics_dashboards/setup.rb' - 'qa/qa/ee/scenario/test/geo.rb' - 'qa/qa/page/admin/overview/users/index.rb' - 'qa/qa/page/base.rb' diff --git a/qa/qa/ee/page/project/analyze/analytics_dashboards/initial.rb b/qa/qa/ee/page/project/analyze/analytics_dashboards/initial.rb deleted file mode 100644 index 3800fd4e3ef84a..00000000000000 --- a/qa/qa/ee/page/project/analyze/analytics_dashboards/initial.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module QA - module EE - module Page - module Project - module Analyze - module AnalyticsDashboards - class Initial < QA::Page::Base - view 'ee/app/assets/javascripts/analytics/analytics_dashboards/components/list/feature_list_item.vue' do - element 'setup-button' - end - - def click_set_up - # need to refresh page due to https://gitlab.com/gitlab-org/analytics-section/product-analytics/devkit/-/issues/41 - wait_for_set_up_button - page.refresh - wait_for_set_up_button - click_element('setup-button') - end - - def wait_for_set_up_button - retry_until(sleep_interval: 2, reload: true) do - has_element?('setup-button') - end - end - end - end - end - end - end - end -end diff --git a/qa/qa/ee/page/project/analyze/analytics_dashboards/setup.rb b/qa/qa/ee/page/project/analyze/analytics_dashboards/setup.rb deleted file mode 100644 index 9df2f31af319c5..00000000000000 --- a/qa/qa/ee/page/project/analyze/analytics_dashboards/setup.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module QA - module EE - module Page - module Project - module Analyze - module AnalyticsDashboards - class Setup < QA::Page::Base - view 'ee/app/assets/javascripts/product_analytics/onboarding/' \ - 'components/instrumentation_instructions_sdk_details.vue' do - element 'sdk-application-id-container' - element 'sdk-host-container' - end - - view 'ee/app/assets/javascripts/product_analytics/onboarding/' \ - 'components/providers/self_managed_provider_card.vue' do - element 'connect-your-own-provider-btn' - end - - view 'ee/app/assets/javascripts/product_analytics/shared/analytics_clipboard_input.vue' do - element 'sdk-value-field' - end - - def connect_your_own_provider - click_element('connect-your-own-provider-btn') - end - - def wait_for_sdk_containers - has_element?('sdk-application-id-container', skip_finished_loading_check: true, wait: 120) - end - - def sdk_application_id - within_element('sdk-application-id-container') do - find_element('sdk-value-field') - end - end - - def sdk_host - within_element('sdk-host-container') do - find_element('sdk-value-field') - end - end - end - end - end - end - end - end -end -- GitLab